pax_global_header00006660000000000000000000000064150106561400014510gustar00rootroot0000000000000052 comment=7fff56f5172c48b6f3aedf17ee14ef5c2533dfd1 flask-3.1.1/000077500000000000000000000000001501065614000126125ustar00rootroot00000000000000flask-3.1.1/.devcontainer/000077500000000000000000000000001501065614000153515ustar00rootroot00000000000000flask-3.1.1/.devcontainer/devcontainer.json000066400000000000000000000006621501065614000207310ustar00rootroot00000000000000{ "name": "pallets/flask", "image": "mcr.microsoft.com/devcontainers/python:3", "customizations": { "vscode": { "settings": { "python.defaultInterpreterPath": "${workspaceFolder}/.venv", "python.terminal.activateEnvInCurrentTerminal": true, "python.terminal.launchArgs": [ "-X", "dev" ] } } }, "onCreateCommand": ".devcontainer/on-create-command.sh" } flask-3.1.1/.devcontainer/on-create-command.sh000077500000000000000000000002451501065614000212020ustar00rootroot00000000000000#!/bin/bash set -e python3 -m venv --upgrade-deps .venv . .venv/bin/activate pip install -r requirements/dev.txt pip install -e . pre-commit install --install-hooks flask-3.1.1/.editorconfig000066400000000000000000000003511501065614000152660ustar00rootroot00000000000000root = true [*] indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf charset = utf-8 max_line_length = 88 [*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] indent_size = 2 flask-3.1.1/.github/000077500000000000000000000000001501065614000141525ustar00rootroot00000000000000flask-3.1.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001501065614000163355ustar00rootroot00000000000000flask-3.1.1/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000011471501065614000207500ustar00rootroot00000000000000--- name: Bug report about: Report a bug in Flask (not other projects which depend on Flask) --- Environment: - Python version: - Flask version: flask-3.1.1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000007771501065614000203400ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Security issue url: https://github.com/pallets/flask/security/advisories/new about: Do not report security issues publicly. Create a private advisory. - name: Questions on GitHub Discussions url: https://github.com/pallets/flask/discussions/ about: Ask questions about your own code on the Discussions tab. - name: Questions on Discord url: https://discord.gg/pallets about: Ask questions about your own code on our Discord chat. flask-3.1.1/.github/ISSUE_TEMPLATE/feature-request.md000066400000000000000000000006401501065614000220000ustar00rootroot00000000000000--- name: Feature request about: Suggest a new feature for Flask --- flask-3.1.1/.github/pull_request_template.md000066400000000000000000000014661501065614000211220ustar00rootroot00000000000000 flask-3.1.1/.github/workflows/000077500000000000000000000000001501065614000162075ustar00rootroot00000000000000flask-3.1.1/.github/workflows/lock.yaml000066400000000000000000000012521501065614000200230ustar00rootroot00000000000000name: Lock inactive closed issues # Lock closed issues that have not received any further activity for two weeks. # This does not close open issues, only humans may do that. It is easier to # respond to new issues with fresh examples rather than continuing discussions # on old issues. on: schedule: - cron: '0 0 * * *' permissions: issues: write pull-requests: write discussions: write concurrency: group: lock jobs: lock: runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 with: issue-inactive-days: 14 pr-inactive-days: 14 discussion-inactive-days: 14 flask-3.1.1/.github/workflows/pre-commit.yaml000066400000000000000000000017271501065614000211560ustar00rootroot00000000000000name: pre-commit on: pull_request: push: branches: [main, stable] jobs: main: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true prune-cache: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 id: setup-python with: python-version-file: pyproject.toml - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ~/.cache/pre-commit key: pre-commit|${{ hashFiles('pyproject.toml', '.pre-commit-config.yaml') }} - run: uv run --locked --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 if: ${{ !cancelled() }} flask-3.1.1/.github/workflows/publish.yaml000066400000000000000000000041061501065614000205420ustar00rootroot00000000000000name: Publish on: push: tags: ['*'] jobs: build: runs-on: ubuntu-latest outputs: hash: ${{ steps.hash.outputs.hash }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true prune-cache: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version-file: pyproject.toml - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - run: uv build - name: generate hash id: hash run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: path: ./dist provenance: needs: [build] permissions: actions: read id-token: write contents: write # Can't pin with hash due to how this workflow works. uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: base64-subjects: ${{ needs.build.outputs.hash }} create-release: needs: [provenance] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - name: create release run: > gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} *.intoto.jsonl/* artifact/* env: GH_TOKEN: ${{ github.token }} publish-pypi: needs: [provenance] environment: name: publish url: https://pypi.org/project/Flask/${{ github.ref_name }} runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 with: packages-dir: artifact/ flask-3.1.1/.github/workflows/tests.yaml000066400000000000000000000036551501065614000202460ustar00rootroot00000000000000name: Tests on: pull_request: paths-ignore: ['docs/**', 'README.md'] push: branches: [main, stable] paths-ignore: ['docs/**', 'README.md'] jobs: tests: name: ${{ matrix.name || matrix.python }} runs-on: ${{ matrix.os || 'ubuntu-latest' }} strategy: fail-fast: false matrix: include: - {python: '3.13'} - {name: Windows, python: '3.13', os: windows-latest} - {name: Mac, python: '3.13', os: macos-latest} - {python: '3.12'} - {python: '3.11'} - {python: '3.10'} - {python: '3.9'} - {name: PyPy, python: 'pypy-3.11', tox: pypy3.11} - {name: Minimum Versions, python: '3.13', tox: tests-min} - {name: Development Versions, python: '3.10', tox: tests-dev} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true prune-cache: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python }} - run: uv run --locked tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }} typing: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 with: enable-cache: true prune-cache: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version-file: pyproject.toml - name: cache mypy uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ./.mypy_cache key: mypy|${{ hashFiles('pyproject.toml') }} - run: uv run --locked tox run -e typing flask-3.1.1/.gitignore000066400000000000000000000001121501065614000145740ustar00rootroot00000000000000.idea/ .vscode/ __pycache__/ dist/ .coverage* htmlcov/ .tox/ docs/_build/ flask-3.1.1/.pre-commit-config.yaml000066400000000000000000000011641501065614000170750ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: 24e02b24b8ab2b7c76225602d13fa60e12d114e6 # frozen: v0.11.9 hooks: - id: ruff - id: ruff-format - repo: https://github.com/astral-sh/uv-pre-commit rev: 14ac15b122e538e407d036ff45e3895b7cf4a2bf # frozen: 0.7.3 hooks: - id: uv-lock - repo: https://github.com/pre-commit/pre-commit-hooks rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0 hooks: - id: check-merge-conflict - id: debug-statements - id: fix-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer flask-3.1.1/.readthedocs.yaml000066400000000000000000000003621501065614000160420ustar00rootroot00000000000000version: 2 build: os: ubuntu-24.04 tools: python: '3.13' commands: - asdf plugin add uv - asdf install uv latest - asdf global uv latest - uv run --group docs sphinx-build -W -b dirhtml docs $READTHEDOCS_OUTPUT/html flask-3.1.1/CHANGES.rst000066400000000000000000002131221501065614000144150ustar00rootroot00000000000000Version 3.1.1 ------------- Released 2025-05-13 - Fix signing key selection order when key rotation is enabled via ``SECRET_KEY_FALLBACKS``. :ghsa:`4grg-w6v8-c28g` - Fix type hint for `cli_runner.invoke`. :issue:`5645` - ``flask --help`` loads the app and plugins first to make sure all commands are shown. :issue:5673` - Mark sans-io base class as being able to handle views that return ``AsyncIterable``. This is not accurate for Flask, but makes typing easier for Quart. :pr:`5659` Version 3.1.0 ------------- Released 2024-11-13 - Drop support for Python 3.8. :pr:`5623` - Update minimum dependency versions to latest feature releases. Werkzeug >= 3.1, ItsDangerous >= 2.2, Blinker >= 1.9. :pr:`5624,5633` - Provide a configuration option to control automatic option responses. :pr:`5496` - ``Flask.open_resource``/``open_instance_resource`` and ``Blueprint.open_resource`` take an ``encoding`` parameter to use when opening in text mode. It defaults to ``utf-8``. :issue:`5504` - ``Request.max_content_length`` can be customized per-request instead of only through the ``MAX_CONTENT_LENGTH`` config. Added ``MAX_FORM_MEMORY_SIZE`` and ``MAX_FORM_PARTS`` config. Added documentation about resource limits to the security page. :issue:`5625` - Add support for the ``Partitioned`` cookie attribute (CHIPS), with the ``SESSION_COOKIE_PARTITIONED`` config. :issue:`5472` - ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files. ``load_dotenv`` loads default files in addition to a path unless ``load_defaults=False`` is passed. :issue:`5628` - Support key rotation with the ``SECRET_KEY_FALLBACKS`` config, a list of old secret keys that can still be used for unsigning. Extensions will need to add support. :issue:`5621` - Fix how setting ``host_matching=True`` or ``subdomain_matching=False`` interacts with ``SERVER_NAME``. Setting ``SERVER_NAME`` no longer restricts requests to only that domain. :issue:`5553` - ``Request.trusted_hosts`` is checked during routing, and can be set through the ``TRUSTED_HOSTS`` config. :issue:`5636` Version 3.0.3 ------------- Released 2024-04-07 - The default ``hashlib.sha1`` may not be available in FIPS builds. Don't access it at import time so the developer has time to change the default. :issue:`5448` - Don't initialize the ``cli`` attribute in the sansio scaffold, but rather in the ``Flask`` concrete class. :pr:`5270` Version 3.0.2 ------------- Released 2024-02-03 - Correct type for ``jinja_loader`` property. :issue:`5388` - Fix error with ``--extra-files`` and ``--exclude-patterns`` CLI options. :issue:`5391` Version 3.0.1 ------------- Released 2024-01-18 - Correct type for ``path`` argument to ``send_file``. :issue:`5336` - Fix a typo in an error message for the ``flask run --key`` option. :pr:`5344` - Session data is untagged without relying on the built-in ``json.loads`` ``object_hook``. This allows other JSON providers that don't implement that. :issue:`5381` - Address more type findings when using mypy strict mode. :pr:`5383` Version 3.0.0 ------------- Released 2023-09-30 - Remove previously deprecated code. :pr:`5223` - Deprecate the ``__version__`` attribute. Use feature detection, or ``importlib.metadata.version("flask")``, instead. :issue:`5230` - Restructure the code such that the Flask (app) and Blueprint classes have Sans-IO bases. :pr:`5127` - Allow self as an argument to url_for. :pr:`5264` - Require Werkzeug >= 3.0.0. Version 2.3.3 ------------- Released 2023-08-21 - Python 3.12 compatibility. - Require Werkzeug >= 2.3.7. - Use ``flit_core`` instead of ``setuptools`` as build backend. - Refactor how an app's root and instance paths are determined. :issue:`5160` Version 2.3.2 ------------- Released 2023-05-01 - Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. - Update Werkzeug requirement to >=2.3.3 to apply recent bug fixes. :ghsa:`m2qf-hxjv-5gpq` Version 2.3.1 ------------- Released 2023-04-25 - Restore deprecated ``from flask import Markup``. :issue:`5084` Version 2.3.0 ------------- Released 2023-04-25 - Drop support for Python 3.7. :pr:`5072` - Update minimum requirements to the latest versions: Werkzeug>=2.3.0, Jinja2>3.1.2, itsdangerous>=2.1.2, click>=8.1.3. - Remove previously deprecated code. :pr:`4995` - The ``push`` and ``pop`` methods of the deprecated ``_app_ctx_stack`` and ``_request_ctx_stack`` objects are removed. ``top`` still exists to give extensions more time to update, but it will be removed. - The ``FLASK_ENV`` environment variable, ``ENV`` config key, and ``app.env`` property are removed. - The ``session_cookie_name``, ``send_file_max_age_default``, ``use_x_sendfile``, ``propagate_exceptions``, and ``templates_auto_reload`` properties on ``app`` are removed. - The ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` config keys are removed. - The ``app.before_first_request`` and ``bp.before_app_first_request`` decorators are removed. - ``json_encoder`` and ``json_decoder`` attributes on app and blueprint, and the corresponding ``json.JSONEncoder`` and ``JSONDecoder`` classes, are removed. - The ``json.htmlsafe_dumps`` and ``htmlsafe_dump`` functions are removed. - Calling setup methods on blueprints after registration is an error instead of a warning. :pr:`4997` - Importing ``escape`` and ``Markup`` from ``flask`` is deprecated. Import them directly from ``markupsafe`` instead. :pr:`4996` - The ``app.got_first_request`` property is deprecated. :pr:`4997` - The ``locked_cached_property`` decorator is deprecated. Use a lock inside the decorated function if locking is needed. :issue:`4993` - Signals are always available. ``blinker>=1.6.2`` is a required dependency. The ``signals_available`` attribute is deprecated. :issue:`5056` - Signals support ``async`` subscriber functions. :pr:`5049` - Remove uses of locks that could cause requests to block each other very briefly. :issue:`4993` - Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``. :pr:`4947` - Ensure subdomains are applied with nested blueprints. :issue:`4834` - ``config.from_file`` can use ``text=False`` to indicate that the parser wants a binary file instead. :issue:`4989` - If a blueprint is created with an empty name it raises a ``ValueError``. :issue:`5010` - ``SESSION_COOKIE_DOMAIN`` does not fall back to ``SERVER_NAME``. The default is not to set the domain, which modern browsers interpret as an exact match rather than a subdomain match. Warnings about ``localhost`` and IP addresses are also removed. :issue:`5051` - The ``routes`` command shows each rule's ``subdomain`` or ``host`` when domain matching is in use. :issue:`5004` - Use postponed evaluation of annotations. :pr:`5071` Version 2.2.5 ------------- Released 2023-05-02 - Update for compatibility with Werkzeug 2.3.3. - Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. Version 2.2.4 ------------- Released 2023-04-25 - Update for compatibility with Werkzeug 2.3. Version 2.2.3 ------------- Released 2023-02-15 - Autoescape is enabled by default for ``.svg`` template files. :issue:`4831` - Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` - Add ``--debug`` option to the ``flask run`` command. :issue:`4777` Version 2.2.2 ------------- Released 2022-08-08 - Update Werkzeug dependency to >= 2.2.2. This includes fixes related to the new faster router, header parsing, and the development server. :pr:`4754` - Fix the default value for ``app.env`` to be ``"production"``. This attribute remains deprecated. :issue:`4740` Version 2.2.1 ------------- Released 2022-08-03 - Setting or accessing ``json_encoder`` or ``json_decoder`` raises a deprecation warning. :issue:`4732` Version 2.2.0 ------------- Released 2022-08-01 - Remove previously deprecated code. :pr:`4667` - Old names for some ``send_file`` parameters have been removed. ``download_name`` replaces ``attachment_filename``, ``max_age`` replaces ``cache_timeout``, and ``etag`` replaces ``add_etags``. Additionally, ``path`` replaces ``filename`` in ``send_from_directory``. - The ``RequestContext.g`` property returning ``AppContext.g`` is removed. - Update Werkzeug dependency to >= 2.2. - The app and request contexts are managed using Python context vars directly rather than Werkzeug's ``LocalStack``. This should result in better performance and memory use. :pr:`4682` - Extension maintainers, be aware that ``_app_ctx_stack.top`` and ``_request_ctx_stack.top`` are deprecated. Store data on ``g`` instead using a unique prefix, like ``g._extension_name_attr``. - The ``FLASK_ENV`` environment variable and ``app.env`` attribute are deprecated, removing the distinction between development and debug mode. Debug mode should be controlled directly using the ``--debug`` option or ``app.run(debug=True)``. :issue:`4714` - Some attributes that proxied config keys on ``app`` are deprecated: ``session_cookie_name``, ``send_file_max_age_default``, ``use_x_sendfile``, ``propagate_exceptions``, and ``templates_auto_reload``. Use the relevant config keys instead. :issue:`4716` - Add new customization points to the ``Flask`` app object for many previously global behaviors. - ``flask.url_for`` will call ``app.url_for``. :issue:`4568` - ``flask.abort`` will call ``app.aborter``. ``Flask.aborter_class`` and ``Flask.make_aborter`` can be used to customize this aborter. :issue:`4567` - ``flask.redirect`` will call ``app.redirect``. :issue:`4569` - ``flask.json`` is an instance of ``JSONProvider``. A different provider can be set to use a different JSON library. ``flask.jsonify`` will call ``app.json.response``, other functions in ``flask.json`` will call corresponding functions in ``app.json``. :pr:`4692` - JSON configuration is moved to attributes on the default ``app.json`` provider. ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` are deprecated. :pr:`4692` - Setting custom ``json_encoder`` and ``json_decoder`` classes on the app or a blueprint, and the corresponding ``json.JSONEncoder`` and ``JSONDecoder`` classes, are deprecated. JSON behavior can now be overridden using the ``app.json`` provider interface. :pr:`4692` - ``json.htmlsafe_dumps`` and ``json.htmlsafe_dump`` are deprecated, the function is built-in to Jinja now. :pr:`4692` - Refactor ``register_error_handler`` to consolidate error checking. Rewrite some error messages to be more consistent. :issue:`4559` - Use Blueprint decorators and functions intended for setup after registering the blueprint will show a warning. In the next version, this will become an error just like the application setup methods. :issue:`4571` - ``before_first_request`` is deprecated. Run setup code when creating the application instead. :issue:`4605` - Added the ``View.init_every_request`` class attribute. If a view subclass sets this to ``False``, the view will not create a new instance on every request. :issue:`2520`. - A ``flask.cli.FlaskGroup`` Click group can be nested as a sub-command in a custom CLI. :issue:`3263` - Add ``--app`` and ``--debug`` options to the ``flask`` CLI, instead of requiring that they are set through environment variables. :issue:`2836` - Add ``--env-file`` option to the ``flask`` CLI. This allows specifying a dotenv file to load in addition to ``.env`` and ``.flaskenv``. :issue:`3108` - It is no longer required to decorate custom CLI commands on ``app.cli`` or ``blueprint.cli`` with ``@with_appcontext``, an app context will already be active at that point. :issue:`2410` - ``SessionInterface.get_expiration_time`` uses a timezone-aware value. :pr:`4645` - View functions can return generators directly instead of wrapping them in a ``Response``. :pr:`4629` - Add ``stream_template`` and ``stream_template_string`` functions to render a template as a stream of pieces. :pr:`4629` - A new implementation of context preservation during debugging and testing. :pr:`4666` - ``request``, ``g``, and other context-locals point to the correct data when running code in the interactive debugger console. :issue:`2836` - Teardown functions are always run at the end of the request, even if the context is preserved. They are also run after the preserved context is popped. - ``stream_with_context`` preserves context separately from a ``with client`` block. It will be cleaned up when ``response.get_data()`` or ``response.close()`` is called. - Allow returning a list from a view function, to convert it to a JSON response like a dict is. :issue:`4672` - When type checking, allow ``TypedDict`` to be returned from view functions. :pr:`4695` - Remove the ``--eager-loading/--lazy-loading`` options from the ``flask run`` command. The app is always eager loaded the first time, then lazily loaded in the reloader. The reloader always prints errors immediately but continues serving. Remove the internal ``DispatchingApp`` middleware used by the previous implementation. :issue:`4715` Version 2.1.3 ------------- Released 2022-07-13 - Inline some optional imports that are only used for certain CLI commands. :pr:`4606` - Relax type annotation for ``after_request`` functions. :issue:`4600` - ``instance_path`` for namespace packages uses the path closest to the imported submodule. :issue:`4610` - Clearer error message when ``render_template`` and ``render_template_string`` are used outside an application context. :pr:`4693` Version 2.1.2 ------------- Released 2022-04-28 - Fix type annotation for ``json.loads``, it accepts str or bytes. :issue:`4519` - The ``--cert`` and ``--key`` options on ``flask run`` can be given in either order. :issue:`4459` Version 2.1.1 ------------- Released on 2022-03-30 - Set the minimum required version of importlib_metadata to 3.6.0, which is required on Python < 3.10. :issue:`4502` Version 2.1.0 ------------- Released 2022-03-28 - Drop support for Python 3.6. :pr:`4335` - Update Click dependency to >= 8.0. :pr:`4008` - Remove previously deprecated code. :pr:`4337` - The CLI does not pass ``script_info`` to app factory functions. - ``config.from_json`` is replaced by ``config.from_file(name, load=json.load)``. - ``json`` functions no longer take an ``encoding`` parameter. - ``safe_join`` is removed, use ``werkzeug.utils.safe_join`` instead. - ``total_seconds`` is removed, use ``timedelta.total_seconds`` instead. - The same blueprint cannot be registered with the same name. Use ``name=`` when registering to specify a unique name. - The test client's ``as_tuple`` parameter is removed. Use ``response.request.environ`` instead. :pr:`4417` - Some parameters in ``send_file`` and ``send_from_directory`` were renamed in 2.0. The deprecation period for the old names is extended to 2.2. Be sure to test with deprecation warnings visible. - ``attachment_filename`` is renamed to ``download_name``. - ``cache_timeout`` is renamed to ``max_age``. - ``add_etags`` is renamed to ``etag``. - ``filename`` is renamed to ``path``. - The ``RequestContext.g`` property is deprecated. Use ``g`` directly or ``AppContext.g`` instead. :issue:`3898` - ``copy_current_request_context`` can decorate async functions. :pr:`4303` - The CLI uses ``importlib.metadata`` instead of ``pkg_resources`` to load command entry points. :issue:`4419` - Overriding ``FlaskClient.open`` will not cause an error on redirect. :issue:`3396` - Add an ``--exclude-patterns`` option to the ``flask run`` CLI command to specify patterns that will be ignored by the reloader. :issue:`4188` - When using lazy loading (the default with the debugger), the Click context from the ``flask run`` command remains available in the loader thread. :issue:`4460` - Deleting the session cookie uses the ``httponly`` flag. :issue:`4485` - Relax typing for ``errorhandler`` to allow the user to use more precise types and decorate the same function multiple times. :issue:`4095, 4295, 4297` - Fix typing for ``__exit__`` methods for better compatibility with ``ExitStack``. :issue:`4474` - From Werkzeug, for redirect responses the ``Location`` header URL will remain relative, and exclude the scheme and domain, by default. :pr:`4496` - Add ``Config.from_prefixed_env()`` to load config values from environment variables that start with ``FLASK_`` or another prefix. This parses values as JSON by default, and allows setting keys in nested dicts. :pr:`4479` Version 2.0.3 ------------- Released 2022-02-14 - The test client's ``as_tuple`` parameter is deprecated and will be removed in Werkzeug 2.1. It is now also deprecated in Flask, to be removed in Flask 2.1, while remaining compatible with both in 2.0.x. Use ``response.request.environ`` instead. :pr:`4341` - Fix type annotation for ``errorhandler`` decorator. :issue:`4295` - Revert a change to the CLI that caused it to hide ``ImportError`` tracebacks when importing the application. :issue:`4307` - ``app.json_encoder`` and ``json_decoder`` are only passed to ``dumps`` and ``loads`` if they have custom behavior. This improves performance, mainly on PyPy. :issue:`4349` - Clearer error message when ``after_this_request`` is used outside a request context. :issue:`4333` Version 2.0.2 ------------- Released 2021-10-04 - Fix type annotation for ``teardown_*`` methods. :issue:`4093` - Fix type annotation for ``before_request`` and ``before_app_request`` decorators. :issue:`4104` - Fixed the issue where typing requires template global decorators to accept functions with no arguments. :issue:`4098` - Support View and MethodView instances with async handlers. :issue:`4112` - Enhance typing of ``app.errorhandler`` decorator. :issue:`4095` - Fix registering a blueprint twice with differing names. :issue:`4124` - Fix the type of ``static_folder`` to accept ``pathlib.Path``. :issue:`4150` - ``jsonify`` handles ``decimal.Decimal`` by encoding to ``str``. :issue:`4157` - Correctly handle raising deferred errors in CLI lazy loading. :issue:`4096` - The CLI loader handles ``**kwargs`` in a ``create_app`` function. :issue:`4170` - Fix the order of ``before_request`` and other callbacks that trigger before the view returns. They are called from the app down to the closest nested blueprint. :issue:`4229` Version 2.0.1 ------------- Released 2021-05-21 - Re-add the ``filename`` parameter in ``send_from_directory``. The ``filename`` parameter has been renamed to ``path``, the old name is deprecated. :pr:`4019` - Mark top-level names as exported so type checking understands imports in user projects. :issue:`4024` - Fix type annotation for ``g`` and inform mypy that it is a namespace object that has arbitrary attributes. :issue:`4020` - Fix some types that weren't available in Python 3.6.0. :issue:`4040` - Improve typing for ``send_file``, ``send_from_directory``, and ``get_send_file_max_age``. :issue:`4044`, :pr:`4026` - Show an error when a blueprint name contains a dot. The ``.`` has special meaning, it is used to separate (nested) blueprint names and the endpoint name. :issue:`4041` - Combine URL prefixes when nesting blueprints that were created with a ``url_prefix`` value. :issue:`4037` - Revert a change to the order that URL matching was done. The URL is again matched after the session is loaded, so the session is available in custom URL converters. :issue:`4053` - Re-add deprecated ``Config.from_json``, which was accidentally removed early. :issue:`4078` - Improve typing for some functions using ``Callable`` in their type signatures, focusing on decorator factories. :issue:`4060` - Nested blueprints are registered with their dotted name. This allows different blueprints with the same name to be nested at different locations. :issue:`4069` - ``register_blueprint`` takes a ``name`` option to change the (pre-dotted) name the blueprint is registered with. This allows the same blueprint to be registered multiple times with unique names for ``url_for``. Registering the same blueprint with the same name multiple times is deprecated. :issue:`1091` - Improve typing for ``stream_with_context``. :issue:`4052` Version 2.0.0 ------------- Released 2021-05-11 - Drop support for Python 2 and 3.5. - Bump minimum versions of other Pallets projects: Werkzeug >= 2, Jinja2 >= 3, MarkupSafe >= 2, ItsDangerous >= 2, Click >= 8. Be sure to check the change logs for each project. For better compatibility with other applications (e.g. Celery) that still require Click 7, there is no hard dependency on Click 8 yet, but using Click 7 will trigger a DeprecationWarning and Flask 2.1 will depend on Click 8. - JSON support no longer uses simplejson. To use another JSON module, override ``app.json_encoder`` and ``json_decoder``. :issue:`3555` - The ``encoding`` option to JSON functions is deprecated. :pr:`3562` - Passing ``script_info`` to app factory functions is deprecated. This was not portable outside the ``flask`` command. Use ``click.get_current_context().obj`` if it's needed. :issue:`3552` - The CLI shows better error messages when the app failed to load when looking up commands. :issue:`2741` - Add ``SessionInterface.get_cookie_name`` to allow setting the session cookie name dynamically. :pr:`3369` - Add ``Config.from_file`` to load config using arbitrary file loaders, such as ``toml.load`` or ``json.load``. ``Config.from_json`` is deprecated in favor of this. :pr:`3398` - The ``flask run`` command will only defer errors on reload. Errors present during the initial call will cause the server to exit with the traceback immediately. :issue:`3431` - ``send_file`` raises a ``ValueError`` when passed an ``io`` object in text mode. Previously, it would respond with 200 OK and an empty file. :issue:`3358` - When using ad-hoc certificates, check for the cryptography library instead of PyOpenSSL. :pr:`3492` - When specifying a factory function with ``FLASK_APP``, keyword argument can be passed. :issue:`3553` - When loading a ``.env`` or ``.flaskenv`` file, the current working directory is no longer changed to the location of the file. :pr:`3560` - When returning a ``(response, headers)`` tuple from a view, the headers replace rather than extend existing headers on the response. For example, this allows setting the ``Content-Type`` for ``jsonify()``. Use ``response.headers.extend()`` if extending is desired. :issue:`3628` - The ``Scaffold`` class provides a common API for the ``Flask`` and ``Blueprint`` classes. ``Blueprint`` information is stored in attributes just like ``Flask``, rather than opaque lambda functions. This is intended to improve consistency and maintainability. :issue:`3215` - Include ``samesite`` and ``secure`` options when removing the session cookie. :pr:`3726` - Support passing a ``pathlib.Path`` to ``static_folder``. :pr:`3579` - ``send_file`` and ``send_from_directory`` are wrappers around the implementations in ``werkzeug.utils``. :pr:`3828` - Some ``send_file`` parameters have been renamed, the old names are deprecated. ``attachment_filename`` is renamed to ``download_name``. ``cache_timeout`` is renamed to ``max_age``. ``add_etags`` is renamed to ``etag``. :pr:`3828, 3883` - ``send_file`` passes ``download_name`` even if ``as_attachment=False`` by using ``Content-Disposition: inline``. :pr:`3828` - ``send_file`` sets ``conditional=True`` and ``max_age=None`` by default. ``Cache-Control`` is set to ``no-cache`` if ``max_age`` is not set, otherwise ``public``. This tells browsers to validate conditional requests instead of using a timed cache. :pr:`3828` - ``helpers.safe_join`` is deprecated. Use ``werkzeug.utils.safe_join`` instead. :pr:`3828` - The request context does route matching before opening the session. This could allow a session interface to change behavior based on ``request.endpoint``. :issue:`3776` - Use Jinja's implementation of the ``|tojson`` filter. :issue:`3881` - Add route decorators for common HTTP methods. For example, ``@app.post("/login")`` is a shortcut for ``@app.route("/login", methods=["POST"])``. :pr:`3907` - Support async views, error handlers, before and after request, and teardown functions. :pr:`3412` - Support nesting blueprints. :issue:`593, 1548`, :pr:`3923` - Set the default encoding to "UTF-8" when loading ``.env`` and ``.flaskenv`` files to allow to use non-ASCII characters. :issue:`3931` - ``flask shell`` sets up tab and history completion like the default ``python`` shell if ``readline`` is installed. :issue:`3941` - ``helpers.total_seconds()`` is deprecated. Use ``timedelta.total_seconds()`` instead. :pr:`3962` - Add type hinting. :pr:`3973`. Version 1.1.4 ------------- Released 2021-05-13 - Update ``static_folder`` to use ``_compat.fspath`` instead of ``os.fspath`` to continue supporting Python < 3.6 :issue:`4050` Version 1.1.3 ------------- Released 2021-05-13 - Set maximum versions of Werkzeug, Jinja, Click, and ItsDangerous. :issue:`4043` - Re-add support for passing a ``pathlib.Path`` for ``static_folder``. :pr:`3579` Version 1.1.2 ------------- Released 2020-04-03 - Work around an issue when running the ``flask`` command with an external debugger on Windows. :issue:`3297` - The static route will not catch all URLs if the ``Flask`` ``static_folder`` argument ends with a slash. :issue:`3452` Version 1.1.1 ------------- Released 2019-07-08 - The ``flask.json_available`` flag was added back for compatibility with some extensions. It will raise a deprecation warning when used, and will be removed in version 2.0.0. :issue:`3288` Version 1.1.0 ------------- Released 2019-07-04 - Bump minimum Werkzeug version to >= 0.15. - Drop support for Python 3.4. - Error handlers for ``InternalServerError`` or ``500`` will always be passed an instance of ``InternalServerError``. If they are invoked due to an unhandled exception, that original exception is now available as ``e.original_exception`` rather than being passed directly to the handler. The same is true if the handler is for the base ``HTTPException``. This makes error handler behavior more consistent. :pr:`3266` - ``Flask.finalize_request`` is called for all unhandled exceptions even if there is no ``500`` error handler. - ``Flask.logger`` takes the same name as ``Flask.name`` (the value passed as ``Flask(import_name)``. This reverts 1.0's behavior of always logging to ``"flask.app"``, in order to support multiple apps in the same process. A warning will be shown if old configuration is detected that needs to be moved. :issue:`2866` - ``RequestContext.copy`` includes the current session object in the request context copy. This prevents ``session`` pointing to an out-of-date object. :issue:`2935` - Using built-in RequestContext, unprintable Unicode characters in Host header will result in a HTTP 400 response and not HTTP 500 as previously. :pr:`2994` - ``send_file`` supports ``PathLike`` objects as described in :pep:`519`, to support ``pathlib`` in Python 3. :pr:`3059` - ``send_file`` supports ``BytesIO`` partial content. :issue:`2957` - ``open_resource`` accepts the "rt" file mode. This still does the same thing as "r". :issue:`3163` - The ``MethodView.methods`` attribute set in a base class is used by subclasses. :issue:`3138` - ``Flask.jinja_options`` is a ``dict`` instead of an ``ImmutableDict`` to allow easier configuration. Changes must still be made before creating the environment. :pr:`3190` - Flask's ``JSONMixin`` for the request and response wrappers was moved into Werkzeug. Use Werkzeug's version with Flask-specific support. This bumps the Werkzeug dependency to >= 0.15. :issue:`3125` - The ``flask`` command entry point is simplified to take advantage of Werkzeug 0.15's better reloader support. This bumps the Werkzeug dependency to >= 0.15. :issue:`3022` - Support ``static_url_path`` that ends with a forward slash. :issue:`3134` - Support empty ``static_folder`` without requiring setting an empty ``static_url_path`` as well. :pr:`3124` - ``jsonify`` supports ``dataclass`` objects. :pr:`3195` - Allow customizing the ``Flask.url_map_class`` used for routing. :pr:`3069` - The development server port can be set to 0, which tells the OS to pick an available port. :issue:`2926` - The return value from ``cli.load_dotenv`` is more consistent with the documentation. It will return ``False`` if python-dotenv is not installed, or if the given path isn't a file. :issue:`2937` - Signaling support has a stub for the ``connect_via`` method when the Blinker library is not installed. :pr:`3208` - Add an ``--extra-files`` option to the ``flask run`` CLI command to specify extra files that will trigger the reloader on change. :issue:`2897` - Allow returning a dictionary from a view function. Similar to how returning a string will produce a ``text/html`` response, returning a dict will call ``jsonify`` to produce a ``application/json`` response. :pr:`3111` - Blueprints have a ``cli`` Click group like ``app.cli``. CLI commands registered with a blueprint will be available as a group under the ``flask`` command. :issue:`1357`. - When using the test client as a context manager (``with client:``), all preserved request contexts are popped when the block exits, ensuring nested contexts are cleaned up correctly. :pr:`3157` - Show a better error message when the view return type is not supported. :issue:`3214` - ``flask.testing.make_test_environ_builder()`` has been deprecated in favour of a new class ``flask.testing.EnvironBuilder``. :pr:`3232` - The ``flask run`` command no longer fails if Python is not built with SSL support. Using the ``--cert`` option will show an appropriate error message. :issue:`3211` - URL matching now occurs after the request context is pushed, rather than when it's created. This allows custom URL converters to access the app and request contexts, such as to query a database for an id. :issue:`3088` Version 1.0.4 ------------- Released 2019-07-04 - The key information for ``BadRequestKeyError`` is no longer cleared outside debug mode, so error handlers can still access it. This requires upgrading to Werkzeug 0.15.5. :issue:`3249` - ``send_file`` url quotes the ":" and "/" characters for more compatible UTF-8 filename support in some browsers. :issue:`3074` - Fixes for :pep:`451` import loaders and pytest 5.x. :issue:`3275` - Show message about dotenv on stderr instead of stdout. :issue:`3285` Version 1.0.3 ------------- Released 2019-05-17 - ``send_file`` encodes filenames as ASCII instead of Latin-1 (ISO-8859-1). This fixes compatibility with Gunicorn, which is stricter about header encodings than :pep:`3333`. :issue:`2766` - Allow custom CLIs using ``FlaskGroup`` to set the debug flag without it always being overwritten based on environment variables. :pr:`2765` - ``flask --version`` outputs Werkzeug's version and simplifies the Python version. :pr:`2825` - ``send_file`` handles an ``attachment_filename`` that is a native Python 2 string (bytes) with UTF-8 coded bytes. :issue:`2933` - A catch-all error handler registered for ``HTTPException`` will not handle ``RoutingException``, which is used internally during routing. This fixes the unexpected behavior that had been introduced in 1.0. :pr:`2986` - Passing the ``json`` argument to ``app.test_client`` does not push/pop an extra app context. :issue:`2900` Version 1.0.2 ------------- Released 2018-05-02 - Fix more backwards compatibility issues with merging slashes between a blueprint prefix and route. :pr:`2748` - Fix error with ``flask routes`` command when there are no routes. :issue:`2751` Version 1.0.1 ------------- Released 2018-04-29 - Fix registering partials (with no ``__name__``) as view functions. :pr:`2730` - Don't treat lists returned from view functions the same as tuples. Only tuples are interpreted as response data. :issue:`2736` - Extra slashes between a blueprint's ``url_prefix`` and a route URL are merged. This fixes some backwards compatibility issues with the change in 1.0. :issue:`2731`, :issue:`2742` - Only trap ``BadRequestKeyError`` errors in debug mode, not all ``BadRequest`` errors. This allows ``abort(400)`` to continue working as expected. :issue:`2735` - The ``FLASK_SKIP_DOTENV`` environment variable can be set to ``1`` to skip automatically loading dotenv files. :issue:`2722` Version 1.0 ----------- Released 2018-04-26 - Python 2.6 and 3.3 are no longer supported. - Bump minimum dependency versions to the latest stable versions: Werkzeug >= 0.14, Jinja >= 2.10, itsdangerous >= 0.24, Click >= 5.1. :issue:`2586` - Skip ``app.run`` when a Flask application is run from the command line. This avoids some behavior that was confusing to debug. - Change the default for ``JSONIFY_PRETTYPRINT_REGULAR`` to ``False``. ``~json.jsonify`` returns a compact format by default, and an indented format in debug mode. :pr:`2193` - ``Flask.__init__`` accepts the ``host_matching`` argument and sets it on ``Flask.url_map``. :issue:`1559` - ``Flask.__init__`` accepts the ``static_host`` argument and passes it as the ``host`` argument when defining the static route. :issue:`1559` - ``send_file`` supports Unicode in ``attachment_filename``. :pr:`2223` - Pass ``_scheme`` argument from ``url_for`` to ``Flask.handle_url_build_error``. :pr:`2017` - ``Flask.add_url_rule`` accepts the ``provide_automatic_options`` argument to disable adding the ``OPTIONS`` method. :pr:`1489` - ``MethodView`` subclasses inherit method handlers from base classes. :pr:`1936` - Errors caused while opening the session at the beginning of the request are handled by the app's error handlers. :pr:`2254` - Blueprints gained ``Blueprint.json_encoder`` and ``Blueprint.json_decoder`` attributes to override the app's encoder and decoder. :pr:`1898` - ``Flask.make_response`` raises ``TypeError`` instead of ``ValueError`` for bad response types. The error messages have been improved to describe why the type is invalid. :pr:`2256` - Add ``routes`` CLI command to output routes registered on the application. :pr:`2259` - Show warning when session cookie domain is a bare hostname or an IP address, as these may not behave properly in some browsers, such as Chrome. :pr:`2282` - Allow IP address as exact session cookie domain. :pr:`2282` - ``SESSION_COOKIE_DOMAIN`` is set if it is detected through ``SERVER_NAME``. :pr:`2282` - Auto-detect zero-argument app factory called ``create_app`` or ``make_app`` from ``FLASK_APP``. :pr:`2297` - Factory functions are not required to take a ``script_info`` parameter to work with the ``flask`` command. If they take a single parameter or a parameter named ``script_info``, the ``ScriptInfo`` object will be passed. :pr:`2319` - ``FLASK_APP`` can be set to an app factory, with arguments if needed, for example ``FLASK_APP=myproject.app:create_app('dev')``. :pr:`2326` - ``FLASK_APP`` can point to local packages that are not installed in editable mode, although ``pip install -e`` is still preferred. :pr:`2414` - The ``View`` class attribute ``View.provide_automatic_options`` is set in ``View.as_view``, to be detected by ``Flask.add_url_rule``. :pr:`2316` - Error handling will try handlers registered for ``blueprint, code``, ``app, code``, ``blueprint, exception``, ``app, exception``. :pr:`2314` - ``Cookie`` is added to the response's ``Vary`` header if the session is accessed at all during the request (and not deleted). :pr:`2288` - ``Flask.test_request_context`` accepts ``subdomain`` and ``url_scheme`` arguments for use when building the base URL. :pr:`1621` - Set ``APPLICATION_ROOT`` to ``'/'`` by default. This was already the implicit default when it was set to ``None``. - ``TRAP_BAD_REQUEST_ERRORS`` is enabled by default in debug mode. ``BadRequestKeyError`` has a message with the bad key in debug mode instead of the generic bad request message. :pr:`2348` - Allow registering new tags with ``TaggedJSONSerializer`` to support storing other types in the session cookie. :pr:`2352` - Only open the session if the request has not been pushed onto the context stack yet. This allows ``stream_with_context`` generators to access the same session that the containing view uses. :pr:`2354` - Add ``json`` keyword argument for the test client request methods. This will dump the given object as JSON and set the appropriate content type. :pr:`2358` - Extract JSON handling to a mixin applied to both the ``Request`` and ``Response`` classes. This adds the ``Response.is_json`` and ``Response.get_json`` methods to the response to make testing JSON response much easier. :pr:`2358` - Removed error handler caching because it caused unexpected results for some exception inheritance hierarchies. Register handlers explicitly for each exception if you want to avoid traversing the MRO. :pr:`2362` - Fix incorrect JSON encoding of aware, non-UTC datetimes. :pr:`2374` - Template auto reloading will honor debug mode even if ``Flask.jinja_env`` was already accessed. :pr:`2373` - The following old deprecated code was removed. :issue:`2385` - ``flask.ext`` - import extensions directly by their name instead of through the ``flask.ext`` namespace. For example, ``import flask.ext.sqlalchemy`` becomes ``import flask_sqlalchemy``. - ``Flask.init_jinja_globals`` - extend ``Flask.create_jinja_environment`` instead. - ``Flask.error_handlers`` - tracked by ``Flask.error_handler_spec``, use ``Flask.errorhandler`` to register handlers. - ``Flask.request_globals_class`` - use ``Flask.app_ctx_globals_class`` instead. - ``Flask.static_path`` - use ``Flask.static_url_path`` instead. - ``Request.module`` - use ``Request.blueprint`` instead. - The ``Request.json`` property is no longer deprecated. :issue:`1421` - Support passing a ``EnvironBuilder`` or ``dict`` to ``test_client.open``. :pr:`2412` - The ``flask`` command and ``Flask.run`` will load environment variables from ``.env`` and ``.flaskenv`` files if python-dotenv is installed. :pr:`2416` - When passing a full URL to the test client, the scheme in the URL is used instead of ``PREFERRED_URL_SCHEME``. :pr:`2430` - ``Flask.logger`` has been simplified. ``LOGGER_NAME`` and ``LOGGER_HANDLER_POLICY`` config was removed. The logger is always named ``flask.app``. The level is only set on first access, it doesn't check ``Flask.debug`` each time. Only one format is used, not different ones depending on ``Flask.debug``. No handlers are removed, and a handler is only added if no handlers are already configured. :pr:`2436` - Blueprint view function names may not contain dots. :pr:`2450` - Fix a ``ValueError`` caused by invalid ``Range`` requests in some cases. :issue:`2526` - The development server uses threads by default. :pr:`2529` - Loading config files with ``silent=True`` will ignore ``ENOTDIR`` errors. :pr:`2581` - Pass ``--cert`` and ``--key`` options to ``flask run`` to run the development server over HTTPS. :pr:`2606` - Added ``SESSION_COOKIE_SAMESITE`` to control the ``SameSite`` attribute on the session cookie. :pr:`2607` - Added ``Flask.test_cli_runner`` to create a Click runner that can invoke Flask CLI commands for testing. :pr:`2636` - Subdomain matching is disabled by default and setting ``SERVER_NAME`` does not implicitly enable it. It can be enabled by passing ``subdomain_matching=True`` to the ``Flask`` constructor. :pr:`2635` - A single trailing slash is stripped from the blueprint ``url_prefix`` when it is registered with the app. :pr:`2629` - ``Request.get_json`` doesn't cache the result if parsing fails when ``silent`` is true. :issue:`2651` - ``Request.get_json`` no longer accepts arbitrary encodings. Incoming JSON should be encoded using UTF-8 per :rfc:`8259`, but Flask will autodetect UTF-8, -16, or -32. :pr:`2691` - Added ``MAX_COOKIE_SIZE`` and ``Response.max_cookie_size`` to control when Werkzeug warns about large cookies that browsers may ignore. :pr:`2693` - Updated documentation theme to make docs look better in small windows. :pr:`2709` - Rewrote the tutorial docs and example project to take a more structured approach to help new users avoid common pitfalls. :pr:`2676` Version 0.12.5 -------------- Released 2020-02-10 - Pin Werkzeug to < 1.0.0. :issue:`3497` Version 0.12.4 -------------- Released 2018-04-29 - Repackage 0.12.3 to fix package layout issue. :issue:`2728` Version 0.12.3 -------------- Released 2018-04-26 - ``Request.get_json`` no longer accepts arbitrary encodings. Incoming JSON should be encoded using UTF-8 per :rfc:`8259`, but Flask will autodetect UTF-8, -16, or -32. :issue:`2692` - Fix a Python warning about imports when using ``python -m flask``. :issue:`2666` - Fix a ``ValueError`` caused by invalid ``Range`` requests in some cases. Version 0.12.2 -------------- Released 2017-05-16 - Fix a bug in ``safe_join`` on Windows. Version 0.12.1 -------------- Released 2017-03-31 - Prevent ``flask run`` from showing a ``NoAppException`` when an ``ImportError`` occurs within the imported application module. - Fix encoding behavior of ``app.config.from_pyfile`` for Python 3. :issue:`2118` - Use the ``SERVER_NAME`` config if it is present as default values for ``app.run``. :issue:`2109`, :pr:`2152` - Call ``ctx.auto_pop`` with the exception object instead of ``None``, in the event that a ``BaseException`` such as ``KeyboardInterrupt`` is raised in a request handler. Version 0.12 ------------ Released 2016-12-21, codename Punsch - The cli command now responds to ``--version``. - Mimetype guessing and ETag generation for file-like objects in ``send_file`` has been removed. :issue:`104`, :pr`1849` - Mimetype guessing in ``send_file`` now fails loudly and doesn't fall back to ``application/octet-stream``. :pr:`1988` - Make ``flask.safe_join`` able to join multiple paths like ``os.path.join`` :pr:`1730` - Revert a behavior change that made the dev server crash instead of returning an Internal Server Error. :pr:`2006` - Correctly invoke response handlers for both regular request dispatching as well as error handlers. - Disable logger propagation by default for the app logger. - Add support for range requests in ``send_file``. - ``app.test_client`` includes preset default environment, which can now be directly set, instead of per ``client.get``. - Fix crash when running under PyPy3. :pr:`1814` Version 0.11.1 -------------- Released 2016-06-07 - Fixed a bug that prevented ``FLASK_APP=foobar/__init__.py`` from working. :pr:`1872` Version 0.11 ------------ Released 2016-05-29, codename Absinthe - Added support to serializing top-level arrays to ``jsonify``. This introduces a security risk in ancient browsers. - Added before_render_template signal. - Added ``**kwargs`` to ``Flask.test_client`` to support passing additional keyword arguments to the constructor of ``Flask.test_client_class``. - Added ``SESSION_REFRESH_EACH_REQUEST`` config key that controls the set-cookie behavior. If set to ``True`` a permanent session will be refreshed each request and get their lifetime extended, if set to ``False`` it will only be modified if the session actually modifies. Non permanent sessions are not affected by this and will always expire if the browser window closes. - Made Flask support custom JSON mimetypes for incoming data. - Added support for returning tuples in the form ``(response, headers)`` from a view function. - Added ``Config.from_json``. - Added ``Flask.config_class``. - Added ``Config.get_namespace``. - Templates are no longer automatically reloaded outside of debug mode. This can be configured with the new ``TEMPLATES_AUTO_RELOAD`` config key. - Added a workaround for a limitation in Python 3.3's namespace loader. - Added support for explicit root paths when using Python 3.3's namespace packages. - Added ``flask`` and the ``flask.cli`` module to start the local debug server through the click CLI system. This is recommended over the old ``flask.run()`` method as it works faster and more reliable due to a different design and also replaces ``Flask-Script``. - Error handlers that match specific classes are now checked first, thereby allowing catching exceptions that are subclasses of HTTP exceptions (in ``werkzeug.exceptions``). This makes it possible for an extension author to create exceptions that will by default result in the HTTP error of their choosing, but may be caught with a custom error handler if desired. - Added ``Config.from_mapping``. - Flask will now log by default even if debug is disabled. The log format is now hardcoded but the default log handling can be disabled through the ``LOGGER_HANDLER_POLICY`` configuration key. - Removed deprecated module functionality. - Added the ``EXPLAIN_TEMPLATE_LOADING`` config flag which when enabled will instruct Flask to explain how it locates templates. This should help users debug when the wrong templates are loaded. - Enforce blueprint handling in the order they were registered for template loading. - Ported test suite to py.test. - Deprecated ``request.json`` in favour of ``request.get_json()``. - Add "pretty" and "compressed" separators definitions in jsonify() method. Reduces JSON response size when ``JSONIFY_PRETTYPRINT_REGULAR=False`` by removing unnecessary white space included by default after separators. - JSON responses are now terminated with a newline character, because it is a convention that UNIX text files end with a newline and some clients don't deal well when this newline is missing. :pr:`1262` - The automatically provided ``OPTIONS`` method is now correctly disabled if the user registered an overriding rule with the lowercase-version ``options``. :issue:`1288` - ``flask.json.jsonify`` now supports the ``datetime.date`` type. :pr:`1326` - Don't leak exception info of already caught exceptions to context teardown handlers. :pr:`1393` - Allow custom Jinja environment subclasses. :pr:`1422` - Updated extension dev guidelines. - ``flask.g`` now has ``pop()`` and ``setdefault`` methods. - Turn on autoescape for ``flask.templating.render_template_string`` by default. :pr:`1515` - ``flask.ext`` is now deprecated. :pr:`1484` - ``send_from_directory`` now raises BadRequest if the filename is invalid on the server OS. :pr:`1763` - Added the ``JSONIFY_MIMETYPE`` configuration variable. :pr:`1728` - Exceptions during teardown handling will no longer leave bad application contexts lingering around. - Fixed broken ``test_appcontext_signals()`` test case. - Raise an ``AttributeError`` in ``helpers.find_package`` with a useful message explaining why it is raised when a :pep:`302` import hook is used without an ``is_package()`` method. - Fixed an issue causing exceptions raised before entering a request or app context to be passed to teardown handlers. - Fixed an issue with query parameters getting removed from requests in the test client when absolute URLs were requested. - Made ``@before_first_request`` into a decorator as intended. - Fixed an etags bug when sending a file streams with a name. - Fixed ``send_from_directory`` not expanding to the application root path correctly. - Changed logic of before first request handlers to flip the flag after invoking. This will allow some uses that are potentially dangerous but should probably be permitted. - Fixed Python 3 bug when a handler from ``app.url_build_error_handlers`` reraises the ``BuildError``. Version 0.10.1 -------------- Released 2013-06-14 - Fixed an issue where ``|tojson`` was not quoting single quotes which made the filter not work properly in HTML attributes. Now it's possible to use that filter in single quoted attributes. This should make using that filter with angular.js easier. - Added support for byte strings back to the session system. This broke compatibility with the common case of people putting binary data for token verification into the session. - Fixed an issue where registering the same method twice for the same endpoint would trigger an exception incorrectly. Version 0.10 ------------ Released 2013-06-13, codename Limoncello - Changed default cookie serialization format from pickle to JSON to limit the impact an attacker can do if the secret key leaks. - Added ``template_test`` methods in addition to the already existing ``template_filter`` method family. - Added ``template_global`` methods in addition to the already existing ``template_filter`` method family. - Set the content-length header for x-sendfile. - ``tojson`` filter now does not escape script blocks in HTML5 parsers. - ``tojson`` used in templates is now safe by default. This was allowed due to the different escaping behavior. - Flask will now raise an error if you attempt to register a new function on an already used endpoint. - Added wrapper module around simplejson and added default serialization of datetime objects. This allows much easier customization of how JSON is handled by Flask or any Flask extension. - Removed deprecated internal ``flask.session`` module alias. Use ``flask.sessions`` instead to get the session module. This is not to be confused with ``flask.session`` the session proxy. - Templates can now be rendered without request context. The behavior is slightly different as the ``request``, ``session`` and ``g`` objects will not be available and blueprint's context processors are not called. - The config object is now available to the template as a real global and not through a context processor which makes it available even in imported templates by default. - Added an option to generate non-ascii encoded JSON which should result in less bytes being transmitted over the network. It's disabled by default to not cause confusion with existing libraries that might expect ``flask.json.dumps`` to return bytes by default. - ``flask.g`` is now stored on the app context instead of the request context. - ``flask.g`` now gained a ``get()`` method for not erroring out on non existing items. - ``flask.g`` now can be used with the ``in`` operator to see what's defined and it now is iterable and will yield all attributes stored. - ``flask.Flask.request_globals_class`` got renamed to ``flask.Flask.app_ctx_globals_class`` which is a better name to what it does since 0.10. - ``request``, ``session`` and ``g`` are now also added as proxies to the template context which makes them available in imported templates. One has to be very careful with those though because usage outside of macros might cause caching. - Flask will no longer invoke the wrong error handlers if a proxy exception is passed through. - Added a workaround for chrome's cookies in localhost not working as intended with domain names. - Changed logic for picking defaults for cookie values from sessions to work better with Google Chrome. - Added ``message_flashed`` signal that simplifies flashing testing. - Added support for copying of request contexts for better working with greenlets. - Removed custom JSON HTTP exception subclasses. If you were relying on them you can reintroduce them again yourself trivially. Using them however is strongly discouraged as the interface was flawed. - Python requirements changed: requiring Python 2.6 or 2.7 now to prepare for Python 3.3 port. - Changed how the teardown system is informed about exceptions. This is now more reliable in case something handles an exception halfway through the error handling process. - Request context preservation in debug mode now keeps the exception information around which means that teardown handlers are able to distinguish error from success cases. - Added the ``JSONIFY_PRETTYPRINT_REGULAR`` configuration variable. - Flask now orders JSON keys by default to not trash HTTP caches due to different hash seeds between different workers. - Added ``appcontext_pushed`` and ``appcontext_popped`` signals. - The builtin run method now takes the ``SERVER_NAME`` into account when picking the default port to run on. - Added ``flask.request.get_json()`` as a replacement for the old ``flask.request.json`` property. Version 0.9 ----------- Released 2012-07-01, codename Campari - The ``Request.on_json_loading_failed`` now returns a JSON formatted response by default. - The ``url_for`` function now can generate anchors to the generated links. - The ``url_for`` function now can also explicitly generate URL rules specific to a given HTTP method. - Logger now only returns the debug log setting if it was not set explicitly. - Unregister a circular dependency between the WSGI environment and the request object when shutting down the request. This means that environ ``werkzeug.request`` will be ``None`` after the response was returned to the WSGI server but has the advantage that the garbage collector is not needed on CPython to tear down the request unless the user created circular dependencies themselves. - Session is now stored after callbacks so that if the session payload is stored in the session you can still modify it in an after request callback. - The ``Flask`` class will avoid importing the provided import name if it can (the required first parameter), to benefit tools which build Flask instances programmatically. The Flask class will fall back to using import on systems with custom module hooks, e.g. Google App Engine, or when the import name is inside a zip archive (usually an egg) prior to Python 2.7. - Blueprints now have a decorator to add custom template filters application wide, ``Blueprint.app_template_filter``. - The Flask and Blueprint classes now have a non-decorator method for adding custom template filters application wide, ``Flask.add_template_filter`` and ``Blueprint.add_app_template_filter``. - The ``get_flashed_messages`` function now allows rendering flashed message categories in separate blocks, through a ``category_filter`` argument. - The ``Flask.run`` method now accepts ``None`` for ``host`` and ``port`` arguments, using default values when ``None``. This allows for calling run using configuration values, e.g. ``app.run(app.config.get('MYHOST'), app.config.get('MYPORT'))``, with proper behavior whether or not a config file is provided. - The ``render_template`` method now accepts a either an iterable of template names or a single template name. Previously, it only accepted a single template name. On an iterable, the first template found is rendered. - Added ``Flask.app_context`` which works very similar to the request context but only provides access to the current application. This also adds support for URL generation without an active request context. - View functions can now return a tuple with the first instance being an instance of ``Response``. This allows for returning ``jsonify(error="error msg"), 400`` from a view function. - ``Flask`` and ``Blueprint`` now provide a ``get_send_file_max_age`` hook for subclasses to override behavior of serving static files from Flask when using ``Flask.send_static_file`` (used for the default static file handler) and ``helpers.send_file``. This hook is provided a filename, which for example allows changing cache controls by file extension. The default max-age for ``send_file`` and static files can be configured through a new ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable, which is used in the default ``get_send_file_max_age`` implementation. - Fixed an assumption in sessions implementation which could break message flashing on sessions implementations which use external storage. - Changed the behavior of tuple return values from functions. They are no longer arguments to the response object, they now have a defined meaning. - Added ``Flask.request_globals_class`` to allow a specific class to be used on creation of the ``g`` instance of each request. - Added ``required_methods`` attribute to view functions to force-add methods on registration. - Added ``flask.after_this_request``. - Added ``flask.stream_with_context`` and the ability to push contexts multiple times without producing unexpected behavior. Version 0.8.1 ------------- Released 2012-07-01 - Fixed an issue with the undocumented ``flask.session`` module to not work properly on Python 2.5. It should not be used but did cause some problems for package managers. Version 0.8 ----------- Released 2011-09-29, codename Rakija - Refactored session support into a session interface so that the implementation of the sessions can be changed without having to override the Flask class. - Empty session cookies are now deleted properly automatically. - View functions can now opt out of getting the automatic OPTIONS implementation. - HTTP exceptions and Bad Request errors can now be trapped so that they show up normally in the traceback. - Flask in debug mode is now detecting some common problems and tries to warn you about them. - Flask in debug mode will now complain with an assertion error if a view was attached after the first request was handled. This gives earlier feedback when users forget to import view code ahead of time. - Added the ability to register callbacks that are only triggered once at the beginning of the first request with ``Flask.before_first_request``. - Malformed JSON data will now trigger a bad request HTTP exception instead of a value error which usually would result in a 500 internal server error if not handled. This is a backwards incompatible change. - Applications now not only have a root path where the resources and modules are located but also an instance path which is the designated place to drop files that are modified at runtime (uploads etc.). Also this is conceptually only instance depending and outside version control so it's the perfect place to put configuration files etc. - Added the ``APPLICATION_ROOT`` configuration variable. - Implemented ``TestClient.session_transaction`` to easily modify sessions from the test environment. - Refactored test client internally. The ``APPLICATION_ROOT`` configuration variable as well as ``SERVER_NAME`` are now properly used by the test client as defaults. - Added ``View.decorators`` to support simpler decorating of pluggable (class-based) views. - Fixed an issue where the test client if used with the "with" statement did not trigger the execution of the teardown handlers. - Added finer control over the session cookie parameters. - HEAD requests to a method view now automatically dispatch to the ``get`` method if no handler was implemented. - Implemented the virtual ``flask.ext`` package to import extensions from. - The context preservation on exceptions is now an integral component of Flask itself and no longer of the test client. This cleaned up some internal logic and lowers the odds of runaway request contexts in unittests. - Fixed the Jinja2 environment's ``list_templates`` method not returning the correct names when blueprints or modules were involved. Version 0.7.2 ------------- Released 2011-07-06 - Fixed an issue with URL processors not properly working on blueprints. Version 0.7.1 ------------- Released 2011-06-29 - Added missing future import that broke 2.5 compatibility. - Fixed an infinite redirect issue with blueprints. Version 0.7 ----------- Released 2011-06-28, codename Grappa - Added ``Flask.make_default_options_response`` which can be used by subclasses to alter the default behavior for ``OPTIONS`` responses. - Unbound locals now raise a proper ``RuntimeError`` instead of an ``AttributeError``. - Mimetype guessing and etag support based on file objects is now deprecated for ``send_file`` because it was unreliable. Pass filenames instead or attach your own etags and provide a proper mimetype by hand. - Static file handling for modules now requires the name of the static folder to be supplied explicitly. The previous autodetection was not reliable and caused issues on Google's App Engine. Until 1.0 the old behavior will continue to work but issue dependency warnings. - Fixed a problem for Flask to run on jython. - Added a ``PROPAGATE_EXCEPTIONS`` configuration variable that can be used to flip the setting of exception propagation which previously was linked to ``DEBUG`` alone and is now linked to either ``DEBUG`` or ``TESTING``. - Flask no longer internally depends on rules being added through the ``add_url_rule`` function and can now also accept regular werkzeug rules added to the url map. - Added an ``endpoint`` method to the flask application object which allows one to register a callback to an arbitrary endpoint with a decorator. - Use Last-Modified for static file sending instead of Date which was incorrectly introduced in 0.6. - Added ``create_jinja_loader`` to override the loader creation process. - Implemented a silent flag for ``config.from_pyfile``. - Added ``teardown_request`` decorator, for functions that should run at the end of a request regardless of whether an exception occurred. Also the behavior for ``after_request`` was changed. It's now no longer executed when an exception is raised. - Implemented ``has_request_context``. - Deprecated ``init_jinja_globals``. Override the ``Flask.create_jinja_environment`` method instead to achieve the same functionality. - Added ``safe_join``. - The automatic JSON request data unpacking now looks at the charset mimetype parameter. - Don't modify the session on ``get_flashed_messages`` if there are no messages in the session. - ``before_request`` handlers are now able to abort requests with errors. - It is not possible to define user exception handlers. That way you can provide custom error messages from a central hub for certain errors that might occur during request processing (for instance database connection errors, timeouts from remote resources etc.). - Blueprints can provide blueprint specific error handlers. - Implemented generic class-based views. Version 0.6.1 ------------- Released 2010-12-31 - Fixed an issue where the default ``OPTIONS`` response was not exposing all valid methods in the ``Allow`` header. - Jinja2 template loading syntax now allows "./" in front of a template load path. Previously this caused issues with module setups. - Fixed an issue where the subdomain setting for modules was ignored for the static folder. - Fixed a security problem that allowed clients to download arbitrary files if the host server was a windows based operating system and the client uses backslashes to escape the directory the files where exposed from. Version 0.6 ----------- Released 2010-07-27, codename Whisky - After request functions are now called in reverse order of registration. - OPTIONS is now automatically implemented by Flask unless the application explicitly adds 'OPTIONS' as method to the URL rule. In this case no automatic OPTIONS handling kicks in. - Static rules are now even in place if there is no static folder for the module. This was implemented to aid GAE which will remove the static folder if it's part of a mapping in the .yml file. - ``Flask.config`` is now available in the templates as ``config``. - Context processors will no longer override values passed directly to the render function. - Added the ability to limit the incoming request data with the new ``MAX_CONTENT_LENGTH`` configuration value. - The endpoint for the ``Module.add_url_rule`` method is now optional to be consistent with the function of the same name on the application object. - Added a ``make_response`` function that simplifies creating response object instances in views. - Added signalling support based on blinker. This feature is currently optional and supposed to be used by extensions and applications. If you want to use it, make sure to have ``blinker`` installed. - Refactored the way URL adapters are created. This process is now fully customizable with the ``Flask.create_url_adapter`` method. - Modules can now register for a subdomain instead of just an URL prefix. This makes it possible to bind a whole module to a configurable subdomain. Version 0.5.2 ------------- Released 2010-07-15 - Fixed another issue with loading templates from directories when modules were used. Version 0.5.1 ------------- Released 2010-07-06 - Fixes an issue with template loading from directories when modules where used. Version 0.5 ----------- Released 2010-07-06, codename Calvados - Fixed a bug with subdomains that was caused by the inability to specify the server name. The server name can now be set with the ``SERVER_NAME`` config key. This key is now also used to set the session cookie cross-subdomain wide. - Autoescaping is no longer active for all templates. Instead it is only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. Inside templates this behavior can be changed with the ``autoescape`` tag. - Refactored Flask internally. It now consists of more than a single file. - ``send_file`` now emits etags and has the ability to do conditional responses builtin. - (temporarily) dropped support for zipped applications. This was a rarely used feature and led to some confusing behavior. - Added support for per-package template and static-file directories. - Removed support for ``create_jinja_loader`` which is no longer used in 0.5 due to the improved module support. - Added a helper function to expose files from any directory. Version 0.4 ----------- Released 2010-06-18, codename Rakia - Added the ability to register application wide error handlers from modules. - ``Flask.after_request`` handlers are now also invoked if the request dies with an exception and an error handling page kicks in. - Test client has not the ability to preserve the request context for a little longer. This can also be used to trigger custom requests that do not pop the request stack for testing. - Because the Python standard library caches loggers, the name of the logger is configurable now to better support unittests. - Added ``TESTING`` switch that can activate unittesting helpers. - The logger switches to ``DEBUG`` mode now if debug is enabled. Version 0.3.1 ------------- Released 2010-05-28 - Fixed a error reporting bug with ``Config.from_envvar``. - Removed some unused code. - Release does no longer include development leftover files (.git folder for themes, built documentation in zip and pdf file and some .pyc files) Version 0.3 ----------- Released 2010-05-28, codename Schnaps - Added support for categories for flashed messages. - The application now configures a ``logging.Handler`` and will log request handling exceptions to that logger when not in debug mode. This makes it possible to receive mails on server errors for example. - Added support for context binding that does not require the use of the with statement for playing in the console. - The request context is now available within the with statement making it possible to further push the request context or pop it. - Added support for configurations. Version 0.2 ----------- Released 2010-05-12, codename J?germeister - Various bugfixes - Integrated JSON support - Added ``get_template_attribute`` helper function. - ``Flask.add_url_rule`` can now also register a view function. - Refactored internal request dispatching. - Server listens on 127.0.0.1 by default now to fix issues with chrome. - Added external URL support. - Added support for ``send_file``. - Module support and internal request handling refactoring to better support pluggable applications. - Sessions can be set to be permanent now on a per-session basis. - Better error reporting on missing secret keys. - Added support for Google Appengine. Version 0.1 ----------- Released 2010-04-16 - First public preview release. flask-3.1.1/LICENSE.txt000066400000000000000000000027031501065614000144370ustar00rootroot00000000000000Copyright 2010 Pallets Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. flask-3.1.1/README.md000066400000000000000000000027161501065614000140770ustar00rootroot00000000000000# Flask Flask is a lightweight [WSGI] web application framework. It is designed to make getting started quick and easy, with the ability to scale up to complex applications. It began as a simple wrapper around [Werkzeug] and [Jinja], and has become one of the most popular Python web application frameworks. Flask offers suggestions, but doesn't enforce any dependencies or project layout. It is up to the developer to choose the tools and libraries they want to use. There are many extensions provided by the community that make adding new functionality easy. [WSGI]: https://wsgi.readthedocs.io/ [Werkzeug]: https://werkzeug.palletsprojects.com/ [Jinja]: https://jinja.palletsprojects.com/ ## A Simple Example ```python # save this as app.py from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello, World!" ``` ``` $ flask run * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) ``` ## Donate The Pallets organization develops and supports Flask and the libraries it uses. In order to grow the community of contributors and users, and allow the maintainers to devote more time to the projects, [please donate today]. [please donate today]: https://palletsprojects.com/donate ## Contributing See our [detailed contributing documentation][contrib] for many ways to contribute, including reporting issues, requesting features, asking or answering questions, and making PRs. [contrib]: https://palletsprojects.com/contributing/ flask-3.1.1/docs/000077500000000000000000000000001501065614000135425ustar00rootroot00000000000000flask-3.1.1/docs/Makefile000066400000000000000000000011721501065614000152030ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) flask-3.1.1/docs/_static/000077500000000000000000000000001501065614000151705ustar00rootroot00000000000000flask-3.1.1/docs/_static/debugger.png000066400000000000000000006260211501065614000174710ustar00rootroot00000000000000‰PNG  IHDRvV9€IDATxÚìwxTÅúÇ?g{K!$„:Ò{fD@DPÄÞ{ë׊^õнw vE½Š½ (Mzï„$d“l?ïïM–Hv7€íÎçyxÈ){Μ©ßyç0¶›o¾ù'Nœèp8Zëºn …B¡P(ŠxÐ4 ƒÁ WUUm|ë­·Þú׿þ5ðj€í•W^y{êÔ©'ïٳǣbK¡P( …BqHØívÒÓÓyõÕWçœsÎ9L×\sÍÍS§N=yÓ¦Møý~4MS±¤P( …B¡8$*++)//gêÔ©'ÿöÛo7~øáçNgjyy¹œ …B¡P(ŠÃ‚¦i„B!ŒF#™™™Í v»=Çëõ*Á©P( …B¡8ìÂÓëõb·Ûs º®[U”( …B¡P(Žº®[ "RQ¡P( …B¡8RˆHȤ¢á°F(&“ ƒÁ@(R. Åa._F£]× ƒª|) Å_ %:#‹…ªª*òóó1™TÔ*‡›`0H³fÍp:! …BñÊ\pÐ4@ @~~>yyy¤¦¦ªFQ¡8̺ݻw³uëVZ¶l‰¦i¨M, …â¯#•”9î0 "Øl66nÜHrr2mÛ¶U‹ì+G€öíÛS^^ÎŽ;hݺ5G ³+ Å_„Æ‹N0Ðm6ÄnKXOƒ× ºq7£1ØÃÏÓ=h! ¯AV H4Ø1iá úAñQ®{ÐÑÑ£×`0àõz äååáóùƒ*w)‡¯×K^^‹-Âëõb0”µS¡P(þÖ¢S× ¥$¢»˜·ïݺôés²Ñ*<÷–Á£B !–4BZZåN Þ áרš£;›c7š¿4cLÒUG'ØB.–{·Qá_¸Ì™t±µÀME¡½0éi±XضmÉÉɤ§§SQQ¡r–Bq¤¥¥‘’’BAA¹¹¹ø|>1 …Bñ·ºhÞóÖ4¹ãY—¯ÆXYzª‹ò.)¾|<Üæ˜w¡aY§¡°å¢•o!%ÿN’BßcUa«u¢Ô8„²¬ËÄ–˜}Û NA§•)›ßª¶3³ò.æš±W+R¼ÉŒªèŵÎËèæÈasp'á ñx<´iÓFùq*G¿ßOóæÍY¾|¹QP(Š¿Ú¢E‹BÍš53”——Ç 8uÍ3p,\Cö%÷bÝ»²2 ÅNß[ ;Šð§6aÇÓÓñô> ÓŽÂú-ž¢²å`ÚûÙ›OÇéȇÀdKÈ`* ²*‹-gLé†Ñ»´ƒ?/„NkS6ßW-áŒÊK(IÚöL0&UßPžÒÊrxÃù4C=Ø܉C£#Ðb±ŸŸ¦iôïߟÊÊʈ֫Yèijoƒž¨ñJÐEC×?y–B¡ãt:™?>"BVV~¿_EŠB¡PüQb2ÊÄÎÄÄDvíÚ¥Ç.:EÐÐJ÷ÒjÜÍXUÐ><°'„­šv ¬ÝßìdÓ»÷#ÉÉÊÝññÄ”D¨²‚–âLßøuCX¶iø+¨Ø“ÎÖ6ó0:\hÁ²d˜ ¤“Øá)ehÅ鸛¡øÕ°ZØYÍ ¯#aWß%̦¹-™âPY£|<5MÃl6³nÝ::tè@vvvd‘VýVo@¨òéèÕÑ£!ìK—ðß1`B'¤‡õ´×æÂésã´°YMh(·5…bv»;w²fÍÚ¶mK0< Â3›Í¿KE Õº¼ …â‰ZÿÕˆÎØ‡× BÉNšÞÿ–¢èß*}ÕcÚÕK$|®}s, V“öìûÞw9†ŠÊU“f dL%¹à^œ¶=`u‚OÝzµÊ2¾X¸l{HÎŒ½íÿ9ä>ày ¤ÈÕ•àN؆žà«_¨Z|~°¶Ã°˜»ŸãUÛ=ìÅ¿ª3™La·ÛÉÎÎÆëõîkðLU>áã¥Tø„]‚?(h"hzø]A]G›Á7¨a2êø*¤o%Ô¬‰IÎÑ—Ý„/ ×I`“Ù^ ô5*üþ°€Õ_†2šÌ˜Íµß§!ª¶0iÿ3ËêJÄj€ÊŠòê8Öþößl4Ñ4P(´_•žÄóGLäñz½dgg³uëVŠŠŠÈÈȈ¸¶hš†Á`   €ŠŠ ŒF㋳ÙLÓ¦M1›Íj¨_¡PüÏŠÎÚmBƒº)Ö‡êV ¦‚"W®…ì&à Ô8Q‚.Õº£Z|zÕ„„kØSP„nµ`ðÖuöƒªŠH | I€_Ý! jvæ ™ÂB1$@bÙ·”V!V+ZÈ[çyNƒ•u¾æƒ³)øƒáIï5ZM´êpêákÎ æz–°ÎW@’ÅJ…îm”•£´´”Ö­[£iº®GDˆA_0Ä.·N6Y8Ê…Bw°úZØÂÔ5ìF?íÅl®J¦Y’¢­UŒ™7‡OÚßÀo%>ü‡y¿Æ^Ã`¶`1¼1 ‚èZè°‰AƒAÃdµ`9 ñ6ôÉá¸pÉAII.@(++ÇPË5Äd2±wëZvìöÒ¦][Ìf]?|‚K×u,Ž$ìfðU¹ñÃŽØQét:ñûýx½Þ:BZÓ4L&V«ŸÏ™1aK¢®ëøýþß]|×”µ¬¬,6nÜHÓ¦M÷åJ£‘òòröìÙCffæÆƒòòr¶lÙB›6mÔº¡ …âƒÁ€®ë1Ý»¥ÓlDÛQŠVTÉNéa!'ÕÎÇCááö†Ýåh»J‘¼f°¿¦Ó,h¾| úÆp(|T N=<¬ Acøy0èÑüE`Ëbÿ1S(¤D+S"BÕ£êÕÏŒ°ZÔš)ÑöR(&ÕÒœ%âL&JJJ°X,äääÐX˜MFʪB¼3wvSMPtt]GGªÝ„õ!¾™e„t–´CÙÒu¤¤¤b2›ötF\.³g\ÇÕÍÂ`sÒªM+ü%;ØQTÆD^üï7wT3ÜnwD8ÔˆâšÌ¡Œ˜M‚ !]ªß^¶Êl1£‰N D0àtÙùüåGù× o°yÛ.tM=ˆ3çx¾øìEZ$¨¨ô¡L˜ þ@ ÕRšgia ­1¼~(T-Òµç ³ÙD0а¥V3@×1˜Ì4!à€7zˆ@ ~¦¦¡i솚†¡:öÿV ˆMØM!~þzÄ6ôß篶k8¾|åfÆÜ³”E›é”,‘¥²j~Ä*l2 Ô3ìºï>Á@€`HÇbµQ±k9_-ÛA¯£‡’l…`H9njâƒKõ½Áš8®ù^³=b•3™Løý~~úé'òòòHNN®cÑt8|õÕWüúë¯\qÅ8|>&“‰/¿ü’&MšÐ£G}n%šV§ç‹xLHHÀ`0PQQ©¸ .— ]×q»ÝuÄÍ{¼^/999lß¾’’’““ ƒèºŽÕjÅn·c±X0™LGL &%%±yófŠŠŠÈÊÊR3é Åÿ$±⛽nÐÀd‹ÌšÔ®Ëk¿SLZ³×à 3à©û[¡–H¬uN3W«Z9èóD3 c‹ÊšŸjRßí蚉4ÈñQ#:³³³±X,ø|¾"Ýdqòå3ÿ•+‘ªB¼~£ÉDbB£¡ºq5‚fÀh bƒÑ€® hN¿Íh%ì°/à>Ÿ¼Î½9ë|ÆÊ|î{ôR:ŸÀÅS{àõ…èÖ¡Y†?!!€r·§ÓY'ŒV+ðRVåÃduà´í³ªZmà÷xÑ0°æ³wønÞ .›~5‰!?¾ ˜štÄbôfÃå²Tÿ*ˆ/$ÁåÚâ@PHLØ÷n‹%ü]å啨°Ô “ß 11üûв2tÍökÕu0šIp:ê|ƒÍf¯{l‡òòr\ h@Ey9:`q$`3BUy9ºÅËfªó­¾ XLpç”Ñì=æß,xýZðûð„ÂÖÞêÐn¢yZ8ÃÛlvl6;•åe%,Bj}$v ¬¬ ÍP½J‚º@RRb$[×ÄÙ³v§œr;Ë*ü4³Ceœµ Ýzð`Vì°å¡Á _ @Hk‡Ø³@?Кé–¦äèiÜ[ýÛˆß'’5Âׂ%ä詤[2 H| ’Ñh¤´´ƒÁ@nnn½»¢ÕCé!•e´ÊËÅé°RXOɞݔí-¡lo1îÒbÊKKp—•P^ZŒgoFM[hHÄ^¯—Þc&qÿŒÜûȳôI€Sλƒ{ïÁ̇þÅÓÆñ¯—?'ÑåÀáLäûÿ<Ìé®Aó†xòîë¹û‰'9up7ÚµjËy7ÞK…Ù†ÃnÇi3óòŒ›èغ¹m;óχfc±ÛÂñô‘Òី÷^î½ÿß<ôп¹ÿæóhj×xþѸæò9åè.´p9xàÅûoàšÛ®¥ûQm9múÃ$&8Y5çuïHË¼Ö ;ãÖ—It:Xöγ\}ÝUœ;åZ¶nÏ¢Ýn –ÿÀ's Û혌á¸5Ù8‚Å<òÏ«¹éΛpTŽê5˜æþÌ33®¤u^.}†ŸÌW«¶âr:øôÑ;˜|ÅÝx[¾•i—ÞÌnÍË3ﺒ.Gµç¨®Ý¸éžYXMÂó·çû]Â’OþűãÿÁž «– «=•”l3®ŸN—vytì5ˆ—¿\31 WR{×-`ü>dg5gÀˆ©,ÙVARR†êt͈#)‘ÍK?çÄ!½éر=ƒFLá·]>*7Ã)< 8­OÞþ®Ïoì‹›¼6|µioA.Å;/<úÓ¯û'ç0ˆV­r}ñml¯´âp8pÚL¼|ßÍÕ¶-í;tæ®»îÂjµâõz9óÌ3ñx<\uÕU¼õÖ[¸\®:ùÙn·ÓµkW>ÿüs~ýõW¬VkD0Z,–ˆœ?>ß|ó , ¨¨«ÕŠÁ` ²²’òòr<ÏnL&f³™|Y³fa·Û±ÛíÌš5‹|sïr==kÇC‹-0 ”––Fü7ƒÁ ÉÉÉø|¾˜‡}C("!!]×## …Bñ¿héŒÅÚ³è4øý„RS(í×vîKµ¨«±‹O ‹O«ò÷PÖ¿'¡Ô YÎDÓýhödöZN†2ÀƒÆŒaQk……­9å°×zØ“ÑôŸW%>ò,iŒÑ‡@Ån°ÂB³vÕâØj€Š=Œ !Ï’F•Ä7$f6›Ù³gMš4Áét68@Â#æX¬.½üJxp&-[µ!¨ƒf4c0Yêþ3Z0˜¬h3Ò÷‰æZædƒÁ@…ÛWÜ›ðéàÞµ£ú…~VýüÓožAÈ`Âd„û¦_ËÛ‹òq¥&óî}ñÏ+.ÇyÔ1œuò^zðœpÕã˜-^ºk*çÞú'}NÄÝ×O亇眔JÅÞͼ÷Ó/üøõ—|þù™¿r#šÉÊÏs_ä‘'¤8¹#SN…ÓQÅkÿü7Üó0mz˘áýÙòóÿÑû” ؙދóÏ=›o? ~§0šðçÏçÑ™ñÉ|gL>›Má¬c†pò±gP©[p8¢Ãh1c²ø™u÷£úhÆŒÃ3Ïûì3>ûì3ž{î9~øaÜàf Á`§ÓI“&Mؽ{wdÆz("11“ÉDiii½Âõpõî].Wä …âUxF#®šØ¸»Œ½Œ!éÛE8~]½[CHÂþ“`2†…Ý/ë©êÞ™’ Æ`Ü]El ân~ {×}@Jùod«žPTÝÈ50éPî¥TïŠ;û̃WüÀ.}×'ÇK¿g½}>Ø»†­¨¡`µ5‚ð,¥må ®O>]úž¸ÖÁ¬ñ=Z´hQ¯•³fˆQ×C8Nv—lçƒ>`ÆŒ 2„÷Þ{ƒ!ìcg00 ‘ž‚‘ðä =¤Çî‹q'°pÿ“÷1gôt¾ßì‹ùfÜõÙƒhÆr*0xòÌzö²“ 9ïžGØsÛY¼2ó5:Ÿzwßvö@óßý3ïšÁCלL³ì…_2õÔ1XB^B!AÓnà“GnÅÐÉì7ysf‡Ãà^E‰YãäkgóÎ}ã˜6¸)¾œQlùò?\:*‡´çñƺ*ú¹ì˜RsùqÕ·´µ!=ô$'ÓIÔ½x}cÄÔs¸ø–ÇØUi&/órúÜ=ƒòASyàÖóðW”áñj±TYV@(èâ£o—sbk#üë*ÚÛšñÂËßÓ¦ÏϬ+ ðögO0¾g#stw ¾„™{~L`e«+àè¼ÞŒ™|gN96˜Ñ†»o<‹WÞßȃ/|8yyyˆ‹-"??ŸŒŒ ‡—ËE(ª“§k|6 Ä3Ï<ÃÅ_ À3Ïgí[|!8êøsÈ3ÁŸ,cÃçâ3esîq-À·¶“AO‹Ü‰£I± «×¬ÇérR¸æ{ŽîÛ•£:õa£–G¯1Ã(Îß‚-k Ö­cÝÊe,Yº”ço¹€ÒÊ:æõ '©þ¶ {·­±C³i'Œ8nB佩‡“‡‰;ܽ^²òÚ“a|”º½Œ?çR®>ÿtB/þ`­/ô°4qÒ¶sÛÊ=`»1ìógLnJR³Œp\B`4Q3çÞa·b0ÙQSŸ|— G÷et·3¹òá7IÊt*‚§|OØb†¡–?'x+K1¹ÚÒ¿uÍS3>¸+e¿²y‹ƒ=‰{®œH§Ž9ëæhסY)¶°?*PUéÁÜr_ýç^ŠfߊËd óÐSù±$l¡Û¶}7 lÛT½É€?HV^{šX_%•þÐÖÂ<þD:U§@Nj*¶{7n‹…ÎÜ—î#GŽ$--²²2JKKvíÚuPk¤¦ikãå—_ÎêÕ«ùå—_HKKÃ`0à®öÎÈÈ ªª ]×ILL$ E¬¦5χÃÁÞ½{è{ …âÏháŒUtÆ9‘È€±°„`F Û_»ƒ¤ÿûŒÄ_~ÃT°7Ü@wkNYßî”u<˜-˜ KcºV3`ô²gßîÊòß ÉóÆŠßÂã Ý(KAE³I-FŒþ¤ýר*$ÛÒ„wS^á÷ë|ìû‰=ÚnÒéÈ)2ˆsRÎ$Õlbg¨#Æ8>߀Ïç#ТE‹ƒÎXßO¥¢ë!¨Ø]­·ÞJ0ŒÌÌÕ"3¬µ:–N`4š«…Jt;lí«þ*Ö„L®8c8Ï=s Ë­¥ wÙ0 yYöó8¥5«.d¯Gh‘ÁîâJºœy_=pŸ@i!› ³ú÷VTâHlÊQ) @iµ?Süoíˆ)¤p× ‹ÚHOÐùvñÀá¼R¼ÍÉlâ@_cDó{ð‰h˜Ì–Z‚ÄD Pkº¶¦a ê„*ª;3¡ ø|añïózÑk–ÜÒ¼)ÕÏ*Þ±…ªÝnB~ðºšóì‡?ð¬TðùOrüäKÙ¼'™¹M"è­ÂžZÝ+ÓÙç«bµ: y7³©R]A–ý¶†ÄmIsþˆîµòòÇ¿Ð#ÉG@±mÍ&B MðÖÌj– ÅE ¾b’œáôÎÈrV‹xAó{ðÞA3;pÙŒ ~ÊÊëZ< |û&ÜTùý`а:“qyü¬ø~1Œl ÀÚµk)..Æétƒ4iÒ¤N¯uÿ¼¯ë:999L˜0·Þz‹ììl† ‚Íf£²²·ÛMfffX{<ƨë¶Fl6¯½öÏ=÷Ï>û,]tÅÅÅœ}öÙ„B¡ý2kf²·hÑ‚%K–àóù"ï5¸\.Ün7IIIGl²Oi~~>Øl65±H¡PüψÎXGeãwt20îÞ‹Øl”^z:eSODÛ^šGš$"N'ÆÝ¥h¥{œÁbÄàÛf´ãÉ»€Jÿd4õ¼% ÍâÀØæó4(8÷ O#;C{H4ÚùGÚ…œ˜Œ?°а˜“ijvR*fg¨4.Á áÙÀÛ¶m#))‰ôôô}Íjl"‚Å^¶% F&NÔÌè­iÐk Pc­F^zwKÒ4@ Q\ iî°¨Ñ$€`çâ[oáÎ#Y¼·lnuj;i™âƒû&ðïvïÑ5ÉÍÉW>O«ã/'7/—ñ§öeúƒñʰ\zºª8÷Ì3É8ù6>}ºf“’5?òØÞ&]wSQåÖ؎³Ï¿²Œ=¦Š}a’Å{*q{ƒúµ×Ç{Sïgêuƒ¹ô´lþ1y Ø»rz—¾¹¢â½è€f±ã²TpøÁ|YÔ“ÿ~ùé–*ª<¡È³÷쩤ܾԃ>‚·zñü ¯wÑ*€Çö§ò¥‡¸ö‰÷ÓÒÃ)W¾@€^tL‡‡.Ì­ïûøìóÙtìÜ `±„ó«)Aç³_æ“o†0¬OkÌæ5£¥f› ”2xÔ|úÈ5Ì}öVæíöóÚ™#é•ݳ<Æ…_ÃËw\À/Ìà–W¿göKhg³á®¨ÀåJÀ½õkZçžÊÉ×=ÇŒ‹N¢U“ ³LIéxÙÅÿÀ—žÕ䧨¤:nÌ6L…¸â–{éyÆULÞ¥zC‚pþ(+-¦Ø¸o$ ¢¬”¢­ FŸ5€ËïÇ“>"×bêÔ©œzê©$&&²gÏJJJxñÅÉÉɉ,=±îz½—’@ ÀqÇÇŽ;øá‡8p $$$ðÍ7ß0bÄJKKÙ±c:uÂ`0 …êµþ9N>ùäž~úifΜI§NxðÁ¹öÚkIKK㤓NŠ,ÿÕµ³I“&$%%QXXH‹-ðù|øý~ÒÒÒ(//ˆÁ#1±HD°X,X,JJJÈÍÍU¢S¡PüOÏ’t&³‚¸Á€æ÷cÚY„˜Œà ÏpÖ*¼h¥•‘{b—É4݇ѷ£fB¬¦jU¾’È=1ëb TŠÍÁ|,Fk8|A*Ù,‹Ü¯’ƒx<Z·nÓ," ‡t FcXlšlYãÇY[tîÓõF  Iƒ«9é!]sЭOGrÚ§Vû±'Žö#8ù(³ŠúrB—j6_%zVÃúdöƒ—3c{)™ÌoÜO0䆗þËnNçÒ3Æ`Zö<;o>;láêq ¹?l㮫. OÓGë1L9cÝ{õÁªå„×ÕA39éÖ¿'­Ò躎Ç`ÐÙ÷òön/—üãj>û? ŽôÞ|õÓG¤!èé­éÛ+“Þ.Tغm3k Ò0†Cõìu=¤£W?;/-ül³#Ví[Ñ,!ìÇgOÍ¢ó ÎTVét<íV®>s)ÏÞt³Ró~ê©™²) èL¾ú^¾^p cG Ĥ{ñ-öVœi-yã«ïÈ1éø»MàÒ‰o0ëáèÒõ8Ží?„þËWcÒu0˜ØµáGžxå5î<ñjÌfsXtJxí×v{’àʉˆª¦­Ž¢W•‰*]ç²?c+¸ñÂspX Œ;–Y³fáóùHNNæÎ;ïäÎ;ï¤k×®\z饑ar]×IOO¨š5:O9å”È "ÂÈ‘#™?>óçÏÇn·Óºuk233ñûý¿åƒUL>Ÿ¬¬,üqz÷îéÈ 0€Ç£ÑóÚ—~¿ŸœœV¬XA0Œô¾m6N§“²²2ŽØZš5Ö΢¢¢ˆ¥÷HΜW(Š¿œQtÑ¢Ez³fÍ´¨{¯ÿc±XÈÏÏ4ˆ••• Þo·(õ™¹õ¥¥lüìŸÊóÁX-:kM2ÔX8k„§€Ù[:†ÜI<}ë 4I2Pé©ob‚†ÙjAׄáÅß-fkÌ¡ßñœ0ãÞ¹esñÚ·îÉi¯®å¾SÛ°3¿ˆ¬¬L´êõ2í®DÌ(*È'ˆ‰¬Ì ˆÛíÁh4a0„öÛ«˜´ðŠVÆððÁPõ‚üf³ = ¤ ¢ëÌN\•{÷°× If3¬p»ÝhF3føê5`͘­f4SÝ ¬5Ü«Õ}6š³Éˆ AÓ -üž &› §UcÏ®|‚Öd2S„BU”U„HJJÀˆP°31YiÖ4‚^Üž ø++p{ýápHÝŽDbbxÝÓ¢‚]­ ¤¥¸ðV¸ñ‹Fb‚‹§œü=e$¦f’ä4ã©pã×û ‰®c0Ùp9­TíÝCq…Ô¦Ù8-á¸0X8­FÊK÷2±š-µpÜ$$$ðÑ­c3ãgV–pT¢wÅ>e4…ºV D£Ñ„ÑÞ@kõ¥{vÁDzZ*"ñut¹\TTT °Jjš†ÑhŒX?E«Õ±|‘cOõ$'«ÕӞ䵇߿JLL¬wqøúp:ÌŸ?€fÍšEvKÒu-[¶‘‘ÙIéHÕ;vìÀáp­‹W( "{¯‹IEEìæãŠŠ Ú·osƒ^ª^ð[³±6IÁ`²í›d^½™“V}^Õ)¼Îi@På7‘fÐдèfkðjS¡êFwÛ¼Y 9…ô~'óÌMã| fLšÜÖmx÷šefà÷Váó‡­BžŠrü&3i™h"x<•aÑ ÕÂÃnß—eDá Ð ´}{ÜKX–Öì„£ H¨ w…[B YIÁ€·Ï_½su|6!¼•€`h¿eövµe8D0"JbÀh€ÏM•n!¥i&èAªª< ,†•åå˜-V2š5CDÇSYI ¤£iநÄjµ’h1ãóúªßR-ìÌf|^/A]'µIšèTV¸Ã["”»+°Ùdg»ÐCA*ÜîêõZ«÷2Ý»"ˆ-!…ì$PЇÛí4t_%Uº»+ ßKHŒ&áÅj…Í[= 8éÚ%A Ê¿ß„†&Ú¾½ †ÈªaÊrB&3)©iˆèTy<„ªý‹E„ÊÊð7×—ûž{àÄ"¿ßñÙÔu=²9BÍRJþêeÒ¢9˜×¬ãYsomj†Ô qŒšèºNVVk×®ø—Ö¶v–––FÄ葲v&%%QZZZÇÚªP( P–Î0™Lãõz0`>Ÿ/jCb6„t>_¼›Ÿ×–£ÃT׫'E„ôÈðe¨fKÌ  £ ÷È`üˆ6XÌ&ÁØüÃŒ&3»·±© ’Ýzl Pî®BÓÂ;âøýA0± ª1ükU pWUb³Ú±™ŒøƒúßcÏûÃKš†ÍfcÞ¼yØl6ÒÒÒ"Ö[¿ßÏ–-[ÈÎÎÆh41ŸK³ÙÌ®]»"3úÔnH …BñWAY:ã f¢–-[F[C za÷“údpRßfT›¸Oê•€èCzÌ‚ è÷áÊÈ£ 3¡€—òrxOntÍ@BR"zÀ‹/Šy¯TÅŸ!R’Ó0¢Såñªô;ºv1ÉÌÌŒ §× ó;œN';vì 777²´Ò‘f³¹Îl~…B¡P4föúÿZUï±n6›#‹ÁÇ% uöN×êLÝjH6èh5ƒPÀ‹;à­>Þ7l­!x=UÕ‡J°üÕÐ4 ¿×£Ò/JUUUÑ¢E vîÜIII IIIƒAü~?ÙÙÙìܹ“;vD–Š:Üq)^›7//OM$R( %:ãÅÅÅdggcµZ#b%¤ uí”jH[¡8’躎Õj¥iÓ¦ìܹ32Ä®ë:š¦Ñ¢E ÜnwÜÈxߟ †Ö …B‰ÎبV7 ´hÑ‚ªª*eaR(þäÔ¶vîÚµ‹ÒÒRœNgd‘y¿ßÝnÇår±0ÔX; …B¡DgL˜ÍföìÙCZZN§3¼¼ ÅŸžP(DBBéééìÙ³‡äää:"°fŸB¡P(~gÑY³¬‰bƒ½{÷hÙ²eô-/ ÅŸ†š­1[¶lÉ®]»Ø»w/.—K M…B¡øƒêdÓ®]»…B1léø¿EÍ,ØÎ;ãt:ñxßa5K-©zA¡P(¢ãr¹(((“ÝnÇf³)§÷ý¨ÙI¤f§…Bñ×,Ç5ûÅ©u9 …BÑ06› »ÝŽ©ÆÇIù:)Š¿+ª~S(Š?¾V³×gD£ÑxÐ=® …B¡P(“É„®ëGdó‹C oŠ£8Ì‚Ól6عsgÄú¢†ï …B¡PªÆ¨Ñ™™™ÚÐæ£)KçïˆÑhÄï÷³uëVÒÒÒHOOW¢S¡P( ÅaEEElݺ•¼¼<ŒFãŸÊ½H‰ÎßYtæç瓜œLvv¶Š…B¡P(‡•œœ4M£  €œœœ?•èT‹ÖýŽhšF("55UE†B¡P(Š#BÍ.l¶‘T%:ÿá©fÒ* …B¡8Rèºþ§tÝS¢S¡P( …BqÄ9$ŸNÁb±`³ÙƒTVVñmæj–2 „B¡#²Ø³ˆ`0p:ôD„ÊÊÊH/Âétâõzÿ”K( …B¡øëâv»HHHøÝß­ë:‹»Ý€ÇãÁï÷’Îk´è\.Á`矞N:Ñ·o_<Ï‘UÉ&x<ÒÒÒ0 ‡u¸ZDp8˜L&6oÞÌ’%Kزe yyyôèу–-[ 1™L¬\¹’äädRSSûº›åååüðì\¹ÇCóæÍéÛ·/]ºtQ%Q¡P(Š¿9~ø!S¦LùÝßmµZñù|¼ñÆŒ7«ÕJ hô3%Wk§Á`àñÇç•W^‰l5w¤©Ùž²¸¸˜Ý»w£iÚa³®êºŽËåॗ^âúë¯çý÷ßgÇŽìØ±ƒwß}—ë®»ŽW_}“ÉÄÒ¥Kyúé§#½ÃÉÛo¿ÍرcyàX¾|9›6mbΜ9\xá…\yå•ìܹ3†„ò³bÁ¯lÚRzÄÓ%è)å×…¿RìÓ¿üʦ­¥‰Bò{p»«¼Ç[é¦ÒŒÒ#-' Üu …Bq˜X¸p! .üCÞm³Ù˜;w.3gÎdæÌ™Ì;›ÍvHÏŒ[%Öœÿþ÷¿ùì³Ï¸ãŽ;6lG^܃$&&"" "dff²ÅSD"{Ð?ðÀ|ÿý÷œuÖY :”¦M›°k×.¾ûî;æÎËÎ;ÙµkÁ`§ÓyX‡ù{ì1žxâ ¦M›Æ„ hÓ¦ ¶|Ο?Ÿ|³Ï>›_|‘–-[6ð¤ÆõïK‡?âÃûO9¢é²{Ý×ôí3Ž7ÖVpF;×ï¾7èaÉ’•$ç¶'/ãàCŠBÞûhE{*1Mh¸K÷ÒñèQŒÒ7rß¼ÿ¾Å'ß-AÇ@Zv;Î>g2.ó¾Uíå½×^eÁöBL!¼þ#™|êpìµÞU^¸×^{ƒ%•hF'COÃqºªÚR¡P(fåÊ•lÛ¶-òw§N~÷0ddd––ùûP‰ËDX[p>øàƒ|öÙgÜtÓMŒ5 Çó»ÍÊ…B$''Ó´iSÊËË)((8d‹§Á`Àjµ2{öl¾ûî;n¹å¦L™BNNÁ`@ @nn.gŸ}6§v?ÿü3¥¥¥$%%ÖïþïÿËã?ν÷ÞËôéÓiÓ¦ lÛ¶ÄÄDŽ=öXfÏžÃáàæ›onØÌ­ÅFÛï±k8îÍÕ\™-`°ÛŽÌ«L¥ôëÛ‡;ßYWï-•{¶ñëÏ+Hh’G«¼ää4§e^M’“öõ ?{•'ÞœK¯£™8ñT¼k¿g棯PQcÐo=ñ(Ÿ-*ääÓ&rÚIƒùyΫ¼0ûû}ú·týû6ûS˜8qúdòÆÃòå’mªÆT( E£yï½÷èׯýúõã½÷ÞûÝß_YYIÿþýy衇x衇èß¿?•••‡Ö|Ç#8N'3gÎäçŸæÞ{ï¥_¿~Øíöˆ³im<@ Q“l’"Bzz:F£‘ÂÂB iÚ´i£-žv»ÂÂB>ýôSÆŽËСC넽fÍyóæñÉ'Ÿ€Ï磬¬ 9,“ˆ¼^/O?ý4ãÆcüøñ‘ó/¿ü2O<ñK—.Ån·“’’£>ʸqãøôÓO3fL½ªÓdØ/‘Ë÷ðö³Yº¾€Î=1ñ´ãêö<Ê÷0û£Ù,[[D—Þƒ˜0fTëK¿ù”ÿþ¸ÍÕŒÆO kNZõ« `ÿ:F‡Œ”Dv.ù†×>þ–fí{1vâh’ê¼k7oð6K×Щç Î[7,ÛW|Ë;þ@¥-…ácÆ3°M&øJxë• ÿû&´.gøða$šë~yEé.\É-™:锃îóªÚÅ;sæ1xô…œ62lùìx…«o{Šo䔾y¯žÇw«¶qöÍ1¸C Ðëƒ%<ôÖ¬ÞŽéV~ùþ¿¬/Ïà_÷\N3+нîü]|ôîG èvNƒš\¦P(Šúq»Ý¬]»£Ñˆ×ëeçÎ1þ|žyæ.¾øbžzê)222ÈÎÎŽŒÌ¶oßþˆM2 Fz÷îÑ(5玸è´X,x<^~ùe¾þúk&L˜@Û¶mÙ¸qãAgn× 7§§§c³Ùâ‚5ëYúýþï±Ûí$%%±wï^4M#---â÷F£‘•+W:th$Âk¾Ëjµ²}ûv¾þúk222HJJÂãñ™I¨,[¶Œ={ö0yòä:ç'NœÈðáñZ­‘s­ZµbÈ!Ì;·Ñ hP…ëçqbÿ‘,.ñ“™™AAÁÝÜ5h_}þYNØ»yÇöÎÂ=^23›RpÏL4…o¾ù?rÍ~®;„G>X@³¬fxöîâæ›nåÕ¹ßröÐÎû™Ìm´ÎµñÉÓøìÎJB6#ù»îbÆÌ©,øþeR­ëçqRÿ,. Ô Ë·_¾D¦ >~îfN¿è~ìYYØ}åÜvýÜþÜ÷Ü1¥ wÜù,œû7lYÅ÷‹†Q°ô^ùx!ç\|íš8ðîqcHM`[I {7¬ÁoN Sç.8Í5qñUÖ† é µµewúwLf㆕Ð7•«ÖbÍêAŸ)‘{:öJâ'‹Y¹­”ŽéMY³f5œÕ=jóž‡í¥^:¤ÚUªP(Šz …B|ûí·|ñÅdddо}{B¡Pd´`òäɬ[·Žââb>øàvïÞͱÇq¿;Øl6¼^/ÿùÏ?~<6›í&™âyùgŸ}Æ'Ÿ|BûöíY¼x1Ÿ}ö¡Pè ÖÈP(Dyy9×_=Ç{ldÚ<"ÐívSXXØ ªÖ4 “É„Õj¥´´«ÕJJJJ£"¥  ›ÍFffæBÚç󑘘ȵ×^Ñ5âÖï÷ãóù9ׯ_Ëå¢uëÖuÎçççóÛo¿Ñ¾}û:qÝ£GÞzë-f³¹þD¶%pÙøãX\’ÌÜÕ¿1²C~úð)Ž>õ2Ξq4_Þ{.7ž~, ÷8øbá&FõjÊï<È 7òà WðÄ%¹ü´t·¾ñ5wŸ1 €cò4¦^~g/‡ÅP£q3†ÂRvîMâ£ÅË8娦|úèeœtõS<÷ÝÝÜ|l—?¾:,ËÙ¡ ó>|šA§^ÊÙ÷ ã‹;ÏâÝOÓ!7±å»…Ã~r_Þüøî¸àQV¯üWj7N½ã þsÓ‰¬Û¾ÍÛvâö…Åi•‰]«âù§ H±™Ù»u;o:šqÑ5—оi2ÞªrRÌI¤:ë¤f©ì¨çw0@BNuœLNrB‚eå@S‚UBvR“:ϰ''`rZq—zA‰N…B¡P4@rr2×_=iiiÌž=›œœÎ9çœ:÷œyæ™@x’ó¼yó˜4iÒ÷ Ñ9gÎyä‘H8ÇÿûˆN¿ßOÏž=éׯK—.eܸqôéÓ‡’’’ƒ®|/"ƒAÚ´iƒ×ëm”ò·ÛídffÖ;t]#8+**(--Åétâp8mu4™LˆÈAÝjÖ$5 ø|¾ÈžUUU‡mx½&Üû‹ìyóæñÔSOqúé§×Y!Àd2E_«TÀ‘ðþ² ÆÍx‘Â"iИK¹ü˜ûxbö<ÿÍ› ËwÛÛŒêž85xü ¬Y8Œ€«Д_6o%´g_Ïý1[IÌÌF+—ê0k5¯ül-„‘—ÞÃ)G…Ÿuâ•7á¼ú)Öm*1óÁ27ãf¼ ËÀ1—pÙ13xò…×áγèœo,~“Ÿé̱£†ñäœ_ö}“Å‚ 0›œ‘SCNœDß°;Â"Ïk7àJÎàÔS'Ñ»SU›xꑼøÔûÜç4œÌ5®_|°S§Ù`ÀlÜ?]uLþzU8O53"uÓKt`PÃçªÚT¡P(11mÚ4ÒÒÒxöÙgñz½œ{•qü~?/½ô}ô—\r £Gþ]ÂÔ´iSÒÓÓ#*1‹N¯×KÓ¦M¹îºë¸÷Þ{ùöÛo9r$;vlðw>Ÿ¯×÷$Ád2‘””Tï=f³™ŠŠ ***"J£ÑØhÑ™——GUU›7o¦iÓ¦ˆÉ!˜˜ÈÚµkY²d Ç:ªe]ÖŒfìŽ}ÖÞ£GMàèQ"ÇŽÌVœsÖdîyêc ‚`7ÙðRÅþý5ñVár„‹EP×))õ"²Ò ”ÛÍ4IJ¨– |ì·6m(€ÕÀ岪ZT¡P(13zôhÊÊÊxë­·8묳"í{ `Μ9L™2åwœ•••ôë×™3gйsçCžH³Ô4òòr¹í¶ÛHIIáâ‹/fÙ²eTTTP^^ŽÛí®óïPV¯¯yû§i¥¥¥lÛ¶ £ÑHVVVÄò×ü~?:u"33“÷ß ²4Síð$&†‡ªŸ{î9>ùäL&Ó!9ÕÖ¦{÷îØíöÈb°µ¾°°°NXŠ‹‹ùú믟¾}ûât:™3gsæÌÁétþ.ðÔ& ¢ë:={ö¤gÏžèº~ȶ¸Ô`ðt:Üu×]´mÛ–[o½•åË—ãr¹~·m M&•••äççcµZÉÎÎÆh4Rdø|>guK–,áñÇ' ‘˜˜HBB $&& …xì±ÇX¶l—\r .—ë°øsÖˆÜ)S¦ð /ðóÏ?GΟxâ‰<öØcuV¸é¦›HJJâÔSOmH¶#:T¹Ãþ‡“ŽJfÖ=çòãÚÝ,úø!žùa;ÇO˜€ËžÎYÓygÆY|³²€¥ϤCïÞ¼ú]1éiá´0p‰fXöÅ,^ú|©k¦Ô}oD—ýBUåå@fuX¦ñãÚ=uÂ2úšé8(㘎Gqùm/¡9›qì‰éÕ#ªŠÐ4>`Ý‚©‘µÛÖ,äÅ×ßc§;l»Ü¶rþë>6–…oð”0ûõ7$w ‰Œi­éÜÔ›¯¿Eõ|÷ÁÛ,*µÑ·OxÍ}ú‘hÞÊ[oÿ·úŽ*f¿õ"&[=sÓ‹úôíKÑ’¯ù`Qx‰$éNžg.Y=‘å0¨ÚS¡P(q±iÓ&\.?ü0/¾ø"/¾ø"?ü0.—‹7þná°Z­x<ž{î9ž{î9<O ÍÒoñþ ¶Åó®»îâŽ;îàúë¯gÆŒôêÕëˆ/o4©¬¬dÇŽ8²²²YpÖP³&ÕÕW_Í“O>ÉÚµk9î¸ãh×®&“‰ 60gÎ6nÜÈ•W^ÉÀ©ªª:¬ß7yòd–/_Î\ÀôéÓ=z4-Z´ E‹@xØÿûßüöÛo<ÿüóËk}TAraϼó!›‡Œdp‡ Z·Éfã†uò¹üçö‰<ôÎG¬:Œá›Ñ¶usÖoÜA›ã'sóÙmI.Â1-žç¸–Vڵͣ$¤“™`¥¤pWXP†ü¯_ TAŠ»V, (()Þ/,Mê„åÿnhß2‘'ï9÷f=ŒÝ¿M…ÂÍÏÏ&À”Ë•Çwæß/_CʧŸ°µ`.Å›òòË_ұ߲’8qü9æ?Ê÷ÝC£Ú²{ãZvù¸èê „íf&L;Ÿ-<Ž·ÝGNº…K·2aÒEtË ûŠšR[rÁ”ÓyüÅÙÌÈ_‡µ*Ÿ »…ó¯:‹Äj†œÃ9sôfÞ~ò~¶õîBÁ–•ø»3mì(ÔbI …B¡ˆ‡½{÷²gÏ–.]J‡¸ÿþûÃmæ3ϰfÍš7oÎÞ½{III9âa±Ùl|üñÇ<öØc¤¤¤0a„CšH¤åççK³fÍâž]^3Ô\ZZÊ#<ÂÑG͈#~ÑY^^NEE˜ÍæÃ¶ç¹ˆ`6›±Ûí,Y²„wÞy‡­[·…BX,òòò?~<=zôˆ{ R‹Å–-[hÖ¬YƒbQ×uzè!fÏžMVV;wÆl6“ŸŸÏŠ+ÈÌÌäŸÿü'Ý»wòF?_}ú_œy}èß±YøTE/¼6›­»JiÑeÓNU·çQµ›_yù%´ì>ˆsÆKǨ”îä¥7ÞeÛîrŽz)]ü;øiã^N8~(ÁÊ">øüC³#_}ú1μ¾ôïXm õ•ññÇsiÚs(}[5‰„åÅê°ä$,¿~÷1Ÿ3 )…ã'N¡Û¬Z‰Uλo~ÈöÂ\}ŽÊ],__B›£:ᨠ°îãןç“_T‚91ýÒl¿Ùêâ|¾ÿy!e¡õQ=éÖ>ç€X,Ü´ŠËÖ Ýú ¢eSç÷¬ýå'Vî(À‘”Á ÁƒI°¨ÊS¡P(ññÅ_ðÜsÏ1iÒ$N9å”:‰>þøcÞxã .¼ðBŽ=öØŸS^^ή]»hÙ²eƒKO6„Ëåâ§Ÿ~â¶Ûnàî»ïfРAÒy ìÚµ«ñ¢³F¤ÙívÌf3^¯¿ßÿ» ±×,/"‡}$Á`0àr¹ƒlÞ¼™¢¢"B¡´iÓ&2cþ`³ö‡è¬aýúõ|ùå—¬X±ÇCNNýúõcĈ‡lâV( …Bñçâ×_ÅápÔ»ååÊ•+©¨¨ˆlÌs$E§Á`Àb±°bExþEçÎñûýÒ]‡EtÖ˜ˆÖýÇÿ  GaYYYÙh¡¯èT( …B¡ˆ—Ã!:kVð©ÙñÈív7z‰ÈÑyX6åþ½ö\ÿ½Ñuýˆ» ( …B¡PüÙ¨—µ’‡:š­¦×þ ©P( …Bñ¿¢3”èüù;º!( …B¡PZC‰Î?º®ãp8ÈÏÏW‘¡P( …∰sçNìvûŸÎýѤ’æ÷# ’‘‘Á¶mÛX½z5©©áuÔp»B¡P(ŠC¡Æ²YZZŠÉd"##ã°-)©Dç_Xxæää°wïÞCš ¦P( …B±¿èLHH 55µÑÛ‚+Ñù7ËÁ`””ÒÓÓU„( …B¡8l„B¡CÚ5H‰Î¿!Á`ðOgöV( …B¡8R¨‰D …B¡P(Š#Ž©ffÓßuw…B¡P( Å‹®ë˜<åååTUU©Q( …B¡PV4MÃãñ •••‰ËåR3¨ …B¡P(‡¡¢¢“¦i ʵS¡P( …BqøÑ4 MÓÔD"…B¡P( Å‘G‰N…B¡P( … …B¡P( %: …B¡P( %: …B¡P(Jt* …B¡P(”èT( …B¡P(”èT( …B¡P(Ñ©P( …B¡P(Ñ©P( …B¡P¢S¡P( …B¡D§B¡P( …B¡D§B¡P( …B‰N…B¡P( … …B¡P( … …B¡P( %: …B¡P( %: …B¡P(Jt* …B¡P(”èT( …B¡P(”èT( …B¡P(Ñ©P( …B¡P¢S¡P( …B¡P¢S¡P( …B¡D§B¡P( …B¡D§B¡P( …B‰N…B¡P( … …B¡P( … …B¡P( %: …B¡P(Jt* …B¡P(Jt* …B¡P(”èT( …B¡P(j0ýž/[ºt)ßÿ=&Ó¡½6 Ò¤I&Mšô?—`ÅÅżõÖ[ˆš¦Åü;AD8óÌ3IMMU9_¡P( ÅïŠV^^. ¿ËËî¹çn»í¶Ãò¬ÌÌLvíÚõ?—` , ÿþþýªU«èر£Êù …B¡P(~7Ün÷_wx]×õÿÉD3›Í*ç* …B¡øËñ»¯uÔQwÜq kš†ÉdbÙ²elݺ5"®†ŠÝn'Ô¹_DhÙ²¥J= {÷îäääÄ,“““U¤) …B¡øÝù]‡×£qË-·pß}÷ФIŠŠŠT íÇâÅ‹éÕ«Wäøã?æä“OV£P( …âOËŸnxÝãñDþÖu·Û­R) {÷îU‘ P( …âOéÏ©smÈxîܹ¡š™™Iß¾}£¾cáÂ…äççàr¹>|øaXºt)ëׯ'??Ÿ‚‚ÊÊÊ…BØívrssi×®£FÂjµÆýÅÅÅ|ñÅüúë¯lذP(DNN}úôaðàÁ´k×îâìPX±b«V­bçÎäççS^^ަi¤§§ÓªU+²³³9r$F£±Îï~ùå6nÜÈÎ;)(( ¼¼£ÑHff&yyyäææ2tèÐÓä—_~á×_¥°°«ÕJûöíéÕ«Ç<ñXâû …B¡P(Ž0åååòg᪫®@IKK“ÒÒÒïoݺuä~‹Å"^¯·Áû€¸\®Èo Tçúí·ß.;vŒ\oè_›6mä®»îŠëûî¹çiÖ¬Y½Ï´Z­rÅW4øŒE‹ÕùÍ«¯¾zÈñþàƒJÏž=Åh46øÍ‡C|>ŸˆˆƒA¹á†¤[·nQãªk×®}ï¦M›äŒ3Îhð·-Z´™3g‘oP( …BñûP^^.éÅáÇùÛï÷³xñâï_»v-‘ãÓO?½Îõùóç³zõê˜Þ½aÃþùÏ2lØ0JJJ¢Z#ÇÏ­·ÞÚà2O>ŸÇœ“O>™P(ô»ÅãóÏ?ÏâÅ‹£¾sèСX,<ò¿ýö[Ôç0à€s?üðݺuãÍ7ßlð·Û¶mãÚk¯eÊ”)‡ý …B¡Pü~˜þÊ=z4<ð@äøË/¿<¨À©áÛo¿­s¼ÿÐzZZZäo»ÝÎ1ÇC›6mÈÍÍÅjµ²|ùr–,Y¯¿þZç™C‡eÑ¢EõŠ™iÓ¦ñî»ïFŽ“’’8ÿüó9ùä“1›Í¼÷Þ{<úè£ÁôÉ'Ÿ0cÆ þùÏFƒÃ1 ¬Y³f¬[·Ïn2dݺu£]»vV¯^Íüùó1bDä7ƒæÍ›³yófš6mÊ€èÕ«-[¶Äëõ²dÉ–.]ÊàÁƒë¼oãÆŒ9¿ß9ײeKÆŒðaÃ(((`Μ9Ì™3'r}Ö¬YØl6^xá…Ãö …B¡P(~GþÊÃë@@²³³#¿éß¿ƒ÷ŸuÖY‘{sss% Ö¹~æ™gF®÷èÑ£Þç¼þúë’””TgØö†n8è½?þøcûZµj%k×®=à¾ï¾ûN"÷¹\.q»ÝQ‡×yä)))‘üüüzÿmß¾]víÚ%º®4ŒC† ‰û,r<`À^ýuøÝ 'œÀ‡~Xo|îoP( …Bqä0üÕ?`ìØ±uŽ÷)5¬Y³¦Ž Ýh}¢ÍœïÚµ+·ß~{ä8ðÿ÷uîÙ¶m_ýuäøüóϧU«Võ>sÚ´i´hÑ"r\[$ׇÇã!??Ÿâââzÿ•––²víÚ˜öjo¬/i¬¿{å•Wê?ôÐC Þ?räHŽ?þøÈñW_}ŶmÛŽÈ7( …B¡P¢³^†Nfff丶ïdmj‹?ƒÁÀ°aÃùÝ\pAe“~üñÇ:×çÏŸ_çø¼ó΋úÌîÝ»GþÞ´iSÔûSSSi×®¹¹¹õþkÞ¼9=zô8¬Ë+5–Úþ°;vlз¾xÛß7W¡P( ÅŸÓ_ýÌf3&LàñÇÂÖÁ­[·’››[¯P:t(M›6=äw§¤¤0hРˆ Ý±cGë+W®¬s|ýõד]gM F£‘`0ȼyó"çjÖmˆûÏ>û Ï¬! a0üñý‹òòrÖ®]9ŽEpuv`:X¼* …B¡P¢ówáì³ÏŽˆN€·Þz‹o¼1rìñxêˆÎùmdVVVQ0›Í,¥TÛÚz¸HMMÅf³a³ÙþôéTRRBiiiä8###¦ß¹\.œN'•••‘xV( …Bñ×ÂðwøˆÞ½{Ó³gÏÈñ¬Y³ê\ÿá‡êÀÑ£G¶w×ö¬ªª" FŽcñ¡lˆ†¬—µßùW%Ö &Ó¾þÑŸÁM@¡P( E|˜þ.rå•WrÎ9ç°lÙ2æÏŸOÿþýê¬÷Ø·o_Ú´isDDgZZv»=r\cñ¬áƒ> yóæ1Ot©ý¬¿v»›Í†×ë ¨¨(¦ßUUUÕ±nªÅÝ …B¡P¢ócâĉ\wÝuðÌ3ÏDDgí¥‡b]:(VÖ¬YSGtÖ¦mÛ¶uŽ»víJ^^ÞÿlfËÈÈ eË–‘8«íßÙÛ¶m«cÝü_ŽC…B¡P(þªþ.b³Ù8ÿüó#dzgÏÂ3Àk/•T{ëÌCeݺu,[¶,rܱcÇ:×{ôèQçø™gžùŸÎlš¦Ñ¹sçÈñ·ß~{еM÷çí·ß®sÿüóÈõ£>ºÁu2k‹¿áÝwß]çø¤“N:@tfggGŽŸ~ú阇”ãr%N;í´Èߺ®3cÆŒï/++ãå—_Ž·hт޽{«’«P( …ÙÙÙLž<9r#!!¯¾úŠádz{÷îÈ3jöONNÆl6SYYYgù£åË—Ç$:ßyçÞyç¸¾ë‚ .øCgÆ;–[n¹…{ï½7rîÉ'ŸäÉ'Ÿ¤E‹×é04mÚ”O?ýôO±È½B¡P(ŠøùSµà5¢  ¸¸8êþçõqçw N.ºè¢˜o4Xl½¤¤äÁi0¸ãŽ;xâ‰'|^çÎY°`ÁA÷{/--e÷îݬ·¹|ùòƒ>Ëçóöxß¹sgäïX&ö@ØjºeË–Èñž={âzçŒ3xüñÇINN®s~Û¶mÎcŽ9†_~ù¥Á¥®ó …B¡P(~?þT–ÎãŽ;."4“’’p8zN·nݘ8q"o¼ñFDô}ôÑq=£G<öØcÌš5‹_~ù…Í›7ãñxp8´iÓ†ž={rñÅÓ¾}û˜ž———ÇW_}Å·ß~Ë›o¾Éš5kزe hš†Ëå";;›¬¬,Ž;î8FŽyÐçdffrÆg4:ŽÔçŸ>¿ýöƒŽé9f³™ë®»."öN9唸Ãrùå—3~üxž~úi~úé'V®\Iaa!v»Ö­[Ó®];&MšÓŠù…B¡P(¿Zyy¹$$$üí>¬W¯^,^¼€W^y…©S§FýÍäÉ“yýõ×#¢³æ÷¶tú|>l6[ÜÃôõáv»©ªªBÓ4.—ë:3SRR‚Õj¥yóæj(]¡P(Š¿ n·ûïçÓ pýõ×GcóæÍ9묳â~ÆþCû5>‡“„„þŽ‚¿±¤¥¥°À¾B¡P(Š¿YS’®ëx<ž:ç***¸õÖ[y衇"çfΜ©,f …B¡P(0YKgYY={ö¤]»vŒ9’72gΜ:J&OžÌ„ T*+ …B¡P(ÑÙ8.\È–-[زe _|ñÅ×O:é¤:‹·ÇBíÙý~¿Ê …B¡P(‡‰¿ì¸s}KôtêÔ‰‡zˆ9sæÄýÌ#±‘B¡P( …â/<{]×uÖ®]ËÎ;)--ÅjµÒ¬Y3zôèÑèmW­ZÅîÝ»š4iB§NTQ( …B¡8DÜn÷ßwÉ$…B¡P( ÅŸGtªiÝ …B¡P(Š#Ž …B¡P( %: …B¡P(Jt* …B¡P(Jt* …B¡P(”èT( …B¡P(Ñ©P( …B¡P(Ñ©P( …B¡P¢S¡P( …B¡P¢S¡P( …B¡D§B¡P( …B‰N…B¡P( …B‰N…B¡P( … …BQÁP¿ÇòBÀïñU|þ/ñgJ÷PˆJ·G%›B‰N…B¡8R¼~ëTZ·MñÙ——ǹ·ýç@Å®8g"—\7/ç­kT|¶ú£ãó¯ÂL÷7½žË®¼žŸ)ˆéþ__½–ä”D>X±W¥‹â/éÏÀó>âÊSâ3a±:Hsi» úƒt=í<¿úÌ#_N˜ÂVI Áä¥ÌDCð½$5ïÀƒ=I»T³ÊMóŸ¿’ëæóù‡³pýÍ¿õÕkÎâÝI|4û‰?0œqìqtºüMnÝñˆ¾IÏßÁŽ]Ûð˜ÿÀpšÌl+ÜEÉÆÝxú›m¼{öð«³Ù²§)#¶‹/>wî`ç~ñ¹ã›8÷ö‡°eäråÝÏ3²£G/8·6”“j40lÊ5\wΨ#þm;æ}ÄÕ3§ÄoÂl©[Ïww]uæïÙG0ÝËËJxêñ—)M<ž}3£ÞŸÜ$‡Œ¬lœvËaËæïžeâM_òÞWoÓÜ©ÚÅ‘çOoéÜüÝxﳯrìq¤-äÙïÒoØ0rŠ—ñÔ3/Pˆ®£‡Býá.¼  …Ѓ!B@Ðçeß(‰PUQUïûÊ ¶³hþv•í;it2h𠾟óÎ/dÈðLš:z´à£Yϳ`kqØáñà÷í3#T²vý6B5go•— ^s‡Ÿë×±»ÜS7âcóšÕ¬Û°ýA¿¯ÇóG9׬Ã]uøã¾bo+V­fo…/¬½¡ºC<`ÇÆõ¬Z³_ÝÀëñîë8lYϦ…u þA8"î=¬Z½!òIȇmYµržÐלîæ}ô#ŽCüֲ¬\½–r_Møö‹yoV­bÖüº_ âóú#ÇÖ®¦ ¤²ø\Ó@|úÙ±qÝAâ3ŒV´‘¿]Ñð‡x*X¶xËWoÞ/œ¡ºá\·š‚’ƒgšÊâBÖ¬\ÉŽ¢ƒYVš²pî2~[×p†+ÛU]Žv—5:Mš¦!ÝJ}maÉÖ Ìÿe!Å•Uxª*©ª“f5áôô·árä­r´vînO€,Ħþoy ëV¬dóÎÂý¬iA|Þ@¤.X·j%UúÁ o^Udðüœ¯¸x@;Ê+â/ðMSMV7>sò8*ÍÀÇïÎâ¤SÎ ô2×öù|òÕç´hÙ¼®8ܰžUk6â©Õºèá:©ò-nþö?¼ûÙW ®UÏ÷6Œœâßxâé¨A‚¡:õ¼„Bƒ:z0„¾}uDUEz´üYT_þL€l„Ƨ»¿r/kV®bëöºÂõ¢¾ÄIƒz£%%…ÃYYuàй@ Ê‹Çã¡ùð+Y·j=ǶvÆŸµ)Ú±…Õk×ã­~‰ß·¯H¶ùøuÁçЀà xÊY¸`>·í!àñPQY©†ú‡Ðë*/—?3oœ7@\G‘Â×/kr_ÙùçHRîpùþ«7¥mb²äääH¯‹ˆÈË_+­ss$9µ©Ìzé{ylj/ižÓF.¸á"éÛ6EÒ““Å‘ÞI^ù|Á¾‹åúÓ†ŠÉd•„ì&b3"#Ι.åú¾[N?*G&ýã[Ù½j‘”í\+ƒÓ“åÍõn)“Û6—ÌÌl¹ñž™òòÝ׈Ýá@ÒúŸ+""Ï͸Hš¤eHŸÁçÊï¾*½[:D {XÉ{ëÊDDä§÷ž¼D£$$'Ij‚KLYäÕ¯~‰„áÎ †K“´¦2lì?åã_v v WQ-di¡/rß¶_¾”‰§'g^pƒl/õÄë¹ó’±ˆf4‰Ód”ì¹Ò÷¢»Ä_}Ç7ÏÏ@’Äf±Hrë2oy±ˆˆTä/’–ÍšIë£:˰^]$Áa@θø¹Èîºd˜´ÈÉ•QG/Y-ÒÅ’ÞëY²;¹§ð×ïdp÷±;$!!E0˜å†ûß _ôæË W],CÛæˆ¦Ùä¤ gËYSΉ“ϓջãøÞ`‘\{âшÁh“)UrZ´” o{0rË'ß"飤&$ŠË錣Ê— vˆˆÈºw¬ÌæÒeä‰Ò£G;±š‚Ù.3ßý©ø4Ió¹ÒçâƒÅ§ë€øtoùY®¼ø|é‘›!˜šÈ˜)çËäIdÊyÓ¥¨Ö§~þêí’nÐ$5-I2Òí’Ùéhùê×YûîÒ,3{¿p:ä¡Úá¬*’:f‰Éj§Í&fÍ"“¯Ú—fÿ}ûy™6ö$IÑ4iÞa€œwñy2~üxyð•÷ö+GLjÉX]ŽLÚå(V6|?[xæ íŸd¾¹lìHIjž+»u’D«I\‰Ir׬‘pž3öÄH8Ï¿øüp8_}ï åhÎ~åhI¯ÖÛBòä3É3sW5¢öª”ë†wL6q¥ÛÄb2Éqÿ!ÕW?{èVÉÎj!#N&½Û·‹Q£-W^ÿq}­×—Ê?Î=ALöDÉHo"CO™(ý:´’3/}:îÐl¬'>Cë?‘ÌV9È…ýWDD–?v¶´zJäž…ß½+½³%1!U“Ä`j.3gý ""¾Â%’¥¼‹ˆü÷•Û%] çϦévÉì4H¾úµ(\ÏŸß_\‡‡Ëý¬}õ|þÿMgf?Y±j…ôÈJæ99Ò²s)‘_^zHڶΕôätyî•ååûÏ‘æY¹rÎÕ×Ëž­%--E,®¦r÷¾<°ž¯ÉŸÆúògMº¯lDºëòÜ­ç‹Ñ`’¤„qÙmÒ¾ÿH™¿¶"òì©§•þ§œ%Çôh#™M3Äš˜#wÍúj_Ý·ô]ÉMÏôô&Ò<'G:´ë ßõH·|þʾò^Ÿ_W—÷p#µTÎÚC@£Ù"Æ”i‘›+w?5WDDf^¹œ0 ³hš&#N9M¦=YN?ã,ùﺒ}iöê¿$+5Ezté"­³RÄbqH—³o> )±P^^.zÑùò¥§Kë~ç„+¾GÏ‹£«T‰ÈÆ·¯—þCN•²â|yù¢ã‹_øNDDÊv/– WÇÓ¥pk‰l_>WºV‹»)7< Ÿ½7[ÆvÍ@Þø~—ˆˆ\>¬³àj)_.Ý,""ëyOÒ@zO¼+RÀÎÑQZt)×^{‰8@^Y°Q 6m”Š@XX,ÿ£œ:¢£’ìj#ϼó™¼ü¯ëåüëî W [×ÈÿÝweuãf’³n¼W>Ÿ3[¦?KdÛ÷o Æ^, –-“e‹¿‘©3 ‡sóêÅòÈcÂÏ0%Éu÷¾$sÞV&Ž»XÖíÙ'خӳú=È=_mŒ«Â|ì‚c n~Z-Z(/ý«:Ì9ÇFâbñ{¯ÊõWß"ó—/“uË‘ŽIHÓçŠ."!o¹|úò¿K—SeÞ/ å…k& ³—‡™Mk‘~MÃá{úý/å×oÞ;ÈÀóÂb/´g¹t18úɇ_-–eË“»®KI1EDD~˜uƒrÒEÿß–-“Åß~*#R5\Y°Ó#UE[åáóN@Ž¿èŸòëÂ…rvÏT<ÙUûâó©†ãóš[dþòå²nEÝø,_ÿŒ:TÚ6M£-UŽ9f¨sÌ0uü%²³2üûŸg‡ãûšG^«å;äâ-:Èš"ŸTm•GÎ;þ áÌ÷V·[ºê yüõdùÊU2ûîK7—…Ólö“wÉ1È $#»« :T† &Ó}©ú~¹|xgÁ•{Ðr½mš´ê[«žwv“*Ù0ûzé7èd©¬¨™&€œ|Å«_Y±tO@Hè(ò‹dçÖ•2´]¸N9õ&™óéûrÑÉ]Âåû½pçêÊcÂùsnMþ\pøóç7 ø[ž–eË—ÉwŸ½%™F„Ô¾²µº?sÃøvȘó§Ëwß)WL w|¯3,½îùòÓÿÊœÿ+?ýðµôjiÌ¡gì+‰ÏKº‡Ëûæ ‘*9á(‡€Ef¼ö,\ø«\;u¨rÂù/ˆˆÈõcFHŸ®y¢a’žý‡ÉÐcŽ‘aCGÊœUá4“â5bxɳáã’Mrzç¢u?UJ?)þ®¢ÓçõJ…;Ü*~ûïIbqu“í ùŽgwÄ¢ÐÅŽtŸr‹ˆˆ}û˜òõº`ÄÒ4¢©Y޽é­:âjp3³´8ù U‰$·û±òÈCÊý3î“™=&mH–üjsÔ%'u“Ô¬Ž2dH¸ð¾¹Ú}@xo9ˆRÛÊÚâƒgålƒÜøì‚®Ó7Eh2耆ë(;ÒuÌ‘3›ÞºIÀ*}½©Þx[:çÿä”ãFÊè SeeQE¹b¸@޽àÅ:§ÿsÇ52æâ{¤ÆT¹a éóG²ñý;¥CÿQ""òèy}c¶ìÚïw#ZÛÄÞcRµ@™·_yß&™rõ‡ëªóg^8Þ_+Ž çÏËßZ)¢‡êÖóήµêù}•é¨ÖVÉíu~ø`çÈÌo¶D®OØ\ºtsp^Ð/Mè8QÊ«öJêAòg›Ã™?=ùÒ¤ÏÄûëöoWÏ@®{;lÅÓ3CºŸv[{.ØTH «­/ÓU²9£áò^ŸÿüÖ+²ë3äßsënž:^þùð‡û7–¿*.c¶´¹ªØ%ýR blwŒÜõÀòðÏÊ“=%ß­Ù¥Ô“¢Ñ¢óO?‘Èbµb±V; Ñ@ f\iéÕw9xé«ésŽüpÇ6,Ï6Þyî'ºŒDû&ái,Û6ï"ïèQ´K­ÇGuûvŒÍ¹þ¾\ÛY¶—´ž=÷;k¥Wž/J6DÎlÛ¾ kB.–Wo¼u;i 4¥1hTÝî_çôäÛg2¹ÆWjÛ/toÓ­{qûu—Ð1+=ó>§<¤íû–Â-94­ñ..Ç¡ÙH°Õ8Ç(3&’Ö$cß«ÍN\ÎÄð7®ÚäÒc?—ªãzvçÑo6R8€í»Ëƒ" #ÎOõ•—0`@Ý8¿aæK5^˜ì.„žã†ìçHÚ…Îسn{Øgjw1ÙÙ„ª¤KlÆìf@Ü1Ä纷é¿_|έŸ»K+ÐŒ.B€q¿oq—ïÅÅo?|Áîüb‚h$fårÓ·0²G³°oìîb2²² –H8S±VO,yåÊqL{ü=N½ä*¦|óÊ´|ßüx ó;C›“¹æ˜ÜHRXê¢×ñcë„ó”Áxþ‰=ì,ÜÛÍF>þäSÊKÝLfú»œsWþôî¡8ièÈ:§úã6nÛ ´¡Êß„½N®sÏiƒðÔ¼•¸9ÐŲÒÀdÚ×\WºK,ïÙ¹ÜtÃ-ßÊÈÞ kÜ·UçÜ÷ÊÛuÛ£{0jvCjÚþó™|¾à;®›~7ÏÏ|„`°‚]{Êé3n þߣ4³+÷DÅßЧ³NüÙ ÄšÔ­¯ß,Up$ ¤Ë¯kßµG†¥"y}/ˆô"Ë×}-© ý/yJô`•¸@†ßôÆ~n=nY¶|}d(abç9é‚çkݰ]’Aùnþé裤õÉ×û {}NM-¤à ×þ}·ü¸iŸOYéÂÙÈè[÷…mÅ3ç‹5©gƒq5ÿÝg¥mÛvÒ¶×qòíβ8ÜÐöHW’>àÌ:ñüÑ ÿ’Ëî [W6}x‹²¤Ö Ó¶cï3#ÇÂï$=¹KÄJ,²KZmrïü=‘{NØM¦Üûó¾¸;¥£´:ñ¢ð;Þ¹7ì0ë×ZI¼[z&"Öž§G|!çÞy’`n±¯§îÙ+¿þø½ìrû£~jUñrqôW󯽕Ϝ/¶(åèPÑÉ)†£Æî;Qø½d‚\ñü>ëëkÓFÉ cÏ©UÇ/¶¤naëXá<dÔuûü7wþøš˜@.¸ã£ÃΟ•ÔV##qwϸîÈQgÜŽï‡Ï@^ý®p_ùÞµPš‚t=ýÎêÌòý~å½ \Þ†kÜF5?xþ\¿Rv”UÆQÏ‹Lš-&œš<÷KQkgh"É­Ê®êGö¬—n.Ä9êjñûK%døMo?W¬Ø(þÛUrb*bl5¼ŽÅò“‡Ã.5Ïü®ÿ.=&SR[ ”âšH¯Ü.}R{ŸIºžz\GɹoÄåú‘Í…„ƒÇg~¥_ŠW‡-'ßþrm;“̼ýFyüÿ>Û×Ý-(+"2À#Kü(›KC"{×ËØ!§È—[j¥Í¬kÃniËö*³âï9¼."R¶ú9{ÊÒ)!ìÏtôÈ“äþß?à¾üÏŸ@úžýÄ~WJeZ§’ݶ«Œ:&<ÓÂâ̳éKIIËë'W_q´ÏLø©^tó›R¹§X¦×A9æÌJ°Ò/Ï_ö{´·;QÖ–ºåçY·…c;Jn÷Ê®…ï‹¡:¼½ÿˆåæSÂþ–ÇN§\ú”ø«3È¢ÿ»/ü»1“åÜÓO 給}ÂC±;I»${3¹ê¦›ä†KΔæv\òÆê=²wá×Ò%u_8½~·œÖ%Qzæí‘GÏû•M˜r¡Œ5(’Òº •¯¿ÝZ#uåÆáyV™zùµ2ª[^ØOìúð¤‘fT—££O‘›nº)RŽš´+îÃÓªË÷…‡©“Z ‘é·L—Û®¹H4—̬åŽP;œ×DÂyêô—EBÅrãygKÛZåhÜèSä®gß>¬õÖ‹·O »œ~–œuêˆHþNlÚC¾Z²Q¶/ûV2«Ï=óåo¬Ø"½ÃÇ㯋…go<%,»(“'‰¤ I-䳯¶†áàÝrN·pÝ8pøyÿ» "%ÄBËÂñ*”qZ ϽVn¸vš¤‚Xšw•;*Û*ㇴ—÷Iû•÷¶Ãå§|”oøù€ü™]?Ÿ[°k_=?ù é”X«žéÀz¾|ó7ˆiàûÎì-€¤ät”Q#‡‰W޼¿2_DDÞ‰Ôó£«óg_$ã0æÏ?¿,é æ&ä†n³O"€t»ä^ùðÙëÄYŽŽ¬ö2qÒXI3 ØråƒÕ;úÌi'v–#&ï3^¬û©ÞòþÒÒp›þìM“ÃsŽ-×_{‰´¬®§þùø×û\Œ¶ý$M@RrÉÕWœ/9I‡¼±Þ/ÚUß49犛å·Ý&½Ú¤‰1«§lÙ«¼:ÆéÓ§ßaµZÿÔÖXoñV^}óSÒ»öç˜A}ñ”“Ѫ;Ãúvªs_áÊÏxüͼóñÛ4Oª=$XÎsO½È؇ÞçœÎI¬Ø¼‹!§]ÄGïÏ¢uJø¾£†œÀˆ®-Y<ï;–,_XS¹ôú;yá‰I0‡xû¥×i=àºwÌbWA•ÞJÄ–ÂðQ“¹hâ(¤hÏ~ð-ý£o·ì"¨é2ø$º¶N©âó·_ÆÔ™c†D*Šq{}$å¶cô>-1K.;Sùf~øîÖº3íJ>y÷Yš˜o1ï¼: g— Ø÷ž< 9m{qÌ~qÑꨣ¨ò ½N˜ÂÍg‡Ý ÅßMŽêÏ”Q}Xµr)‹—¬À‘ÝŠ{y™Ço< SJ+ÆlÃo+±ríNFM¹‰§î¿ ñûñ™3=¼ ?|9®¢9Ò9ý„Á,úéK2»ô#Ïiçøñ§°yјš´¦kk']=ä]KØ%©ôëÞ‘¤äÎ êÝ–‘“.¥½~üé–¯^O«cÎ`Ö'3*oßp›+§ÇwoÎÊEóXWXÉ ±ðôKO10/•X¾¸}ÿQœÐ¯ +–þÌâekhž×G_xé焇¾¬i9\zÁéTíø¯XHQyiWÎàW¦cJ7ÿÆÏÅ÷ëNbf Fôj÷߭ çѱXœœ—Åó¾ çOk—^'ÏÆü™Ò¼;É®KønÑö\v÷cÌúÇù|ÿÅGX²ºrÃÍwÓ£i9 —n¥õ€±¼6g#[%ô™Ÿ½2“%Þdn:÷ li9\<õ´ƒ–÷“»†ÝÎz<£Û¸X²pËWo#¯ç©¼ðú§L³/>-I9œ6¢«W/fí†Bº =•ÇŸ}…“»¤£ùý$–VѼ[;òW,å·åkÉèt"o}øÒMj˜Xÿ2b~?Zyy¹$$$ü¥?$  Uî¡{VÆ¡·ñÛ§wÕ]òLßDÏÜŽŒzj-œÒR¥¼B¡PüÅ`^ŸMø1é8Š—|Tçº.~†tëH“±Ïòþ#ÿÚô±ys>I™éìX:‡ACΤ×Ù÷óí«7ªŒ øËâv»ÿúÛ`Vì\DûYdæ´del™÷¼´pgÂ{Íð“YºÃÏC»‘Þ¤ ¼¸T¥¾B¡PüeZ«íŒi›CfVs>Úì§|Ã÷Ìü¿eun™qÉÉü´|Ü;†ôô&ÜrÿÝv­p}»u EN݆œIF¿Sxå_ש| øËó—·t†<%|ñÅwTb$3=§ŠÜCi—Ví2 °ä‹/)±¹_){+ýtë2œvíRTê+ Å_ ‡ïÿ;—Bˆ¦Í2TzÈiÑ›ví’#·¬úõkòÝŒz»K½tèÜ—®šÿ5Ûµ ‡Å¿üJþeµæè>UPüõûŽn÷ßcx]¡P( …Bñç …B¡P(Š# …B¡P( %: …B¡P(Jt* …B¡P(Jt* …B¡P(”èT( …B¡P(Ñ©P( …B¡P(Ñ©P( …B¡P¢S¡P( …B¡P¢S¡P( …B¡D§B¡P( …B‰N…B¡P( …B‰N…B¡P( … …B¡P( %: …B¡P( %: …B¡P(Jt* …B¡P(Jt* …B¡P(”èT( …B¡Pü=0ý^/òC<7w!~¯Ìû½Vjý­Õ:§äúÁÐjÝ£ä·û_'ʱÔ󎃅¥¡wÇC}Ï­/¼4"ÜZ”÷ÖgÄðîh神8#†tÓê çþïÔ¢Äm´´ª/ÜZ ïh(]µâ”z®×—gµ(ßÝÐ=cY!Ê=ÄŽXÊJCåò ÄPFc QÒ%–2%Q¢„©¡ïV®Ê{Ñâ8Ë äo©§\4ô-Ñî%޼"1^—Û$¢|71ÖÑÚ!„-ÞòkÐ!?c|G«Ã‰±‹çÑÒU‹Ò&H e.ž<kÙlLZs.Äb³rá¨ÞXLÆ¿—èôøƒ\ñàëPX öØІJs, òI´ðH#ÔáBñ 6í¿µ±áŽ&¨âùöÃYÑÇ*ÞÎxÒBbhüc©˜iDî|ÔØ¼K B»±Ï‰çy‡’?cíT4öüá Oc®J¹µžEØJÙŒµSÛPÀ‘©«cÍw±œÖ)?”¸Šç|¬ùäP4ÀáJ“ÆÖÃñvÒUkìYn4Mæ¬cºÿýD§AÓHÈj‚Ûj§-znL8–Þ1Z¸ˆáݱf†X«5F‹Q(Ç÷ÆbMŠÕÚKÃ….š… Vkšƒe@‹ã½ñ6xñX ´Cl~-Æ|Ò˜´ø=EP¨Åð®X+êú,²Ò@ÙТ<«>)1Zdc-£±¤C,‡h–¡úD\}u\¬å!šÐ§CKÇSkÀRØP¼G«Kâ™ìÖP~¬¯¼Æ2´¯ÅQ†êû½Fl£Ѿ+–N—ÅZŸU²!Ã@,£`õu´céÀDëˆÆR·Çbì"Î[,F›úÂÍõA‹Ñrú;Ï…ùcDg4 ÄQñÅbRu(!VŸEâ°8H=ÂAkàw D‰A4kÄ?¬‹0Ô¢TœZ=½êhÃ?±Æw¬B++_4+[´^g<–©††Ø¢ùÎÆ2üÏ,h‰±|Cç©!KèÁ†¢cå‹VyÖ†x‡Û‰!îêi1tòâé8ÆÛùkL'6ÚèPCÔh~Ú±4†¥]넆 4 ”çñ KÆÚAˆÕÍŒ8ëÈX-‰óéÔ±<Ö—ÞÑÒZê1Xh1ÔIZŒajHp5TïF«û£¹MÄ"Þ%+j¬2 m4Ñÿ·ç}±ø×EëÕ5ä÷"õ4±XG%Æ ­ò‰'žâµÊE³¾ ŸIëW}‚¼>AÖP£«x&JekEÍúÍ?-–FDâo´x•¨Ö€Õ”ÄT‹O¨ªþçñ·úÿš^ôõÿbµh1Xkßë`LMgÔ¨>´ibo°q‡†2B0ˆÁbaÐîtn•Uþ†‡§"Ï [ÈêÚŽfY„â+Ìõ-áQ_!opò“@H?¸•%žuc±²Dó›jÌŽ*µßÒ!$ñÍkÈ"ü|cv32{·ÀbÔÁÄàL Iß6¤gXÁ·_Z†B\®ƒ_Å*rÐNDušÕœ…Àb'§;ÌÀjÜÎ*±ÌB%!ϲH±XTrG‘8ÓQ‹ã\t]¯åA]ÂùÇ믣@‘—NñŸç2ix+(-ÑW‰J‘Ë_¬å»¡õfcieË9‰bŒeRcJ¯o Hˆßå'a-¾S˜€IDAT$Š0¬oX?£E´çÅ*^Êg]k¹±“bë‡ß Ö2ÖPç«¡bZ€<ÚÄÜh¢/Öõ}êPDzþvCõU¬mQ¼Ë°C8Ö]ì¢Åacwj;Bü~‰4ºö”£¹:³ú™ói›!|)ºÁ &-úN´‰uÒRÇ`¶’é²AQ€. p&8H!è—}®‘ßk8]RDGy4ñÝPüƒ`0Òþ´cèŸmå£Ù?³·"íë62WÎ7ˆ¯~ÝÊέ^H±Æ·­\<“ç¢ ãÅb ‹¶`{,ÖŽhÃŒñ,ËÄYäùP&:4ô{b¨â]±,It°ã†Âz0‘Íí'šE(Av°IsѬƱֽ±Äo´ÉDõY=%Òˆm)VMCe*škF´õK£Õ1ñlïªÒ€@µÃËd°XüÖãé¤ÅÒý-EgT‘`-@UȇÇ•¾©‰\aGIY%V»•ʪ*6ûýPY åa+¦Ar"$Z`wYxHšpãM‚’ìáûj^æ÷…-XPø·$»À„‚áç–ûÂV»D$¹ÂÏÒÀãó ”WTÀÎÝðÓI0@'üœ2wµ…ª:Å]Îð=šìó™ ÂáðÂá°X@=V>]Â>ž ßç A‚\¶}Ã×PZ‰k@&ÝŽù?}ËìÏ~GRØïϨ¾WÀl—5l-«ô‡ãÄd‚D;˜ ûâJoØÅA‹ìÕþ¥Õ÷ƒàö€¿:­æð3\vŽ=šKmn>zá+(õ…;N{ø›EöY ƒ¡pZùÕßb‡Ån_«ô†¯ à°†¿»¦4Ðu¨ªu±ú[¬&ü~pW[|M¦pü›±‡·úÝ5a£1ü« ¼^ŒM3¹pÂP<¿}Íû¯¹A€Ý¾gÿR¯ia_Ü OµkCuz&ÚÁ CyUõ0¸€ÁŽ» Ð=D¥×)¨£U Ì€×O…?„^¸ øª¯×¼Û¸«Â."ávÚÀi©>ÖÂyÛí ‡Ó`§«©nš}øÒ7Pâ—…ò…¿ñð;-™}Å0&ìÄ¿Ÿù tkÝñ‘Xv=‰¥¡E°D[©(}I5ÖzPQOF«WþÖ¢³!'c‡ åÓó²û0tü›*¸èªi|óÆ>ò)¦VM ø½àÕh~T'ŽéœEŠÝ„ßïcùª ÌÛPBËî]9¾}6“‰¿Š/®bíÚÝÐ,%üÒݥȈS†Ð£Y2—Õ›¶ðËæB*Ü~(ó“׫3'õjA²ÉÀ‚Ek˜»` $;Ãb¡š¬œ\Æ\˜C»,¿­YÇ?¯{$˜¡*Dó6mÕ-‡d£M÷óù²µ¬\º2Ò±^Z~3õæø¶MÑCÖo/bîW ©ªÎÕ´ìª"ó¨vôm›Jyá¾]±5,(j|ïÌú êL[C¯|µ *àÐ0:’Iî’NëDV“¢ݬ[±RÓÈëÓ”4›÷Þ½¬]²ÌVpšÃVÓ´hF×.9dX4¶ä±añ0Z¾ª ‚šöíH§Ì$Bþ›v±kCÁ`ˆ²Â½$ùHnߌª*0š  JkMzÑDÔ—EËœTšYŒ¸}^6l*¢b×Þ°ÀnׂÞš‘f†•«·²cå®pÀf ûú–{¡usºuÌ&Å SPüÿìwœU÷ÿß3é»ÉöBïéHï"Š ;¨ U±wÅ^PQQì]À†bCTTš"Ò¥÷ÎÒ¶·ì¦ßß“ÝM²“d‚>Ï×_Ž/_@2™¹sï¹ç~îçœ{N ‡åRYé‚b;$§ÓzH{§šÉ/(dãê=P©‡d3¸º”dRgÒ,ÉŒY/QXTÊö-‡ Ìzx\œ8QˆÛ!akÕ§!“pRq²oðÅÿg©„žÌ®miS/ ¯ËÅÑ“yä-Äé”HlÛŒ6õSHÐË”UÚÙ´i?”T*@=œ•ü«LX°!@)|àX›4¢UÓ4lzN—“mÛP~Ò6e̼!cv2—{•1+Î-æd’“”Vu¨°ƒÎ(ãÚw”“k6±ðâÎôïÙ†ÜÌ©B$èc[P£ÌH€&š'CÒ°@žNÎÛ¿%itÏŽñ×rrb‹ý«ÌQ¬Œ‹ˆ2– -]˜Ö÷ ÇjI4) A´Ô?±¤ÒrÈ4\6-ï Ú«qiaå´°…‚è© cqÄ0áÚË|×b N7­”–êNZ€u8Æ V­^•ÿóLgØ b…ó•WâÂeN*]Ê‚,;݈²JÜ¥¥pÌÁÕ×aö䞘j0 ›G—G׳ñ… ¤>ïš!Üüø¼ùçÀMrë¶|sß(5M jÈÑ÷òkA÷<0‘çGt«ùêê³™ýåwL|óðfàõ¸xt쥿?—¥K—sñsó©(pBR2‹^½‘öŒÐ *xö•÷yà»}`*Òyã±1ÜtVã î]~ŒßsË«± ûOÐdìxÖM»˜ #<òÄ[üfw) ¶4#‹žmêpxßa6îȃ@N­Ç”[ϦMãdê¤$¢“dÊN`é†#d7iH÷võ0âÃi/åÇù¿óΧàq%. îÅícûqFãtô€£¢ŒU?¯æõYË))(…ÄT.™xל׎‹6w ßû¿n«¤²Ò¨“Ì´GÇ %$bu2õÁ™¬Ûå€d#/ z]>„ —·#-ÉJ²ÉH2Ͼõsß:L“.ä¡+{Ò4ÆAqa>s>^¼·ƒd† M.ÄÝ#{Òºn à# ïð%o<¿Ó€žÜuÓ0µ¯ƒ,ë<•¬üå^¹ŒÂ“ÅèZµeÊMgÓ¦IuS‘$ o¥¶óÊ;?qòê@yiõ›´äÝé0%%R|p7O™Ke…¬0•’%v°¦3òº³¹¼KÒŒ€Q~’[ny¢/<>ž3R  Ðy]ìÞ²‡ß\¡ǡN˜Y/ü$ÓSeaH«Ë]ÃE )ðù0áåèÞƒÌxc[¶æ@R—L?fÎ "Û?f–lÎbžxàÖýqœkŽpÙ% éÐ:›S?õ3´¨Ço ÆZkþÑh Jëýµ²SÑ®1,È’÷w$·¥–äúÚó N?ªV×&hËsª%¯® 5;v'æ>ÃíºpdL4Æ0ÜÁ­¿¢1‹ZÊ ‡k1Z¥–¹¡¨E«7/iØI1Ú7-¶#V«%¯°–yë!Ð è «(þÞN0)ÍJ³ŸÒ Ȳ˜@ò‘U'“ÖlÙÁü•ûÈlͯë@ºÌ?¬§"ÿ»3`p&õnÂë^ÊÌ‘Óp‰zÌyr2ƒ2t”=È3ß­Ç\¿1mÓ,?PF¯‘£Àé©àö7çSIC^¸q®¸ˆUÛw3ë³cÕîÏS'sxûÇ 8u™<<¶gŸ=€÷ O1òÅÅ’Îû‹þ Ë^Ì–}ù´íÕ‡·ãþ[G3kÅTöpsßsã¹é¬úàµóèÇ¿Pá³Òµekå“ÕÏÀÁ#'IîÕƒ?^¸˜ à홳xjá6h˜Q£A¦6ui™Äþu›È+w€t:3ëf"ØÏŒ÷·â‘­ô=» W éÀŸ;ðîŒ?p[lôÚ1#û±óp¿|¹ãà>Ü{çt ˆÙïÃþ#Nz]Ô—‘žƒÇåá¹Ç`¹ºc.ëÎÁe+˜¶hRz­ÙØWà†=F£ÛÁ‚¯ÿÄã•0É.Ž• 0ùc;à•ÈHK§}ºy߬`ÃÞê4°²~Õaê\0„é7 ƒý{yòÍyØ¥tÆŽÄ]ןËáœ|Ö/9D½K‡0í¶sHÏ?ÉGï|þB7-Ú×cËö#д×ßu1#šÈÌüôÖm.àÌ û1qØPʊʘñÒ"&×Ë@>¹Ÿ—>ÜŽÃa ãY¹¬_/ÊOñôŒïñxÁh¶`Ï?ÌO?Â`²R^RˆKè”ØNPB¼F_{wÓžõ›¶ðöâmÈÖT²2ô²K ¯`Á‚å|sê'òtêß çwaÒ¥¹<üôAÜ8!¤(ÆÑ™ÇÏ®ÂÁ•÷½Î— ·+íHL‡ƒË’•R{vfñ¸öÔÞýl67¾» ê×WÜ®>x"Y°ê)Îõdz𠝝8uü8‹?_‚M’‰ží‡±gýv¾™¹|Vzèsk_§§ÏDÿ³;ÓÝæå½Y?ñÑÛ+À%óû‘b¬b耎|ÞrE# è8v¼‹7@™— éÉeK“ÉUÎÜ9?A‘,‘¤¸e}51n—|n–-[φŸ‚ÕÉ\qGwš;ó¹iæ¬ùn'¸ œHLdÖ]Cé×®!ë?Ιgw¢µ(ã©Ù?0ÿóÍ`5³z± ­®½-Óùö«ï™ùê(u±ù”›ö-ëÑ­kKŒÙ+qÙ]ø„ĉãÇùñ³_¡LðËÞrZ5ªK‹¶õ Ý‚py1'Z(;x’¯f.aV6?é60úCÊ+1tìÌåZrxïv¦¾8S;K•¸T“Ò­PqŠŸß¥Ä;+ÙrÈA·.M©ß$Ò”<˜§ãª³ *KYÿÊ<Ö'$.¶l˧iǦœ•Ž)!Kª 2Çò§Ú˜È®ræ~R5ffHO†‰#•åœôèi˜j³¼ÔÄuJÜ\±$ù—ЖbEŠáz­,I,®ðXªÃü•ƒ!±²7DðZsá†cù´ôk¬yBù#1RÑØp‰Ø“· ºŒ0¡•iŒ–¯5–´VZb…F6›‰¦'ÑúTËü6ÎZòKQÀ¢Ð¨‡ZbŠÿJ™äXØO4Î4~ÿ¯d:çêÎþ˜¶ð=›™šMê@ƒT°WB©™kœÌmúҭn"eö2ô: ²œÊ =²ë°â§Í:\ mƒÏ e¥H‰Y4³*w¿çÚ‘ÜwÝD„»ƒMù°Y=$xqûO ]4ªI>¶,ÛÌÞÛ΢¥%ƒö‰ýå.¿a,·Ÿ&ýš¥RQéÀa¥ú¶™X€ò£{ùòÏÃТ˜eåð̉ <>„  ¸ù¿þn“_ ­[+ÙÐ/>!QetF €?Ý¢ÏçCg4"×IÃgtPpªˆcNAfz4J»`·½€£€Îé†T3Iõ“qçdÅÚ}” )28Æòõ9\0¢!-Ú¥óÓ;YqÙ™\qÕ94mß’ŸVlå×;)=V–D„ÿTVV ¹f7˜ þ‰c'ðø )Ù Ù©`’’i`‘Ùwª’s.Ìðˆ#ܼ‰ xH¶Ij¥y¦ýûóÛÚ#P7¬F%µš&%Qi/ÀÖ°)ϸ}¢o…— ½³%«ÎH!>|‡Î`DÎN× äq´ ŒNF36Ù@>|>/z£YéCÉìÁ5:ëò «—DJJ.ÞÅ©Ã%Ð0K9ü…€ò ÀF¯›2°gsš¤˜©(,Çâ´S\áQ6±V¾ ̇ PY z+íǞϹýZÒ*ÓŠ³¬ ·ÞÅ©2–=E÷²òhw.¿êš´oÁO+·ñëò€1þ1Ë 3IFØ}H.]—T4V$V@¦õ$v,±’Æïµ²‚Z€S4$–ê9Z©–ZØÑ€®–ºé±€"ˆ^1ÒÁ–h§ÓÿÌZ,(0Òz\+øÖ”b-±„³DÊÂ"þ‚^iùb/‰†ñ ú#eXg­Þ“paZUia§…Fû'¦­ù?:£åûR,~7ÊiÜJ˜|÷8Þ>¿99Çr©Ô[hžàÅ# ½FÊ*½ ëP+IJµ¯ ƒìÊ^3‰f:K2•vGínÖï+Lèý Qï:¸œ”û”çªpÒuÔH¾¼¹§r (ðÈ4®«œxv{z£ò"žJ—âr®fxuU¡­AÒ¡mGÚŸ¹žíGKÀœ\sȾ ²×‡Ûëg¡”CÑÝ*@'cBÆèõá©YFö¿‡ð)앤‡ŠJ/Â铟r–tàô $0$%ÃîͼüÔ\\Ü“¡ýÚð`Ç6Œ8k'ÏNÿŽ=9þtByN„Ô"t %@è°ùÀáñ"Izd£„lL@çt±aó¶ä”€d!U–°»¼8}²?‘,õ’NBò¸ñÈ ³ÐëeôV‰Ý‡NPQ\DEõ&)XÅ,:t: ’¬4H>ŸW9¬£jð%„Þ‡»Â߉U, × ='œÇ“WžAþñã¬Üs ŸÅBG³.hó BúG~*Âíö%eÓT)è<î,¦Mè§ Ÿß·§\o Sª>Œ)VØ} hÌèØ†CvñÌôïØ{L3ŸO9ÈôLŸ·EÂm1Š‹Œ òÁ )†F"z½jqš;üX\IZ‹D¬±¸Ïµ”­Õª´~‰8¢\Žô<­ Ì£¼HÌœ–öÅ2öí)z"ÝOkñ‹hŒh4øhKá‰åÓ¯­5-—–êZÚÓøÅšE!Öñ: NükÝÇrÀ.ý¥zÜ¿t ˆ;¤“…´-N7d§qëùÍWÝ:Ï~Ù ê³ê›iô¶$`”í—KèÒµ>èíPP&¹e[ v“ró{Ÿ|‰Oÿ8ª¸íp åLJ zåá•N§òÝÑ2¤Þ=ig|¥ìÐ%1íÂ^<øì[<óþ*È®Ãk<Í-Í, 2Ž<%< ©^Ò„Dan d&@i%”»0è•RŸÌþ¼ôfÜ1¢%ë^™L‡q3ØŸk‡¬¥J€^Â[æJ/¶+õàµÙÿikje?ð: Â‡®ÄMF‹êÖMaߎÃl³•º-ÒpWx¨¬TÒéØ×neî–}Ìm^ó/Èm—œÉÄK2å­•81¯ÒŸ’z)Á¢¸×½Be^ˆª£ú€‹<ƒÁVO¿7=Û‹ N¸ÜJz&³RÒ9æqÓ?3…æÙF¶n*€ºÉPQ Å>J}>’“ÓÙ»ñ'>~k%ÔÍPÒDYÌ “ЛôÁ§À©¡Ï'@¯Ã¨“©>|.eƒa÷)i´ôþS:À.Ý>6ÍRfTQ%$ÊPXõ[3¼3òíå†G?'o{.ÔmÊ£/§I‚A Œ^'ãõxpÙ YÐtH>ÝáðW L7%!éuH‡½Ø¶DÎèߊäÜãL~äS¶­?©Œxn“MÈ’ ö5þ1kQó/Èm—žÉ¤À1Càs8 ²jÌÌàÖQ/!‘ ƒ`k‰\þôLZsÆêVæBŒ<œÃi¡Òš"H F–%šÛV `;÷™8¾"÷p´>Ž%>/Üb¯5w«ÖöGZ«´ÆRj9¥•uŠ…%eìb©b¥¥E „Óµ#±äœŒg-FX+˜Öú ­l|,õÖµè‡Z{ÿ ‡Šþ;e0£-.á~$Â|îwÝ ¯òJ­²LJj›¾éÈL–ø}Ín@‹î½™uÏ¥d[t$$g3üâXŠŽóüŠ<@æ©{¯ w§†PjÇÔ ç í‚,Hòá[fYO›^ƒXòØù˜€=«¶â9RHºOéÖìºIž‰ èüéul¶X·_JA¶eóíãch×,‹dfÈÙ=iÑ&¼r§òZ‡¹ó¶—øäˆ KZ?z-x*jJô¸Op¤¨œ¦ ëaM0ƒÇ«¸„à*?¬’¤Ú$=”–°nÝAN™27j0ÏÈD'L´»b WwkÀŽ9l\s]Ç&´½´úzI°ëË×¥XHX€Œ\á#»n:{6À 3Рgê×O€JO’‹P0 ¯€[Ž“U‰ãS§e†’{2#ƒºg¶@oÖÁÉ|þÜ| KF=&Œ;‹†í²Ñ»|$·kAJÛÖ¬ÞÍ»ÄÈózÒóÂŽ€dÙg6'«¾*=~Œ[ÛÂT5E–dð8)ñºhÚ´1-ÚÖÇ`±Ñ¨gkþP€DîÍGøs>Ýûua̵0§èЛ,duiNb’L‰ÃƒYoÄj±‚Õ Fd¾Àœ§’¬‰4lŒìö¡s õ23Ðu 𬚱.Ñ‹‰ºYé ÓáqúÐéu¤ØŒ`SršÊÊ´Àe·C‹z´½Ì?f;°býQŠ}‰‰@òYz6À 7Ò WÖ3ƒOO«zÙ4ÅÎþüBeŸ*qz‰¸£ÌHå£U¡èU<´–$ ó©âG¸RsZƒhïí·ZjÊk½§¤Â8‡s7žn*¨plK$"R¥ ÓÉÕ©¥JVOĨg±2_Z©Hì6hKå)7¥ˆ ÷±T ã4ôZëA§Xãxµ´/Ú<–"l*"ůFbx¥ÓhS,ï®êÔ¿ŽéÔŠÒ £ãò‘ž” @£$³?‘»·´ä²l&ås“r xñ»­Ì¾¤Ož|ÓÁ§eJ9ž-ƒûkF?:“A³n¢y÷3YþXƒžX€/3M 0¹§X·?—sû5 sË4V.ͽ‘zõR¨,JPúÓëE2˜¨o1S˜dVܲ^ £ÙL#H·ZÀ*qhÑÌlœÍíwâ­çëq¸ÀE«VõpæãY¿Pº¿œ3ÇöáÕ‘=Ù¿oå^™ÌìTR¼E|ºö)eÇšÝî5„ÛoɈ++騼.¯Íþœgn„zIþö€Íf£™1D“ÞŸÀ]:'ëæ/cV“TÆ @û.í9žSBrýlRŒvn»å]v畳îëåÌjœÁØÞ½x«mKN/£q»æüùë¦Ü=ççÔã¹ëòÊÔñìÜwŸÎL—fÙ¼üÁ|´æ R3#õê¤PYXÕ?Š"¦d§ÒXçÆf2Qzä0?l<ÌÓ´ã¥GÇPì2ШŽÌC÷¿ÇÊMÅb³ß©£|8k9­8—Ç^ȰÁÝq{M´nšÀ½}À§ßþÉÀ;ñöô 9Q &=¤4½Á€'÷8¿çäqg޼þ¨™1×Ïæ û8vNKžº÷J$Ã<¾ùzdÛ”$÷elÝqˆcg·à釯¡ô‰¹,ÿj5—=v!OÈÁc…ød=õëeâ)>AIž“Vç÷`ö„þʘy$2뤑â+âÓ5GàH;Vïæp¯³¸ý–‘\|…ƒ-êòÒ›³ùt›“¶]qòd.[÷ô'›G{Õ- N´ª(j€”(®Ãh9DµœîÔÊâEc(µÆk†;Åît´E8v3ÖÔI±°NZYqé4Æ‘6- ½caµ0ùZ\𱲓Z©–´^Zz¤÷’FÆMkj¥p÷×ÂÆÇ‚KQtétØf4ÎñH›îÓ=p ¬þˆîxÜd2ýíry½¼¸àw\NÅ,EXD$@–q輜*Èåë {ØVdWªÞÈ2‡S¹üðç>–T(©”d/›Ömew¹ƒÉˆÍ '·°„_7ïeÞ¯kY¸ç¥&ÁŽ5[øù°Ÿ¬';ÝBQAß¬ÞÆ¢]9Ýw÷7¢´R`±˜I´è9|ø_þ¼šv!Ï'QTYJaE%z½ £QâhÎq>Y¼’I¯ÏcgŽšZ9¼}'¿ä9&i&=Å%e¬Û¶—OWmäûMGÉ3Y(Þµ›/·EèdR“q{+X¶f'Ÿþ´•ce**íÌß°‡Ín'sYz°œ‚’NÛùùÐ)²?U%¥ë<¸6w1¿ü¾Ÿ%{y)·dÿ±R¥ZN¢´¬Œ?7àp^…X… ¤¼„›pÂîá`ׯlË/Án4€ÞÚU[yíݟرýÔIC_éæ„£”§ÏÃùtίü´î0¤X(t’ì>[¶ìãxn¥R) LPpø$;óíØ…‹=Ûö°{Ù.Öœ,£Âí¡BÀ‰ÂVÿ¹•«v±÷x)‡—\W…²‡ð°{Ïa>ýäW~Z{Ò”1ûó˜2f¥^«þÜÎÒo¶"uéÈã£zðûO«YðÃN%´@Ž6"ÕµÇ`Ec…†0«wº.ÂX7Ä’ºF¨°‰’Æwa“XrŠ0Œ¦`k*+IûG#)ûÍe©å0™ÐØöhe<ÿJÕ—Hµ·µUÐêÞŽ¦»Bƒ^G«x£5FQDÙTJh„R vAësµl~µ°¯Ñ¼(ÑB¤ã­%ÆeŒL:¥¿‹üé¨L’òw³Ea½>%éyUU¦b7d&C‚A9É¢ø¶•þ*q(}hÖ+'´+(*¤T¯òz¡¸B¹ÖŒÒï–å]ªôÁ'Cf’Ò‡EeàòÕŒd1"Ün¥½&“?ý’?Ý”@)Gêt*÷7› Á¨´?¿T¹>Á¨<ßîP–ÕMVú¦Âç3Y9U8fØÁê³ãùÝ”{gŒãÒz&ß;‡-» !â-ºÖ’mávö§s"^k•¢XJžŽÛ6ÖIë³µ2ƒüž£%!¿†IÒ0.ÑÜãZuHëNÆ-­Žõcù^:ÍÏ£õc¤÷ÑÊ2j‰eÔZK¿kaáù 㯥L(1Ü'ÚÆIK{­s£ê^v¶Œ$޽w6³ñïÇeeÿƒ N/›~`§º=5‡/t:ªœW]'£Äѹü×0(Òåñ×"—•ï«w’² »½þƒ$²©Ö®Hí¨º¡Ï«< ”˜Fì?‰â#§Š°õêÉÜçFsró &?ô ^½¥ºÌbÔÅQ ×(€NyÁª£ÎU H¯ò¿ôþß ´çÊ«îÀ~Ö‡y·× G§Œc x}5c ×Õ‚‰Eç„ðolü÷»¨Ts­×«¸ñ%Iy)$!¼×ߟUï)„ÒÇ:r`ɪBù.à9^Ÿÿ¤½ÿýx<]Ž4f>(prÆUòá̓ùô³Ï™ñÖJÈLÁ‰Ëb ÐÒò\-èt€XJ>Æ À£õaÎhni- ?D¯¸óW¶VÝѪo±ˆ–Zˆ(àmˆ5À®Vp®%ß«–µXË;h­ªkøa®h½‡@®µ€ƒtú­ø‚›hw`KOâØ»÷a³ü3 óŸ=½.b ¨c¹o¨ WòUFbd½Â@U=3(_¢TË© @dYa£) > ªéfü ¶j‡QÏMÔ”,ÝFÙ¶ÝÜ7}.ÆÂ“ød³:¨w:Tmrë#õ£¬ÚH9Êtºš¨áH§R#Gƒ.zLU¤kt²2^ªãT÷»T³ñˆh …òŒzuãRÃ-HrHJ’òn¡í<¥Ó·I'+c£fœå0cÚF½ :ö­ÛÀ¯œ`ך ;.ûOÐkeÒ´$׺¨Çš`>ÒbÍÀ‡kO4ð©„¡–…9 ÔCîÙÑt6\½l) )Ï2캖Ӽ‘Æ%\}{¡bÍÍ­ì§`h©¿®e3%iйXñÅ ¸µ2¸„Ñ­X’äGÒo)‚ÎDÊÚ]ÒÑÒ¡Qç‰q>h%á"oÐB.n¼èPþ7Ê`j5Bƒq§É~eˆ%MÈé|-ÊIaô=ìøáÅ•lV°Dxv,ìA¤40ZwåZbn"½#/b˜p"ƒÍ F3¾‘Òåh ¸–Ö'Ö¾ ¼È$ã(Êgż£b…DCpQ‚Óhx—HFWŠaÜ´0“§“` lž–>Ñr2Z Ë«e¼µÎ1-‹Ö_9­ÅæF›WÑJHjq7ÆZÿ=Že—4ÎIÀuF›Oÿ‰“ÌZ™I-€,Ú<-©ž´²øR”qŽVõ)ZIX­%hµ„aœîøFZŸÿÕ§×µ¤5ˆVgUËÎ3–ÅN/ŽHË R¸j}^4O(IÜ3SjX7qš1š¡ŽµÔÖ鸿´,hÑÒãh©âi11LTAìy£é¨ÐÈ|iI ~}2!ËX»šT¤÷ÔâNŠå´©ÐpŸhc¦Å]¦4ªXÝþDXÔ´veS£µ¬âé27±Øw¡ Œ°FÛ¼Ærú7׳V€.¡í Y¤²ŸÑh¬§öµØ+-›÷XªEEKiËšÍnJD#ŽÖG±‚a-ïyº›né4Ç'V²â¿(òå©R„Å2–„»’F&R uOðª50Ú®O =`¤X,I舖S,ÜBªÀ"¤}‘rŽEƒ‚è'µ&¬œÞ©?­ÿŽð"pAôjmyjµ²3Ñ*›DÛôE­¡yó„Fp d ´ÔǺPh©"4êm´±‰¤Zí¡Dt÷áâpUhßÿÕ›$sRDa·D”>Ž”»SDjR óPÍhIåC „ƒÖ6ŸŽ¾ÅâÑÓúw¡a¼"mO‡eÆŠkù¤aS«e-DƒÅ >£ÅaÇ’g7\þX¡QþU 3ZY,5#‹‹'ãnÁ×Z B"¶„ÔÚC¢)Ên:E’4>["rêhu†µìèµìþˆò -‹‡8 6G‹»]Š0N‘Ó¡Ä2?¤(`V+øä ÐRžV„1¸Zï'EÐ¥hPÂÅó‰(.ICŸiI*.¡VvTÒ¸±QtGË&H˩䨖¼£±²CZÁP,‡¿bIþM]•4Ú+-ë„V»¦µFw8 -—¥–54æët6€Zú2Zû%b yÑ’7Zül4¯Q6ZíkÕS4n% 6EÄ€}þσÎHì_,îÛh¬!1t¨ rO¬ŒH¬‹œ–…3ÚnOŠÂjh1±¥Ó‰Ù’bœ<‘ g¸ª8‘ I8–'ƒ¡5–PбoD½‹ÖçÑ\'áÞÿtô6ÜÆCK.ÃHÁûáêÉ«å>QŒ¨–ºçÑt*’'CÒÈ’ª½‹–çk¹VÒp_-›p}tºá.§³édãÃ-ŽZlV$0 "Œëé”`ÕânÕʾ¡,IôE ŒþpéÀS4];r'ÚºÿWóRFÛ¸žÎ™ƒH› ¡“D³o„±«ÑÖŠHxã¿èbÿgA§–²–@ûXŒã_‰/mi-¤†- MùN§k,;§Ó¹¢»—ÂÁH“‡(Ì\$*ÅÀøDsCŠÓø}4æ/˜Ñš>&k¯…ÕeÕjð¥Ó؉‡ÑŒ§W¦xt;Zl¸@{ú-Uf¢Í™h× ¢'ßךÚFkðHLQ©X€u,`#C©•ýŒ”$?œG%Ö1ŒeÞÄšFè§³‰Äîz6†ZÖqÄJQúú¦5ótì0ûMDÙ@ŠÆ=Úº siÝÈýŸZOCì5LµîØ@›KL £îÝbÐr­•3´œÞeÇÍ(E6ZÝ?Z¿V/"‰è!’FP§e\D«å~ZÀo,qZA¤–±Õº a,±Z"ÄξhD±05ÑâµÔ®t¸ ÒbKŸDÛ¨hÓ¢/Z:Dcøc ÍÑš‰BËÕHóN+û¥5$@Kø™·¶Ð0ÇÑ@¨ÄBŒh!•þJ.Mb¼´ê[’FI÷£àpm‰´1çÖ«eô_`<ÿ±Óë>!(Ë-†Üb°Zjwˆ/ÆNˆ¶PD2zÿÉ{ÿÀ<ÚIVUÅû‡-ˆž²"Vã?Õ¦piFþS5”Ãdé?ø¾§›ÐùŸÒãHúëób©ÅüWß3ÖgT‘ÿÁqŠ´)<½ˆåDóéôŸÖ”]±$%™ý_±Ý‘NÇâÞýOÛ®h‡î¢},úNµÔM§ñ^2ÑCã¢BÕKÿ¡9_…A"e*‰®/þêZ€† þ_yNy%e>ß?xþ1Ði6èxઔ•"Vø!FUŠ´sŒVYCKeÅÀEcd"ít£µQ+iâk=i®µBB´Š'Ñ&P´¸B-)8b‰# ÷ûhíÓ¢+±ä,Õ¢sZuHk­`­•Qb¢G4ŽÖñ‹å;C;µŽ_,óS S£Ew´Ô¸ç/Î-¬I,å­ú­å÷±æÐZFQë:¨5ZkŬXÿn|µê{¤k´zD$ ë¤lBãÚ­ÕF³ûRô~.I©I˜ ÝõêfüSe0…χä,Kÿ­LMq‰K\â—¸Ä%.qðAe)”„$ÿý¸¬¬¬ìŸC^ŸÃ¹%qÀ—¸Ä%.q‰K\âò_™Ã¹%x}¾ð‰ÿ°x<žø8Ç%.q‰K\â—¸üå¿Çâ´c\â—¸Ä%.q‰K\â 3.q‰K\â—¸Ä%.qЗ¸Ä%.q‰K\â—¸ÄAg\â—¸Ä%.q‰K\â 3.q‰K\â—¸Ä%.qЗ¸Ä%.q‰K\â—¸ÄAg\â—¸Ä%.q‰K\â 3.q‰K\â—¸Ä%.q‰ƒÎ¸Ä%.q‰K\â—¸ÄAg\â—¸Ä%.q‰K\â 3.q‰K\â—¸Ä%.q‰ƒÎ¸Ä%.q‰K\â—¸ÄAg\â—¸Ä%.q‰K\â 3.q‰K\â—¸Ä%.q‰ƒÎ¸Ä%.q‰K\â—¸ÄAg\â—¸Ä%.q‰K\âq‰K\â—¸Ä%.q‰ƒÎ¸Ä%.q‰K\â—¸ÄAg\â—¸Ä%.q‰K\âq‰K\þïJA~G ‹Áãûi‘‡ƒ§r)(³ƒø?Ú©"®Wÿfùnúƒd¦¦Ñ°y[<‡¶ä_Pv„áÍê“•U‡®Ýû2éñ·â*—Tt<ðÀã&“éoÏ磬¬Œ¤¤$dYþ»ÆñÝ»9pð»÷ìåÈá#Øw¯ÁJZr»Ö®á˜ÃK´œ'70üÂÑT:Ó½c˜³ø‡x~ÞVÚ™K¸ïõw©Û¶õ“ Aר»"Ÿ‰ÔD#Åù§8tè¶ôtô’ä_|äìÙC™Ð‘”hùïhÛÎþ}û0$¥cÒI1ÿüäñýì;RJfF ²ôŸmZÑöe\ûøK$5ìF‹ìÄÚ0 <ŸM»öCB:6“̯ ?å¾7çÑoPlú¿¦g%Ç8|¼„Œ´dnŽ:ÄÑœãÉ9FNn7éÖ$þnuvžÚÆU÷?'± šdþëŒPP?ŸâÉW¦sý»ŸñÓ²_™½h ¿ü¹‡¶­»iÕŸ&^tðÜ#ðÃ)ÁÐ-¢^~`ï~Š|FRݾ†‘>Îk —òíÒÅÌY¸„#‚[!ýMºý“Šc\ÿð L†T6ýú!³·”1¬s˨?ÛùÍ[\qôz% “ô9°]{s©['ëo×yQ’ì·_fÖœù9^@Bf:YÉIþïJÙ¾u+»öí%çh‡Â!Éd¤$ÜÁË3_åÍçaLm@óÑçÍ‘ÛØµïŸy¿¿K\¥%øRÒ©#NðÝÏ+h3â¶H ê—¢¼"µ?ƒ•_ÉÊ|Ý4 ‰¸üÿ(ÿ(.\.׿”éô•p×°Žtîѓ˯¸œ‹/¾ˆ—Œ`æ·Áøž½8ï®éʵöüôÛ¯¬Øócòróõ®Ãäåç²lÿ^*B¶Œ¾S›¹ðá‡Yžã`Ñâ¹ô½÷)ŽTxF¡œ;\ö_ë®]˾åü‡fï9½=ïìgÐã¾—(ó¿VyYÅvçpçóÕ–­ì*ªPý>ó¯ôšt3ŸoRvô9'òÕ–”¹ÿ:;öÄssóüåÊ?ǹuÚT†>÷w|<‡;ß}›óî¸c'óÕê/¿æ(â«-[Øxªôÿät¬¨,§°´2|??ÐÏÀÜòèÂ=¼pï>¼íÎm•ɧëVqÌî:ýFx]¬Ø±…_Žžˆn>NmæÂGbyN•Εpë3ϰ/µ _?x3ƦÒwŠ÷þÜ÷×t»(¯¶new±ãoFp.mÛÁ–“yì9²Ÿµ…ÚìÛ®,[ö9åÊ\š~Ï¥ôêye3ñüû'¯aËnÈs3àÀ¶ÕÜtÝhžùûêïßyf2zœÉù]ÊСÃ2l~òg¾åüIϺiÜ8í-~ûêc†õjÏÛó6E}îô»/¡W¿ÿýþNé0üR^|a¾þ(†P4iJãÖggðâ‹/qE?ˆ8ÜŒË?+úç[ ŽœtSÿì1ìúúM<.'$ÚR‰/÷íÄcÉVH>YaS“cg{ûLdHføÜ¤†°0«Ö¬¦HÎbx§tg „΀8ÑeF_(þw;AgR€pT€9!Ê–ÂF}õî·ƒúpµj5imzÑ4A§‚x\ 7®`à– €Ð½ùš{¸ú/IþË^˜v;ë^Àw·]ª²ÒºÁdÐÜDzÁLzr ²2”*`1WŸÙy(»ßiC–Y$Z¨›’L­7´w9#Ãì»J°ì`£.êãÿ½… ;,o¹a/cÏž]<<ûu®¸ëæ½ö&—vküÀ $ÔŸ!ÿtFÒRSIQé7a¯@JŒ¢ •.°#|ܧ8+Á†u·Û!1+'< ëÙì§ñüFž~ƒz?*cÔˆ>ÕíÝ¿›ú}ÎáÊžxÈPÌ'kvÔv:ÎKãt€)àd‰Œä¤„èÞeÞf3¼S–Åœ`Yž‡g&_Åmm¹fÇ^TÛk8œ`Ó¦ïd£…ì”T¬µFeqzÁ¤‹þ c2ÍtFä¤DœN‰zm@ؤح€§ÞXÊ-ÓÁfˆÑP HÚøÃ_¾D¿Ñwqî£o³ð‰ÉÊÆòÄ1|^[U'òùŸÑü¬ÛXñÙTœe¥x„:YüóúÝ»ue—ù,r÷üD††Õ×sÓãO1鲯".xO½ñ·¬”: »&Ä%.—üKc:%ðAF½¦X­VRÒÒÉÈHÇbÒ§‚…³?dÞòߪÁ‰5KÚosÞ k»Vd×kÊew?O8¢Y÷ɽ— ¦q£¦4ïÔ¹¿ŽB1rîÕwÑáŠÇª'@FÝúd5HòcÊ ¬8¼ò u3’iÒ´!-š5ÆjUzó£§Æ±3ÖmR'@÷!Ý%QßvåŠï‚ßﺚ÷«§ö~‹[#qÝÚpõ²½ÔMãì>y÷uî{îêKž0€¾ç]Åç¿.§¨ðO>ùcFM%V?ˆˆsÆ%:ÿs’`•)9¾íÛ·±sÇ.ví?®ÙÁ3Óžåá×>UØG]•yUŒÖŸs_gð˜[hõmÌxá¾~ñ>FÜóºê3êwêÆ•ƒ;“š’Í5Ã/$;!`×Xq˜ ‡JÑ»»*&ý»ä_x~þñ3®ÿ`W»IW]Ból[5[ùê3OpÏo{¹ûÆ×+“‘<ÎâÝÊâŸst7OÍ|±ïÍ£SŸaôíÔŠÒƒpÍóoѤïåÜ>j ¾`K“ûç:ö{,Œ8³M•ågý¶?yðåg¹û뵌¸ôJ’Œ'9ë®{¸ðÙ™t;ë<ú5¶pÇ+/ógž¿_¾€Gæü€HÌnAÃäRS²èЩ=u³Ópîå‚'_ÄѬ?wNº–Þ[ Ši?y`'¯-^B¾ËÜE¼½hK(€G–MdíÜúê ÖØ3wÅ0Þÿê=¦¯¸cËlçÑwfñÇÁ¢ê̓¤3ÑÌ ûVÏcÄco“ѲÙ)FðÚ¹û±‡¸óûõ\tåÕ\7r(ß,xa½‰3‹Ï_½–º-ºR×H–ɸÝîà S›2úìžì;¸!Á±Í?1ð¡7h=ôJ¾õÎ{› _þÉuÙ¼ý'´ú§»W.aú×?Q·I=NìøA½Aë¡WÔüîCPªãl•éºÝb:ÝöGÒÛqÃØ1tN·sémw0oíAÊŽâ©9ïsÙk³IlÕ“QçöàÝ9/2îݯ•»Tòáª\òøS¬­Lcü¨óøvù7´¿õÞÛVÄØ‘£8¾÷g&ø}õFà’ïç·òtî¿ýVÒÄAÎ{ìYòÝJ›6lßă¯Nc» 6ì²l匙þ'+Öôú4ÈN¡NB*gvé@½ºÁýüÇê6ïJ¶ê$5mIfŠž$)‘ž;d2ávºø#+sdçöMHi-3æZzÔÑqà ϲö¤²sx÷‹7y`ÉAnºy2ã.;—ìs5~òù¼XSÇã÷ÝÎôŸÒµ{Û&×?o{é÷š˜hÓ¢9:—äÌ4mÖ½,áözÀ]Û¶¦Û°á\7ñjŽîý™±ï}­´Ù{Šq?ËkKî¾n"Ãz¶#Aþ& ¼B¢q½zP¸ŸŽ7ßÅÄ:thª[~ôÈ.žž5‹kgΣuÏ¡\zfcÞ|a*÷-Ü„¹^c–­ú‘Çç×èתŸ¿åÕ¯Cߤ çêK»Ì$ºtʈîícÜÃ+ƒóé›OðìÓÏ J¸YúùGL™p-ž™ÇÕo rÛbÆ G‰ÿûKzôà…å'™úÒó K8ÅÕýz³&¿"ìc6|ø»<Ð2ë87]v-š·äªqORTpÍ7Ÿà^¸i8S¦½NYhµ3íù%Ôë{=ÝRk>.}óqž}zšÿý\5ï7mW¼_¾ûï[·fNÇw3mîZ³Œ>_€¨(fÞ³wðûGòÀû+üà{x N±â÷MÜyEGpT„YÌ¥HÔL\âòË¿–[¯ÛØÌÏK?§OÿÅx*˱ֿ’Cû>Â"›hhcÝ̠ݞΨ¸ _xêVÈèÅ÷݂٫ßy–7f¼BÁ ·Ãóm\ÇqÉÆE]›ðY’Ð…úŽÖVxý‡¥´=÷ÎjÈ~Œ®i/ž¼â\Ì2ÜvöBúñ>wF“ÈF@½óÅçH-†°òñ›ÑWéñý#yhÞ".ëq#6›‰4[&OÝù7 h¥èwÁAøu)%·^J²Õ†E2rî#ùêîÑŠ¾.YÈÆ|úÄ£d™Ázt%ÏíW–ôý¿ÿÄ÷{JùüÕ±œ×1›6òI>»ÿM¾Û^ĄΩX-ôuZñùSOpvótŽeA‡é²%ÏͰvgry·oxqwC^œ01¤+ Yº=—¾£ÇUëÞ°³.eëòy×Ý”—¯¯0…+Vãð(!¯¿Ÿª;]`(döÊ]üy¤˜u²8VX²‘.-;Ñ¢GÏšgyz£É[ÈŸ¾Ëó[+Xøþ»œÛ<9¨Iÿ\Ç ÙÆE]•9dLiÄÃ׎à…o—snŸ‹¹mˆòya¹³Z$ksÞx~zõ=Nl\Ì}káŒ89쨠‘-óû äü>Á˾Ֆĉí+˜üËL2z_Íæéw„&ƒ‘ÄŒ†|ðä3\Ô¦Ž#ïãÅy?2}øƒÌ ÍÿÃw\Mc“—ßýJ¯KGsvÓ¿zHIÑù¬º“ÜHX“$ä¤ölÙ²™öé21í+€iÛ|³ý(7Ï\É5—ôå‚–FÞî| /ÏÙÈÜ;ú©>å‹?°gý!z\z!7´NåÞgÅaKeþk·Þæ^~±?}ùÓº•/¾Zʺßç“a‘9µùkv;àÉ미k1«ÖäÓí’Qß2øýd¬6 Év[·l z¿“vÈHù-_>o¾º˜äÞ×ðÀ¨^5Ÿ[’iÐ „Çï}}ó¡üòô¤ê¯u §•–@ŠSq‰ƒÎÿ˜?\I£³®á·÷ŸÃYV€¬OÃâß+ñOÁÎàasU®´mÔ–2—ƒ¯”ôº½ÝÅ1F“oV¯!³Y„Ûy„„Køp{ž-é0J·Wqÿ^pٰ›³¯¹„½óòä‰ iUò£'IKKeõòï°âkd„Çl¥±MY¢.'Yõ^í†:=‡q×e[¹ï±[˜ñq^¾q2£‚\k,ÞxAÃnR„òJ;Mšu§c–ròÊ+iP· —÷h®°“ f‡ IªÞiyÀí²WgmÔ“÷F^Äuï>Ê?|ÌÔ “¹}X¯jf`3 :Ÿ7àó¹pH6& R€#®r :#F¿û\HÊO}þ$$X)Ê?È 'ÖÒ±÷eÌ?´ºGä’‘^Ÿó»´«þ¬M×3HO׳éÀájÐY¸yû¼zžéÑ^? «¬Ä+›ñÚ8+Løt'8ï–Ûpx½8ÝeôNkƒè9ð,ê¼ú5ßl?Δ.:¾Þ”ãÓ/Tú¬TO>Éð[nÇáõàô”Ó;£Ò§þŽÕùiùƒG qöecfæÜ˜¿ä¨¿…«-…Þg´ª¾¢[Ë6¤ýúùnH–…•zÎëÜ£ºO+n†öêȱëXJèî² ÒRRyíýgyµÒƒÜ¤f4¯ÂN—V”Ñ¢mÎn®lËŠ+ÜXf ~/BA…ÉçªwX¸yû<¡ýì¦È ’ÏY}½^¯„¤Ty#DîAùèSöåUâóV ¥Ö! …ª¾ùâkY·íyZ^t!ƒÏ:W&§Cƒ¡~Fž[ð å•2õ‰·kNeÞ®®5o+ ŠõÊÊŠ€¦~‰¤ W/áÙy?“!ÙZœG«Œö8½`Ô5àõ WpÅÛoaXñwŽÏ#‡b‘@Ò™¨¯·3ñgñš›÷ÅÝÙ'—ÛMjJýÛÔ0¡:6G÷}.à‚s†a˜}/‹v2¹Ñqì-ãƒÛÏþÛll^ž ÍÈ‹iŸ®ôEE¥$br·màã'F3ÿáDì`2˜héÖ…¹[%{–þŠ®iVþ±„ª=ðÓç°zÏj@­ŸG«ÁçqûwðÅ££ùä',;RÊe­SسâW@æìó»Õxr~}Ÿ­xæê±1BlÈË´uQÈû%ü}‹¦½”#hß­µú÷åÇÙ” }Gô±Ùk¬§±•ˆsq‰ƒÎÿ˜8+ÉuêÑ´a= ^ðTµ'\U¬Wa1$6:—M¿¿ƒ=çnS uR3°ÅÒS¢˜ï6Ÿ ÇÈ«‚>®'%à*/¥´RU^s½ŽbŽtYYålõÛñÑ›³¹ò‡ùLû%ÞƮ÷_£QdöÚy|äeÜÓ·1ÇŠ+°XI÷°ñ!#Šã©ç×Oq¥;nboÜÔ£KP;eY‡ÏUY½è $¼^7ÅeÒlz|>ágˆÕ–×ëÃb ðKë˜xÃÝ èÙŸççÅ?D…ë)¸ 7²²NÆlÖW3›Å•Žšô tPZZ$Pã ÂÿO—ËIRrçwoÍ‚µËygù &P@Ù(°»œ”;=5ª_éÆëô` 8²pÍZ¬™m锼@ !ÐéBÁWÀ?¯¥më+1$'‘[vŠäŒ~Ì¿ý \¥E¸$#é©iJìpj;ÆôhÅW‹¾Ã¶Ý…¯ÁŒìš x)u”œ=€ù·_‰«´—l$=Eù]e¥ D +o4 N–GW–V`6'Vw‰Ok¸×ë¦Â#cÑ)ô¾^–(-·×,³²LE¥½Ö|°8Ý”V ˜xÝm>Jœ^l¶$R²Ò/²,#¹œ¤¢Tý|ŸÏ‹Áh¬¥-‹Ö¬Ãš¥ÒÏaT+!1Üù {à>ŽšÚòê­×q†}+ÝŸþ—?\£nÛ,þà=>Ÿ÷~þLÍgíkO“m2Sl/§_Çn¤ã­ÏÞgl×'i”¢ ™·'é1òjU"Â4rßßrÖCosþ¨ë¹gÄæ}û*Ï­²#<€Nfø•“Ùß¹¯|û ϼú$Å/ïŽ;½äÅ.[Û·+¿nZÏu3`þõçG ¢V;tN.ŽJÀXÿLÆuoÂÌï?ã°9—ægôçÊŽÙ›Õé@8jÜŸ·z;_uíþ—¿ãæžfŽ{HNË$;% EXzˆå9pé£Sª'd«úµ½@õ'M3”R’º÷Ï\ÀFÓ×úÓO<º\A“ØßO¯ö~òßç—¶¦ÑÜ[¬†×T¾÷xñNwÀ¡ÀŠ£|±xd„ô‘ßX˜Â2^è qè—Tþµ1² .‡C}«âUðT(ÆåÜ=°o[ÀÆ<#MÚu¤eóFØÒ¨ðjvñö têÕ팠Ï[6©GYÉ1>ú¥&=LÁ¦_øöd>ê+,âñ¼\ÀÀç_É/7Äux3¿¨Dפ =Re>ݸ™„ì´lÝŠêc8}(A!›%y”—yèßc Ÿ»—$O.?lß]ýý¯k× ’šÐ/Äý&Q+ô3`ÉSïR)Æ;ÝNöå×lÐ ó(,® e—¼;õ)zÙÜü°]9¥ó¸8VTLq™O9O…nê˜5Ã%ވ¶9]<˜™ùȃ^­Ä:6ïÒë/}·°ú—3¿ûb—…A›WñI|»a½{öªEl $dáWŸ‡ò¼#ÜûèC,?ªã©+ϽLǶ9ºæwvy©ß¢M›7!)- §ÿgÏƵßqóçK¸òœ¤ø{¼w—f]û;»< Êïš)¿s+8\s.%n±kÇü²ðö*7»xeázuT\·嬨¹§ú>øñ'Ršµ!KQ—r’ èŸB §6=:µÇX~˜ùÛÙ¸)-Zµ »ná|à¯Ð̤TºÝÈ MI¦ôs•~®ðª“ ¥{ùi_!w¾‰am“2E•ØR•\„9ÇN‚!…‘£®åë«/àÈÖõì±+û뢒|Z1/‚ìœßétÇCä;‚çí!§Ž‘!óV ”XáZŸ*sï›)64bÎõ—Ð,;ý‡âÑI2ðÚ9ž_Jv«NL»÷1ƶI`þÆ­ ½+u3ñª»ùé¦ËùæG™ðq@ÌïæÕÌ™·ˆS¥ŠôJÊŠX³ËÀË}Š9‹Ösf»6$øõè†sÏfëŠ<³tW_x~DVaÇÚŸ™5{.ÇÊ<곪JñªQkóî«uE`²_‰øúËïHªß’6íÛR·n.cCêo:¹¯&õÑcc/# Ýsi-'wrÞí³0·ÉàFÊoë6´¥ó«÷Ö™Oòê²SLž=‡4`åןóÉ׿-$S X'ÔÞïo)…;n¾‚²Ã?2æÑ7¨•üœƒì8|³igƒÕŸÏä„ˉ»t7Cë·cO>˜êeÏ#ÿÊ^!͘,£dªˆK\âLç_‡p2ç¤*Î>â„c9JJ áRÛO+ éÞW¿aÙήœÛ"‘C†‘l?Êf]6þò {ë×ukHKoÉ™uƒÓÔÔé5‡/ù•©¯?ξ}#h&øö›té5œñýÐ3ç£xzE—ÕukWÒ¬×9 njdÞ¿ç>ü"Æîäì3[óçŠß~Ñxž¾få%EœÈ9…+À&îûù.{ç3ú]t>Ň6#×=ƒñj‚Êæ­ÚFÓî—`iÿÉüãñ6¯¯%…œ8–[}owe9û§ÂŸ ³ 8’“F\^@ºtã­÷æÐ)ÿÓo¹ƒžÒfúÜõŽØ3™÷ü£dë uמ´3½ÎÊòl®éߣZ'']{ ëvOaÀ˜Ñ\|vÕï2XôÚcX>ŠŽãd±òÎ7Œ¹e›ï¡ç7rq¿¬^±”âº}ølŒÂvx1jÕ1ý­i¬]ÞS9;X|¼z-zÀn/SîWKÕGΉ ‹kοæårð”â²Kl3Onů?ÊúµýèÜÀʺU»yäžg¸²g6§ ‚õ¥²¢”œc98]JLÊY­»2ýëY4ŸÇc¯åÚ¾ÝðÝÂÎÓCûÈÍ?ÁAoͦÅ]YÆþcÇ(²{!³97KçÆîáû&õ¨¬´“’¢çžw^§÷”™7s*Ó¸ s#–­\ÁàaÓ- Næ'wÇn¸¼?¿¿>Æ×ÞL£qw°ê¥it®›À/þyÛ3dÞJ^ÅÇŽq"€]Î+Ìå`®r¢º÷]Išó o¸“žVûŠ ìÔ*îür%Ïô±qёަiº|>?ndêƒ]-/%ïÀ^6œ,ãÖ£˜ëA.yúNœÏðÉä ùôÛw˜úÞV–ü<€³“0›°è=Ü1íQ¾<£ ;w­a£§ +Æ^^ 8ºöÌùóðmi}®îù”úÔû&òù²Ã,|õm¶ÚÞ¢2åO¹žŸÊ9˜ý§»;š“s¼´ŸÈìœ*zÉYðö†ßð0†eŸ0¤Cþܼ‰ ÏÌçÙñ}Uv xèšÞÜýÉ# tüîè>~]WÀÛ_¯ {#àbʃùå”ÌD‰Ÿ-Bj=Ÿ~…Þo—_w žüŒ®Í»pn‡4-ú…‹x·¯”rÃe£ØN/ΠƷ†¾ßQ' XCªÞÏñ7æñ¼êÉÙ|»õ(sž¼…9ï|øp]ë(Kݨ‹&Ò´§Ž*GΨ1·œÙ”¯wÂlIiÑi·”³òÐ Ü)Y4iý[6 ½]_–<û4¬X‡%»9 ëËò Ð7T@LrÛÞ¼ÿôTúµóWÑ'pÿ”Gpe(ß'¶ìÅûO?A¿¶Ê{ r ï7ˆÅM¼÷ôKtíñ#fYAÌŽº™µÍ:0{Ý<¾æ|÷ü$.<³&öñ§?Va·4¤oËEØœÍswß϶;ÔMH劾ý°&Ö8 $[=Þ}ñ-†¿ˆåÇNáKïÆEíº]…ìðú”ÇYã²Ñ£%êï¬É-yçÉ'èÔNigbvK¾{s¯ÿ#;NrõÅ“¸é¢ H÷ë‰Ëí@2%óüÄÈ9º•=I}XuÏz7UB1Œ™íü÷óW¥Ñ[˜rß#x³j*õ\sédz–Õĉ}+[Úvåãµ[©@Çè«G1 ­r¿ñïáQ£/Íûáƒä&tÎTôcèE£Yž”Å'ëv“d´ôsú„ö3z&Lº—!¾Œêû¥·ï͇gÐÆ&À˜ÅܧžæõŸ–sRJâÖaCpçîfÖª=xe™±co¦|ùzòJì\?æfn?ïlLþûÞߣ83•ƒ} Íz²í£Ù¼õÛvœ. gî;èÐg|-÷%³-ï?5•Öm[÷]éŸ=/à×çølÝš´ìÊì^­ùú—ñ Lé­yþÖ›X°ó ^w2ßM»Ž º*ºkmÝ›÷Ÿy’¾•~ºxÒ,nÔžMN¥F_u©]óéXOyƒ ‡ä”º¼2y$·mÅÚûBfŽAû¬€x>[&‰^}:w£yJdGÖC=ÅÂ!c8U\FM¼O´¿ø^³ö¥·ÿù7=ô}ÈþMjB8÷æÇx­ÿ)ûuýÜÉÏrðŒ¼ñÉ|N–:;~†u ët»ëã_Ijó$ßþ¾k»Álšû š§Vó»÷d÷Êx0sÿKïsÏã‚Zmn؛ߗýÀó3?$§\âãÖ3ú¼ªøNS§Žà²G÷Sâ‚•´µ7=¬íý©à²í_Ì`øÏ#tÆ*¼ç'F}xõuxþ½×XûÌ-|±ëTuq1îºk(9ã*æß~éÿÙ·ØðÅlz¿ú%˾]Hïôx?ÿ[æíœ÷ŸåÚ9›8ùógd…¹fÓÒ9tyäæ¾7‡Q"—õ]þÎßùkm¡[–í_>·+yòêA¼w¤9[–}JŠî?{÷ãëòü§KquI\>áZ~ûrËCiÛÿ®»ôœÿ~79òyéñiqª…Èœuñe¤PÆ×ß,ySEtƆ<ðÄ]¤›ã«Éÿù§qYYYñrÿiê8©)ŸN}˜îÿÀ Q—³ùºåqÀ¶Ü9þ^ÒZ´ù?ý éétmÓ «Hˆ÷ó¿dÞ¦§Õ¡k‹fTVø AÅô±dÛº º˜‘Q'@IJ;¾_ñÛÿ€ÀA³!ùõ¢ ÿqÀ PïÌá¼tæðˆ×ôi÷$×ü¯w“9ƒ;Ÿ}1êeýž_.ârZg:ã—¸Äå_!·ËÁhŒwE\â—ÿ9\VVVöï=½—¸Ä%.ÿ‰œq‰K\þ§%:ã—¸Ä%.q‰K\âò·Ë¿ÎÏ-„àx¹§Ï‡$©äñ­Nê,¾””<|’ÿsDÔ\ø§¨É)ܧ¦ìJ`Örÿ=«š åï„äO£-DͳrÉ û>G¨þ3èÒê j•ƒ Ó$›ÚA÷* ¬E;ø±R@鵯„¾SõõT®¸Ø¿_ Y„¤šù¤2U‘«û 9"Bõ¹ä_”€:Úö ¬-Õ$Y÷?D’¤0cV£[Uý+I`R ê§êkÂõgމ YPmB‹>¨\ðq°î†êÁº.„fó¼¦#’,U—Æ ì§0p'¸?!Õš^¡ïboÞ;|T'¨ÌåÐy&ð42øˆ «¥SÏŠZë6_ »¦¾³šÕ.­Œ¡š-ªc•uHeŪݗUvI¹_¨ý:øaîJPk5þ9„º¾‡ÎíÀ[V¿r@%õý jëjÀ¥àýfÐz/UÛ~õMjMj®pû.!ÈNH *UÓ XôzÒõzŒ:]uMn5(¼HÁŒ˜P1awí*ìŠ*Z“Ô)ÁH;j5½ GÕz¦¤ÁtŠð°<,ÛBðN\Ré¡Â²…cwÕØªZ,™ÚŒŽlj1áíVE®*ã!ÕXI… UeÏš†šaI¤qGÅâIÁíRÕË@ $"O,)ÊŽ#ñúî!}[³é ¥ƒ/–ª U}7·ÂÍ5] œ/A¬žPaÁÂôs-CC-;fuIaÚq g ûPÕ}Ž*S³QRä{KTã¡QcÌ4URéóZ µfL"ÌÁH6§Ög*z ª¿DéëH†ADa"0Àá¼T‘¾$iÓc­¶#œ×&\²ÊµúLͨ߫zC#„ KN5Ø‹üÖ)‚.‰0̤ùÖ^_#K.¯·›ÿUùW#€ìó)Õ%ª‹@ u0XkÑa°i€‚Õø-Ãh¡â»ª¢cµq­„R¬>¡>ƒî/€(ê‹©jH°á—Ô ™¨ý,5%c@E ~ó"¤ú¦Öû‰Æ,ÄhÔš¬!ï£6¾jÆÖJkls}AtAð{«µ#´O|jý© *ׄ.d"t£""채J¿Šð /å¨íÃÔ•~ œ3"D§«ôÆJC„ÜS¢6`a¨)_„¢zÎ…(-à Þ9 Aã"ÕÖ ú?`®ùP  ]o¤Úúªk¡g¡b ³x†n#ÒÀ¡}@ˆ>B‡èžcÓBÁ·O„§k¡>_‚쪨m;ÔsŸÉæV«…É ¢ÿµæ“pê›'e=ð©èJàFG¨l†ÃÙ|_8=–‚u"´M¾p¶D¨€¶ÐµUÔè|-½Õt%°Ï|"ÌØIµ‰'ÂÜC…¯©Þ$‡®‘Rl¡¶Aµ×RuŒ.!#ðT—$ZvŒÿ¸ü+I6NÍà-z¡;À·$EÝHG_R6TCÛB'c„ÍQD¿·ên^ *„&Q-¾KR¿—l8î½’JB'b¸ÅK„ÑI¥­’ŪÓ@¿°n£šÑý}¸ë#銤²A’Â, aHkÕ¸ŠZÀ\…y! Õ7‰ð ‚‹´*Vë/©6£ʶÔZ|Dpß©ÄM‡Ý¼iaºD8*p‰Úz\ý¿Ê„^|$IB ·Ù!z*E"’ªâÊ#lP"Ù‡Z›„šE7xî u{«¦Ÿ’ˆ¼ñ‘4èWØM’PŸojv¢Ú„KÚ窈ÖN)Œ“Â0ŒRdðøÛp eæV¡±©½H"‚M 4øAöA o`ÃÚ0kФbGÃml#Ùùˆë¥ˆì‰’ÂõCMˆ]Íú(ÕnGõø‰`ÝSµÖp¶èpþkAgÍ_ª­a<èR4?‹„z@$ÁMj¡¼ÁL_4€£Æè„N8hJ*†Jhªéù’úýj-! ˜ÔìTƒ&¢ ª±"È‘ˆ~¿gJþÿj.)Ì<µž¤E9C6$6 R­1–Â/hR„Lh?GbAÃÛp¬„иˆ¶A Ó®h¬ª \0Õ\YBÃb'B6t¡s.ÒKaú…0›­@…^Ž•–jÏùpìHD5P#ZvýZ.Tc"#õ—¤²áÕºÑV6ê ní?ãæ ÕM™.F3"3'"ÛD)ʹH)‚ •"è³ ˆ‘ÔÆB ñÈ„†9©V) 3¨fÿÕ6Dðµ¼!›i5J=²C„ñ^„n ¢xa »î‰=ü/Ê¿7e’…M LáX5FRa"BY!)Àœ‹˜-!EŽãS¨jï'´3MB ˜è’ #ÆÔ"_„z[áÁj¸ETÒ`ä¥0 +”y‘¤`£–4iOΫ±‰‘@„DĨyB ‹Z_ª‘‹ÑØ`Aôvja›BÝ{RèfL„_Ä‚ú6Zàgˆ¡FÂêI¨7æûÐð•€ûK&tÃΩÚ(EÛª2å„‘ÂŒ¥e!!a&BDÀaÝxÁ¤$ÕwÉby~Gë"ÌB Áî½D²@ReR”Í•ˆ²Aá<!ßEš6’Ò÷Á™T"2Ò¢v£"ü†IŶJáæ¯$…o•I!…èj¤ ‡2ÈáB§$•Í*3’}!ts&EØ0ª±ªù”rèF=TŸBAr(StP†ôþ2ÿ¾˜N!",òQ!Õ…ª&®C :QnÐC&è†3T’†Å0 %ì9¡(ó* „µ•6û|£ÉŒQ¯÷Ÿ^8<>0Ojžï£9‘‹ÁÞ#(*)Á+BX=5ö‰»šA 31«O=öiH€¿ÿB:Ò“mÜÙ‘‘Âǰ…ÜQM¹¥¢0’¨ ¶%5v ÊFÃohuz3)V3.—ƒR»9œ$+,ƒÀ¦Pe7}>Ál!Íb¢Ô^N¥Ëƒ,Kµ™üp‡¬¢öUàr(0Ò1\”êâΠG9ôj'$ ›ÜpÛª“í"€uŒ]¬V¿¦Ô 7_Q™ënÄp›=!"„ôˆè›•ç &¾ž(¶9ð‡¨ :ªÄ瘓H1Ê”ùõR’¥èl¨*³æÐ™„Ý O„…ݼֲ5êÁç"ª§$ò½ÂŒ·$…îªéoŽýÔs‰Èa#¸HLo8o ZžS[o"O[¡mâáU\˜GDÛäE`äkÛP)8H<¦óŸd:Uâ¿B Ì’ëOŠÎDˆÈ‹·‰‘ ê•ØÄ(V§“=\>¯WÂ'¤àµÄ>¼>Afj ÁÆ5ëX¶b5ÛŽœ@Òur£EXvaÄ8ˆzIG‚YæÐÎí>PˆE¯‹¬_jŽZX s)"»þYVÕÉ>$½‰z))d[ÍìÞ´•}‡ó1èt·—"¸+Ub?«ÙIXˆÐñ×áuW°áÏÍTº¼ÕÏ–ÂÍ5µUÐm©¶ëO• ‰ûÅë¡ÂD‹p ‹ V+&£Ÿ¨~)8.+„¥ˆÄ@Œ…O‹ÕŠÅh¨‰ «e—¤ }Sð¯âþ Ïæ ‡Z¸C4O]à’H.ÞçHjcÕ½."ÄÙ)76èuçì`ퟛÐ'Z©—š‚Q¯Ã+"l E˜ «Ú˜Hý"…N-•Ð!Uײ^7BUªå ]'¤(LxØù!ÔA¦u—Ú1à"d.E²óáÊN:8Ÿ‡y€~õaóº_8UîÄ ë‚Úf²Z1”ŸbôsXº¡”_~˪ßòâÔ»H”ìØî0Œ¨½;XO’,£Óé‚]̘êÀ÷ô¸=ÙÆÞ=9û¬Î!ðú„Ê^FC •$ÕN‹"…°{þ1“ÔYÍ[Ó¥KOZ4ÉÀ#¼µYÂÀ_É2²$«žæ×U³Ý",Ë-I¿'ÌÆ#R:DøÐŒÝ ­Pb¶Y1•ä¢>½™þÉ:2m6žp¯Õ¿êqÁÊ|­™'ú+r1·õíÃ}Óç“i³¡aŸglj¨Ñ/joÊ$9ð»÷'j×éuÁö¦'ý Í‘XõÊ:Y¯ ëÌ©ÖqŒ$«›,ëjæ³PßÕØgå‡Eí.½—ÞƒìÊmý çŠÀ¢°®"Üá²ZÏQ µ‘"oŒÂAPñ®ˆ¿¶œS€ÁŒ­Ú{¨Æ`‹È›áÐxÆpv(Zÿ«¾Œ“+ÕÄ‹ðm„?¼*©x–¢2ÒDf›Ã……T{¨"dÞù/Ë¿2Ogt7PÆÐÿw¯ð‘œJš %N76“¡{ðq¢¨ 0'&“aÆí¹eŸ½`4%»6oáϢ؀J³ÙëÃé¨Ä#ôÔOµ!Ç ‹$ðúdê¥%£óæC¢nZ z Ä#HÖ×(QQy)噆)V^n[`;JJpû²LÄT^¯—„¤TÒB4Á/,B–%|B`KN%9d‹r²¸B»“ .ËeÀæÆeÌûãyªWêÀ2i×.eåÉ &¿ÿw]~&½BAi1etA«Ž4ù|`4SÏjÁå«äT±ƒì´TŒÀÉ¢"¼:3õ“,”U”Q섺©¶ZJ]X^J…Û«Äfêô¤'Y1Õº¦»G‡Í¨'¥Nk~^µZyÇÂ"*¤* R VYIÅ8y‘IMM"1äž J]ÔOKAåHhl¡½ »ÓCJZ*6 Ðé&Íd¨þÞ‰—¼ÂR$º ¾íVyéKú\>%_ÞK"Pî´STá@,¶ÒõÁ†'·´—[`°$’`Äît ›ÌX®9VTDBj*©@Ye%•t²ŒÏëCo¶’` Òm§ÄkÁ,Ëô7ÇßÇ K1øÁ«×ç#!E™G¥N7Öyt²¨„LZZ !}uª´· f;]Nd£)¨'KŠðúdEÇÕXžplsÈbàóyÑ™¨“`ªe.NáñÉe ³ÑÍú½»É<éQú×bÁçõât9>ð!“’’„5tܽ KL‰JßùÜÙôÎÇ‹ŠÐu ‚í;vÔ®ÿáÅép*µ¥‚Úc*|>|’žz©6uý*/¡ÂåCÒëÉH²ÍJ¯“‚’ e­ÒÈJ¶ÖšGEå%ØÝ>’SýúYé"Íb ÖÏ¢2$!ÐÍÔ±Z‚~ïðyÉ/.EHI÷ßÃá&Í ãÂM^Q9H¦D™ÆÚá-'ŠŠðùãWmÉ©$ɵǬÂ^)£cÆÝĘq×óÒäáÜõæLjÔO§\†ÝUŒ7TBã»ÃåÎ bãÕ3Õb ¡±Âµ€‡Z>Ô098#Lº^ ΖZ¢YˆðìtXOQhNçÀX‰( 5ÒÐÀêTÕ§ÝQ/Ä"©¹aÂEHªÄöHQÆTRÙIJQ˜ñh$c´¼ÇaA´ˆœ! ýR5@þß%=ÿ…L§ä/»%…9”'Ep1+“')ÑÊÁóxàî'(GÇ·³Ÿ¦wŸ^\på(>øì2¬V’’SIó”ñüÃwЭkWzô;‹ç¿XI–-“®jgï#5ÕDjj†ò„NoBoJ$ÙèàÍ'æÅÙ H°&b0šI·JÌzñ ž~w> ÖDÒ­:Þ{õQ¦}µ ×Î.£w¿!\1zì2r)Ý÷8+UâÜý3èOïÃxòýÅ$''c4è".¼%! #¼“ݺӡSWœ5ŒW>]LJ’ ‰´ÔTäÒ#YOƒÔ$Šö­åÎÉ×пÿ@οøjî{øi–oÉ#Ýjäõ§0gÑV žXBªÕR=Ç’¬‰òÏ£Rt|7ûiz÷éÍWŽâý¹ž˜Hƒ´$r·ÿÆøK΢C‡Nô<œ9‹7“”ŠÍšˆ#g;÷ßÿ»*ä v¾=¶T z)BÂe‡eèšÞ`"Ý$ñùû3Ô¿':t¦w¿!Lš: Ù’H‚I 8%#É@f‚¡zÒý,ŸN÷â}ë¸ó†ÀqŸÆoë–˜€çäNî½ý6®äÔïß2¬?†¿„û^ýÕª€PŸ‘$=¤&ý6t}°'%hñ¯H:“…$}%¯?tsmF¸1éòaôr×L|’Sv#ÉV õ“ll_:Ÿg  Sçn\|óÃ(7Q/%Þ@Z’µf8 ¤%&²ú¿~Zô|ôò}ôîÓŸ GßËæuS,ŒêX-|ýÁ ôîIǎݹâÖ‡Ù_â¡AZ i¶DV}>“_ø‚³Ž×½•={sáÄ)l?å&ËfÆl4cóTðÞK1 Ç™tìØ…>ƒÎfÊÛŸa¶Y1èdRRS‘ÊŽðà]Uvé\æü¼:)©˜ :<ŽrŽQŒž;ßYÊ岘{ÿ~9^I¦Íâþ”"'GŽº `ÔDhîàHÌX(.IêÌ©ZصWÕܰ"øÞAÀO¨Ô×ðw?Ó)nzÂۆ‹ƒ ‡U™I)ræŠP/†–Xþ°ã+BÖ!Ú3Z>b)J¸†¤2†j)™BCÔâ­U üo»×)--ÿ„¸ÝnqèÐ!áv»ÿÞçx½bKnž8RX(N—ˆãÅÅÊÿEÿüY\,Ž—|W"„bÁs Úuj!qF§îƒ®~NyPEÛ¡©Ä9—ÝZ¦ @<7g£BˆÇï-23“„YF`JY‰Ì¬ ‘Y·…øñ`¥¢@4aë5J8„ÅN¢BôLG:]$\ÊCÄÅÍ‚ÄlQ¿A² ¹±èØ&Sbî^»¥»DˆÖg™ R7 2u¯.Ú+„ðÕ~߀÷>YáÂS"n8§•Dßá‹I×-Rub൷‹2ŸOTz¼¢¬p¯èÔLy¿KF].Zf UÌüí€Â#Ž âû× @|¸þˆBˆãÅÅ¢Ðë…9ÛÄ9m›Š$›,‘(gˆz눴ÌúbÚÒÂ'|âxQIÈø(cW$„¨8ºJÈ †ÞñBˆ!©ÊôÜP)DùŸ >ØU&òÖÍÆÌ–âšI7‰‹Ïê)ÅÇë Ÿ_?~˜ñ€?B5A 9w¸¨“¨Üë‹]ÅB8OŠË{uÍ[5RÆuüKB!r‹Ut&°­~ýÉ)*n!ÄîßÞÙ²rß^ƒ‹®Ím_Z#„pˆ3%„¹^[Ѹa² 1S¤X•k¯ñ¡BˆW&ž'ѯ_ˆºõ•1Ïnu8ZâB1ç‰k ¦ÿ°_!DNAau»òœn!ìùúyU~~¸Z!Ä®?¾€è?x¸0€hд¥ÈÒ)㺺@ˆ-ŸM€ûüÿX*óbÑË· @<¿¢@䬜#Î8£™¨gUbN>X{°zÜUÍ£ñm;µTæQge ¼jºBˆý~*ê™&Æ^?Y4ÏD€A<>{½¿Ÿ+}1h¸0µ3E|·µHx…7`lJÂèzI°~ÍýráBÌ~j¢DÓvƒÅ„›n=º4ÖúнåBïI1©_saMSúÐfJõš4i¢i×qbW¾2&»{Od+¡ž¢gÀ¸_ûäBåûõß@ÔoÞAX@´jÑIXAÐî!„S' i©éBaL0ˆz‹Œ¬tQ¯ùP±6§R8|®ðzXT,òÜBû~ÑÈ0e4 SÌ"±IKÑ:Ë"@/~ÚïB±îÃé‚:íÄå£.WÚÔúJq¢\bË·jÏ#K¢øxý1!„/LPô³¿_?ëÕÏPô³åâh™2Ó^¹w´Dƒ3†Šk¯½L"¥Y±%GiÃsW $† n®ØÙŽm]p‹(BáOO<[¢õ  Ĥë¯]›¥ˆŒs&ˆ½^!<>Ÿ(/Ü+:úíÒ¥£®PµKÇ‹‹ENa‘ð!v/Ÿ¥Ø°ÞBqRm.醚¾”¨¯'µÖ‘þ÷ŒÐç„¶3TÔ~îyþçœ(.Q§¢0}®ÏŠJjî:׊Âë¬ê;E}—’ÈcVµ~Ei{¨ /Ó–¢p}XA_Jj¿CؾŒr]¸qVi߉âbq¤°HlÍÍn¯ï—UIii©øwÆtVdz‰ðq AñÁÛ/`ÉÈ`ÇæC|°ø[7­¡)p¼ÐÀ§3îâíyeá>}9—õ{ 8¯CSî¼—@&=ÛLv¦Í_)L¦aóN`?…§N;Næâ‡wR6 vé–*³{é¯4Ÿü2‡°gÓ2ŒÀw_.$tRxúßdÑSvd,ÞC½!W³òÇù¼÷îûì-ô0çù‡(.«Ä¬“™qßX6(æ›M|=÷Köä9èf-â¶Gž  :)l>kË…WŸÈ…WáªáChsÅ%Ls=׎½š3ê%bw¸—lÜéða©×†u¡0_.›Š”ïVÿ^ˆ½hº¤V mmå‡7gâ^_¼ž9ï½ÁüŸVs2ÿý&b ·ýÌ•w?ƒµóöóóÂY2ï Ê óÁ˜Í‡lbßî5t׃Ëíõ7'Âá¶Óbµâ³á’ ®ã”å ~ØŸÇ¿üÂÏ?ÍS.õúM:%á8¾“ÄF[œKQþ:fÁG΢hÖª%+W–òÕOÇ8ž“ËŒ C9µç{æ®Ï`Áâ¥`lÃEC›áò:•Ø@¿Î¤õ|ôâÝŠ~.ÚÏ¢/?eýžÎïÀ”»¦P$gdbÒÊ_2êùO9z`«—¿1ë«tù]ëÀÇO?ÌI¤Y­@=ù:–çq}¿4l=¯dëÖý,úüÕêPÀ>ò–ô:ìÜ|P™G®¥)[¡ôém—Žå¸· KŽžàƒwÞfßñ$à•·_Ä|øÑ|Z¦ð9xßåàq"KÊ¡•¨®¦€€ÿd½ÌÏ3_eG9<ðÙ'œ×,7PìT.ÒIÐSr¼}£3ùnÙ2u€¾!ÃÎïÁ–÷÷PX­{Ó¾ý™ËΪÀ¥“Fs÷ìŸØ¹#†4Â]’íû“dW…³¦9 ’¨`a•~nùç6»1›‡ó* `;GœPß,pz ÷Øé|tÏU³Ï@ÊŽå]™rÿ8FÞñ³~ÜÆCÁÆ^fMàÞŸ" 8a¯ÄšlÀ)DØXwo¥2noc‡5¥²B™G¶>#(9µº<éÎn`$¿¬œ [ÏÞ9Ž!·½ÉÒmå\ßÈàoçó|t¯ÒNSv;$ÀY\¤/"\ýí°qWáqâ#k.¹Š_Ÿ›ËÍ:qóMw2úš´«›ŽÝQF^…™qw< œ`Ö›Ð÷òûybrç ÷\:ç%v”ÁƒskƽÈ!ùUCi‡^v“WWè—€IDATݯxšS®Àåõré-/ÒºÈJa¥ƒóFÝÌy£¼ü6ãmRfêÔÑÕÏÈ/-¦¬t²v>+¦NGá‘|L-z2õL¤tͳÏõ¢]ìþ㸡•^{ë *ì^ì'`ë²M0¦ ƒFƒ¯Wq}ïV¬¸ùz®»ö:ÿ ‡8Tr[Fªøn —šÔkÛG;Uî@áNç‹ZY„æ·Z9×Zñ˜’zZ¯êôY¢f†-*¦¾yÔcÜÙ’J=z)œZ„ïÛpýR+W¬Z8ˆ¬W¡5ãóüª¤á]ꑲºüÊ¿é¬Í5§°Õváòlùÿt;”…|Ð^ŠÑw&0îÞ»snðRQÖ¶­\À’Å‹˜÷ÍêwÎ%WÇìµS逓2È®2Ü@AY)'JJ¨tSsئ:Þ[ è]^|´£´šw£u”V”’Òª÷ß{íS 8Š )ú^}5¹n0˜ôdéAxõµçgˆapÛí”êm¼´xO=p¹ë~ᆑçÓ°Qkž}÷’tà)+ ",ÉVV·„Ÿ}ÏWß-¢Õðó5öRŒå^œ>–q–t2’Ï8t,Oais+JJ(vzÀIÍ©;^_c|z5hƒmëLñ=‡ßÃÔÛ.aÃGo1óÓ´mÑ#Ðúü ,ûy>wÍæãŸd@çÆô½ðvQÚ·îÀ>0tcH»$*=È€Ù¡Ü_ò³n—_PhŒPgËC¦’صf; 3tPs@à’Ln?ᥥEÐ¨ç šë Ï®'·½HÄTT”ÎÜJß¹Ë+65Ŭ,¢’€ wMн_t=xЍôëçöß°dñ|=õ: ç’«ÇaPQª0ö#F^‚Â+Uo ðúøc¢ðܳoðÞÏ‚­ “GwÁáVNÈ»·ÛS‹ý©ê£ªy4pHOe¹”ytYß=º´LS€µÇãÆd7Ub~sòË‘=•þv^\ÝÎD;=RM>RáÈ:=f³µ¢ *_ÙÕWá`ü´™|=ûišy÷ó̽ãi_¯ãz…2s")f=E@Ù¡Cø{a®¢¿Å¥œ()Gv­Ù Èœ5¸ ðÉþq¯žë¥/úô@^I)çσ7\Abe%§\ *àà²+`=·¸”Å%¸}J ¶ðåýj^°¬w@C ¿´„Fý/dÊ}wRW9»öú_û‹\ÂßËŠÅœ;|Ãz×ÃéóÑñ’ëüó(‹_|ŠÓïÂ[ØqØãß(–† ÐO»¢Ÿ¶ 992Sü:àTþ]¿M(¯Ä8ìE µeØàº8<•x[–>‡ :žõ=3Ÿ~ýáõ<6þJZd7æž31ÉൗP~Êo—,aéÂ~»t>£Æ_С܋Ãë ˜»2àÃëô€^¯ ¤àÔ‘ò!þVD㕤ÈàH œ:Rx  À¨œºŽk(ÔcMÃÕî®b4¥0@9R<£ÚéðЊ0g-ÔNLJ‡ª^%Õ^àñ.UfT%5’eŠÃl±¹aKŠ¢’q‚Ú9žUKÛª÷‰TÄ?Îtþ³ÞõÐOØcø“l’á-+*¬è$/'JJõ²³-  oÅwß,Á Ø¡ú´rAiE•,‰|ÎJJÜz (¬fzr’ò§W%¬X‹IOÎîE¬Øã¤Q‡”(£Óƒ»;`•$Ü%œ`IIFÖéÐ΢ÊêDÔNÀ;ú$…íʶfóдW¸ë¡Çøõû¹y{ª¨Ò ‰¦šéœhU¾5ô8\®êt=ÕvNRÛµ ¼R’“î]Œ|¸ŽÃúPòÞƒœ8=÷×6oÈÍ÷ÌäÛ|3ã¦Ì&Qq 2`ÈÅ Xu1{·þÁÇï¿Á“/½Á5Ölþœûzƒ|Nt:°øŸ2ã…ו6ûO‰WÛèŒ À³˜ ”;\ˆÐ¤÷RmúÌçU,ƒ[$ À»/* Íj5)FO£ð/̉ðä·[±t¼štÀQétx ·Ð_Ïü€!=šQ§ ü´šS¥e3Sê¬@¼n$f"Y, oÍ7ß,Á2f`w™€J+J«uG–P`±ßøK)-¸gÂYÜ5{/½×†YëÊñØ£4NÚÁ‰“s‚ò½NÆíMÀ?ö¥ÅE€½Y““É̬Øœ³_a0 FŒÀÞûhÖ0 —Ï–úSÉ?t~ýUÖбÙp:<¤¦$SX\\¤ L~!|x<>d9KÆ?È%ãïgõÒE¼ýÒc|8í’êvàÕ[†P¸…À˜\~€¯7ì?¡ívû —H$,À;/ûÇ=Ñ´2”•”)¿—eòKJü‘dåÀšWàö€]RúOÒéHµ%â¨,§ÜáÈð¼xU…È:ð:Jqøûª²¤”JiÉÉd4QB7Îý¯Üx&NÿØüÛ¢#%d¥§VÏ£}[ÿà£÷_çÉ—Þ`tB:~þ&½²t¨égÿÎõ‘W+ËJÎ!7Ô£ÿýwm<ýIôÊJ!;1”\S^,ÁœÌÄŸfÜ”‡ùuá÷¼øä]̸ç:2»œÃ”!Íð%®…º]Ê-*¤¬Bö3ãYŽ£¬Ý\FzŸfÔ1‚³Ü¼Â‡zª†EŚѪao‹HøDpe…;ßd«•ýk0å…÷Ø•gÇ’˜Æy#oæ¶s.¼Ì-’˜pÝ¥”¯ÿ”Û_UÜp ÀöuKyõÃÏ(“H0è‚”©¸Ô Bàö¸Ðé÷5J;}>µ6I¤%ZY=ÿ .¸àBzí;m‰u. £uOÞ(…>ÓÐ5ëG§†àÃAƒÞ]'/½ô3¬Æ´ìЛ©/ød%zÉ€„OÑy/F®»n¯a¢_?€œ=ëxõ9œt@¢Eï‹ÝʶAòd®½ÿa²$w]+nC+¹ñ*¼ÂO¥R†«¼Rq©;½5‰ÍýóH„T¿©¨t‘ݨ;czÕeͧOðáÚC¤Z)>²™û¦½]¹¨uE¥e~`–ªÛé[0šÉ°H¼ýà8Î8׿¬ 5ÉŠ.S°Ðm6¼9[˜vÿ#,Þ}éuöy<õøÍìÛ²ú§ ‰É$K—~„ȲZزnyNèØ«UиϺ"w¾Z5î†À®PÒ€®ÍþDÒ>H–4“à_¾â0iKd×–uì?V„5ÁŠ%Á‚ÙbVtÏâ×?³“ÙRÕ‡/dÒ—»íûžO÷LxõÎ1üq¨`ÀÇW_ÌåÓÅÛɰʼöâ³Ì\°'ТCo¦¾ð€Gv`õ€›Jÿû,xõaîÿüWšž{+}Ð~Ä¥$÷Þ|9@†½t'Kö{>v4zÀîð"Øø”¶ZSŒäîÛÀ=¼Â¦£èt†^pÝ: €=OÅ.­ µKKxõÃÏ)•ªc6}Ȥɰê»YìrÂå—\A Péêè/Rl ÃVE*С–KU’jÁZÏ•ÔÁFèŸáÀòiÐ]¡ñŽR↠GáYã궉’ª"¤b àŽªrê>‘Õš9@ ƒ4^Z*  ž*\¼f´Ê„H‘SM^®XCtþ4g†Õªn1éoÍg:Ài?¥,¤îÐ¥ËG¾ÃÅù“ŸçΑg3wú$2¶ _Ÿ3iغ'w>ü9fKFÙK¸ý±1¸vqflõëN·3:óõ†c O军&áܽ„l«•Æ=Χç9WÐ&VoÞAŒ;•c‡ã§Ð´-ùÎë¢È/sÔxkô^Nxàxž½vØJHð¶É` Ñ{œé÷^OÛ¬4:õîG·3šsõÌïh:è:.?³•/×O›ÍÈs:òêíƒiЪ½ÏlÏ=†ñÐ'¿aI1c”•>“õ ;s]¦$%&òØó ¨cSòfÊ€×â*«è«  c Á gÞÌ·XøÃ÷<ûæ»8ô,zt6È®K @ZwÎÈÈ gû&œÑ¶`bç¢W¹î¢Þ4lÚ†>•Ð#º$¦Ýq>!wë´¶ÂŒq½HHLæµòyü¶[±WÒvÉh†ß?¹kbãïŽd› ³,#ãÅ'IHrÀÿ:ðø|”#|Ýí noã‡ço$11™‰Ï}­ރ Áï«6f·4shá»ÔmÖ‚„¤¼ýýv®™ú÷]ÜIaÆ’ëœÛ&“:éI\tÇÓ4x)ßÍ›Þë¤Ð-8kÜÍtL‡W.EP7%¼>„ÏGA¥‹ó&OçΑÃô³; [÷àÀh£¤0åwýóøõ©B§2»‹ô–ƒ3¢™Â²^wݲ¡¨¼¢àÈ€Á0Çõi…51•ÙëQÇf ?$ð:*(&^úü+ú5•׳)=û µqgö¸0ç«ùdÉPT¤œ+W8²ØN…á—ŒfL¾>}ýCÖ­]ÅýÓ_ã„WÍ¢XÔ¥Ú%…@¯×“œ”À‡Ï=ŹmÑ¢cwúõîFÞã±dœÁ7ž‡ËçÁQîÀœÕš'﹜¿Bv㶜پ=z\Æ^;œ}Ëý noãÇço$1!™IÏ}ÂmÞ­Œû›pìVÚ[èp©†kxåxôÜ÷ÒÝø­ IfúuoO—N=ÙRì!ÅdÀëñ"Ir€þ)nãêL;²‡8y²CˆðÏgEX2ç«/Hwï¦OÓzõD«:©\1òjþÜé$Á”̮ůsÝE½i2ž¾û>…ÉNÈ*9·MuÓ“¸èvE?Ì›ËåÄÖx(_¼÷Å›?£aZúuiÃð»^æŒa£˜ùô%´àÔ Ÿ :æºÒž‹'^½«ÑÉŒ§î K£ ÎèÖ‡ÞÝ;ÐkÌlÒÛœÍmwÄéóùíÒÿcï¼ã£ª¶¾ÿ=Ó“™IO„Þ{ï A)"ˆ Øņ]{GÅ.6ìAPºˆ€(½Kï%HO¦—³ß?f’igf½÷úzg}žûHf朳ÏÞk­½Öo•Ý7' ¯YzuoCÛîùôRª½¼^Aí´pœä® OAr+î?—ÇZc&℃ã6_óßhÖLŒ;ÑNNŠúö¸§(ÂŒG!¢œN+tL¬Ó~¤Ç׊H,çoÚ:Iª¨¨f³ù/Çã!??Ÿ¼¼<4š¿.ªï‘eö—¦Ñ S©Ãa~)FnŒß£1è´äïýåkÒÿ’kh˜¡ÆîJ²,£7¥“¥•™;k&‹–ýN…SM³®½;v 2 ØÜ.„¤&7ÅÄÚeó™»t1'θèн?W_3–4˜4^¾ûr«7#­M?ž½órÖ-ù†]•f.> ™_ú†SRmÆŽn·ï7@­Õ!Y ™={!uº^@ÿvõpzAO fÍEÛ 'õm‰Óí EIƒæA’Ôtöo^Å¢¥+8|øN)™–}sãø+¨%Ù)±¸0¥¥’"ÊùúÃü¼úOl{dôeS+YÂátcÐë8sd KVnÇRZˆÅ%Ó©ç¥ íߛӉV¯£üÄ>~\¾–nޤMtì.WHˆÆ””ÄžU ùlþ/´¼`$×_|>»Y¥#Y²±è›o±×kËè~ݲÄñ¿³âÏC w 9 §¥ˆ¥ æ²aÓ.Ζ“^· ‹»5ãLi9yé©8ÎîáÕw?å”ÍÀãÓž¢`Îctó,_o;Í•r)°Ø1`aͲùì;VJaY%Í:õâ¦QCÙ½ò#.û vI(ó;".Üùèž¹ç|ŠOýÉ;3g“_â`ðU·0¦{c¾1OË~\ݧ6C›ä²!µÆ¡¡ÛÑ\Ö« åV©ÆdfÞs5^ŸÅµw>@½=µZtæò«FQK-sª´$µÓRسìÚ^t Æ£ùí·/éT/ ‡ìàl‰•äÔL²´2?̚ɟ§ÂQÅŸ—Ñ83‰òâcÌùiŽ }ý,\€d)dö÷ ©Óãú·ª‡Ýé$)ÉDÑÁ,X½•^î¤m-åNWµÓ®Óê¨dûîCÔnÜ‚‹o¾‹!Ís(±Ù±cî‚t8‚õ³pI Uœeöœ…Ôé9ˆþ-ëaw{HJ6Q° 7À:G/öûº’2›;dááNIÂ`ÐS|x æ-dç”ÙÜ4jÞ†+ÆßJ穜)÷5¤HHiΗ¢¥ „§Â5›?—{Öt.DŒùô#¦"^N¥LÃ6JS Æ2÷1K¸½2å^/-³2ÑÄ1<ÿSvYUVVþNÁÞâbR5jt*5²±VÁõÊ2ÆÔtÒUPn+§Â!£ «rBF¥M"×dùÜîuPRn÷ç1 d¡"'=mÐoJ­åX2jƒ‘ÚÉ=Š*+Ió#ƒ'KJ@^F:*|§p)p\¢eÐ&QÇlÀ‹‹S%V4*ð-õÒM€àDI™oÜJ':T…S%5Yiæˆz,N+eV—ï$YF›d&'IöåV_»¯×KRJ:™A?qãåLIj• ¯,£7¥‘­“°ºl”T:}DRÀôÊšž†_Ûª³ee$$!ã¡ê½|§5 µê䞊2¬A’)•,]$xºÂBšAæ×ß%£ÍŒÞÕ÷þgvÑ®I[vi»qäärtvÊ,¼BC suxV%6;ÅÇ63ó‹EÕáÝ$ÑsÐõ ìÕIo9½ét™…Úi¾± ËAº™›Q9î^öÍš^ý›‚òrd¡¢Nš™7'cò‡+8,4ò_n³`qxª$j-uÌÉ,øìe.¹áA0ôŠë¸ëþ§éß±%T:eþ,.·£1˜ÈMÖbwÙ)¶8PI 4òÌIxqsªÔ‚FRá‘eÌé¤Âr‡¿ð+à€i)äèH¦Ía¥Øæ3LiUrTA…ÃàG¿‘å•Á”šFš:X,}Æ‹´z3µ’5ØÝvŠ*¨%Zy¦$dÜä—XÐH†ô4ÒËÑ8£)úqO°óë')«(ÇŸjÃÈÌéi¤„}íª:Jå“;á•‘tIÔ›Ó‚’RÜ€9-´ u?Un¡NªoÝO””¢Õ›È5jq ¥Vÿ\„VÁ !ƒJGÔÐó¬ì‹šÅº­Buš_´ÕÚÚÜ0éFrRud&ù¤ùdI©ïˆÊªFÞ’on½B"--5â䤳åe8eARr*YúH9*(·›jâÍ Ã˜ñ‹˜ò¥8O!I$éô¤'üÈsYG¿§¤|Þµ?äz ;B3Zø8V?ÏhgÇŠÔE¿hañhp¨ŒÇGÖjˆrÆ[Û¸½.cÉK^’ôµÒÞ[ƒ´ E½w.è¯Rtu0/ÄB…ýÚÑ-Ë”{¼´ÌÌ@£J„×ÿó9~Ï7e“¢»Á¿«Þ”¢z\aĺ¼P5PL‘Eä—Q‘¨‚VÏŽ 6ñ Q¢lÂRMB+1Œú(-Œ‚×Rµ.‰ZÆÐШ%e–ÐmD8DülI)«A˜L¯¤¦^ZJuøSæ‰zd™”ŒtRâÊ2ìîà†àA-¾‚×@øÐL­VƒìõàöÊÊ6?qŽXó¯]G¬¼­¸ˆQÕ3˜.lcª*Œ ´Ý’²Ic Ûœ„Ãi÷¥|¨¤øhqÐÚš„Ïâ…L#6ÂrÕ¤Ðâ ©†2XÓÄšlà"”µ„’Q-"ù³¤² ›Ô’)ï52ªâDuj‚4Ey7!|íæ´5^·op™|,£¢çYF‘ûð%ŒËJz_ˆš£`RôLŠƒFEX"H¢&÷F¡=Ñ—×4PÞ‰*¯ P4€&–£x.ù—ÄæÉ˜@”\:ÛÿÏÒ™ù·4:ÿ‘'• ­ˆR†zÈÕ½É{·oÂ"´Ypƒ*åªÇpO9NØsê‡!bÝ;,a›n¢’ꡤ¢W&¢·©I"¦!,hlHˆN…ìvrºÌê¹J ÈGxÑ*žPj \Ô&h !8]^îë’ V‡ F¥ÂVVŽÍ¯øÔêà9­j;"E4±ȸ\®H É¥ æç >Q*GE”3û¢¡)á¨x´ÓN¢ÆæŠ|†Ph9!I*_“÷2_ašÏàŒbÈÅ3¾cµ[© ê Í6_DGè#ÐX¥c|•B³’PîõæKHþiAYDÔB’jþôßG­BáˆÈlÎáëÍŒf„ÇÒU"%{p9=a[dªŽ¢þ“ÍÑR$80Ò 72â…ìk¢ûâµÍ H¢PØ«ÂCøAûœˆJ?'¤ZÁåÖRá¼Õ!‰rr1Ð@EÄXŠ"sakX“æï±BòR ×,ÚÉQ5Ùüâ¶~JôéüOÙœ‘Þ£;QFD1« DŽ3‘„jÌÄjúPÝMR©Ÿ›CÕÄ“¹pqCTQ?ÑOkqÆ~Nn¼AM~õº(ˆ®¨Ê;Sº…TSF$8ó Y£( ¢Cƒ8­K¤Hå"ÕpÂ&W…èA_¶¨hœDôË…òùÈÁ†¸ˆ…'ÅØP¢)_¥ãóâ¡cŒö}±ñ]°q!ÅG ßÀœÔ˜¡Ç触…„q%¢ŸzR(œ—«YH(u,×J} ¥xÞ_8ß(!"¾ìãƒxS’¤8ÎAç)VÚCP.”Ç!…! ±ÎÛ1x Ê8 o)¤pÊW¼ñ*:ýQC…õˆu®y<9±ç[BŠ.¯±ÖOÄJM‹h· 6V¥(|$öAIRŸ]yÅB‚%)¬¾¹(Ú +P>E*8ô7§ä‰Dj ´*ÞhÅaüâk¬ªÄü*Bά•¢1–ÂßUGÕåªÀ&ÜCT(³(BÇV-SR°Áà<¼Y†#!… zVÙ"¢Z÷EóÔƒÁ”ª³îÃs2U1ä§ÊiUùï«Z3d :,D†ÃWc> Ñ÷¡Š¢¬ªç ìÄ3%et$U=C„(ªjàDÑÑ¢('I-”T S}’UAvÖV‘¼H •”lÕÉ7áùGDáSG‚ÊX¨•¤Œ¦EÈF ¤Ä‡Ô‰ènt´&ÞUü[Çë—£à^ | _CH’TD³±ò‘²D:ÏUs^U¤`¯¨Âï!…²¬ ›¡AR V(t]Ãù¸êæª(Tˆî A G>„÷¥0¹—B ŸÏá”y« õVJ!’$¿¼;"<+Å7¢¥p´9Ô^W42T1P¯h=•ø:Ä”"uªKýªÒëaö’ñR]_ýlUØËE×Ã! bMó.¥0>‘‚nÍg¨JŒY“”£1Á:.ˆgƒ3ëóX%Ù#¨ƒSõï‚/­ŠD(Q‘Ÿ[%oA÷ ÞGÂ¥7¨/øß"Ú3Órÿžè?Î蔇×Ëi·­Zz2QÈÑd¤ð°Vy¡M À„æ‹P¦"´XV¥“¾ªÂ6Ñ=)L$«PØ*±:¿­ºðÉ/ÐRh TH¡^xºˆÿ¥«>ŒyŒ¶×„pHŸ:B YªT·@øÇ¤RL{Ù#Âsrƒr/«ˆªï«ðM0´”ö%!‹ cMD*¦àœÐ +F@ŠuS>EèCôŸâ,IŠÅ ;AN;~Ã)Ä¡¡ÆBð~/ªúV™uÕŠ8<Ÿ9\éz ©j^á_)¨À©š‡¥Ð«dDÀP«®Ž–„2(ü¹¨žó ÷¢#ž±˸rLDˆq§ˆÓ„ñ^¤m$ÙÕÝ(¤¨Á…*‹KŠx§H‰(Uz-Øp¬æÇ cþÚ ª’\•™ªëÂŒf)lGÂoÜŠÐñF¾£¨>ÉF„¥…êƒx)ÚRÍÛ’òé/¢j=‰LÈ~)ÈhåüÌjy ’ý Ž{FDȉr$ZÏJdºx›‰j‚Ð}‡0ã›0@O±0Þ?Æpáˆ{ØZG8D‘!†P 0¨ÀJ’¢³"u©±?Š0~ L”¨ÖYÊY\Á~.:?„ )ð€á.…ÇÇU9GBá Âõ¨6hN`¤*2‚÷ÀÐ=/d_>â ’ï„ÑùA8%š¤¥â–E„àÆ=r¿«Is²h±¿sMV<×ëâ=¯&%ÅÄyvMz5Eû]MÇ~.s&Õð}¨á}ÎeîÿÕ2ÏsáG©|U“Ïcý&ÞŠsüݹð£¨áÅ© ©ÑGƒ³k2†šÄ³ÿ•u?×y­IUÅ¿ò¼xó+!ïß þ•ûœK¢ä¹êßxÇSß ¾5Ð#ü ú£&Õ+±Þ³&{…8G9=—Žòÿkúïè†X²/þM^ÿwd'Ši/@«’‚ PFç_Né JP‚” %(A úû*1 JP‚” %(A J JP‚” %(A J JP‚” %(A JPÂèLP‚” %(A JPÂèLP‚” %(A JPÂèLP‚” %(A JP‚Fg‚” %(A JP‚Fg‚” %(A JP‚”0:” %(A JP‚”0:ÿ3äñò/Eår‚ÍðxðZl‰yü+Hö@¥¼¢ækòO[ !ûæÀ#'øáß¡"oü#I€Å.Ob*þSd·ƒÍ…×ã‹ýôsB'%èÜH=eÊ”'õzý_o;È2•••¤¤¤ RýŶ®×ÞßW³ië6þX»ŽÛw°uóVœºLêæ¤òÛœïÙcqÑ4/gþZºô†U×Þ]òÎé1óçÍä±ÙÒ1©‚Io¼Cݶý¨—ª ë7n¥µRôœ>q˜Í §N´Uç¢Ê^v®Û@‘P““jþïpµ˜u[¶’œG’æÜ׿Àþ­¬Ý}–FõrQÿ÷Z¼ã†=ò2©{Ѫ¶)ÒŽ(>Á¢u[‘RꙬfÉOŸ3á­Ù\0øRµÿŸ<¶›]GK©—› ²ƒÛw°}ßAvî?Àž£Ç9V^‰IgÄ”¬ûK—ÇQ°A÷>‰ËÜ„.Mjýã”PÈ<ÿ…´ä§Ï™ðæw\0èßçÿ_Ék/bÉoÕ)d¥&£·²vÓ6ÔÉY˜“þÂ”í§¸òî9(¥Ò·e£¿ïdZ‹Y·uÆì:ÿ’ÞüWðkÖpÌõ²ÒþOoýÁGÏñÆV;MÊOsç›3è2`º8ïd=Bÿ®ýØnÉaHŸÖ¸ ðã¢Ux3ë“cÒýgÖ@.bÙÒÕTÈÔÎ âgWï¿ú"/L›³=:¶ˆ¸ôðÆ%¼øÜ ¼÷Õ·ü¹ç899MÈÉ2F}”Ûv–æ/Æ-g“›cüg`:ÿI» p¹\ÿT¤³‚‡F`Ø¥£yê™§y챩L}üQýqL3–IÓ>ñýÔYÆŽÝ»ØqºâœŸâ´XXWPHie9{JŠ l#sØÌÏ?϶b/+~ÃЧ^å¤=ȃw[˜úü³<÷óÚÿÚlmYùW¼ø–Ñi;ÿÆMÿ «ÿµNŸ:Áá³åÿ'cs[,¬ÊÏç„Õ¡ø}é¾ \õø3,Û[ì3R+JøíÄ)ìî×—yúåydùf¿åwšû^ÆÕ3?çÝ•«xwáLzæIß4‘·ýµkçu[Xuò$‡Êíÿ_JãÙâÓì;YužŸ žç¿ŠÊŠùíäilÿȱóì>®~ò)f­ßÎמÙü+£Ÿ™Î1§øk)ÛÙ|ò{Ê*ÿÖs¹uå<ƽø•ÿ)vò8˜þÊ <>õÿù­OÙ+Xw¶„ŠÒR¶Ú &ˆÇΚ?ÿdû®”l_ÀèË/ã›mgþCRȤ :pÑС,=PRýiåÑÍœ_/‡É,¦Ür’»nÉ•Ï}Dð2½öð54ë>”Ç+IvóÆ´'ùjÕÞØ²sz cÇŽå£ù’ 4ÿÈ·R«(¶CÓ‘“80çä#ë}ž¢ŸG–bÔE(¾Â7ÙY)Q“'ëÉÑ Ô&=ÈnÌI¡‚ºrýz¤´¦ŒhŸ@’VGŠÑŒš 8P%‘l6’¤Ó†¬Å…xÕI$¥E¢{Ö3§Q%gdŽ‚P[+°Ë’ÌÉþ¿Ë¨pRÒÓ*æ¬ÝJóîP?I­`ÕáÕ¥¢6úÇg)ÇæT‘œ@e'_÷F{IÕ^žö[ë\̯]ÙY(¯p’š•ªšA¢*­–¬dõ²³°œ%97§úûìÎrdfRøÀ$ž£Uð{\xТÑ>sÁŽž´(ˆ\´g½Üzc¿€+cu0fôu¼{õpð¸È?|ˆg>}›;Ÿ|Œ$Ó[Üt~«À lå—{ȬíGï¼PG·àïlå””{Ȩ@ý$µ–t“‰]Øõn;Å…edf×môÊVl!¹VVÔ9¶œ!97€ Ú HJÍ]¤Xš_@znPGó÷ŠqaD—b¨þè£Ï¦1íHÊÞºGqž7z™4¾ÿ9‰¸ÿ ×`Le¥š†19‰“µã I‡F]Ã+¬OÍ)'åå¤ÖÉù7PÀJì6AR¦_/yTœ­$¥väúÊ奨<¦Ì'×í¡™ïcÈ­§0©.,g*0å*óÊOë6Ó¤;]s çà1yAíŸÜ²bÊÝR³S£üÖŽÇ&Ðhtd™Í¤„éDÙéB¥¯9‚Vyú4Ii9h’Ôç¾TEgqêS£ëXàûu[hÖ} Oo2X)PùõPeáY’i¨#"%2e§Ï’V;7ôc‘w_{ orÍxÏuö,º´œ€ËDM¨ËÒi¢Ó`4ƒìUa¨‰Ñ©Ò ¤§øö™ÜóoãÔÑqdÖVŽÆX,NL&½¢“QXâ!;ë\"}…ÜÔ»¯µ¢NoϸþMü:±ˆA=z²!©%ûW“.ÁýÚ0ýÑ×y{êMd3¾{§}Åû 61qx¿9°›OL rêÄq̵”߯¢ÂNJJ ú_4:7Ó„ÓcáíûÇE¼zçõTGEDÀ{Ÿ5í¦¼óå67­c«°©7ï3ˆiíÔ´h•Í«w¥gP‡ŒaÞÚ´í<œj•/©P«¤夒T¨$߇¢ø$/~ò1?ì8ˆp9©Óór¾½û2 >k€Ç_}“…;㑌ÜtãMÜuaVÿ2¯Ö¦}“L^ùì;nœðîÏß}Ä[‹× 9ܤÖmÊ'O=J^rв;NðÛ‘2FÝÒ£j‚øâÃר(5¤©±’ç-c]¦M¾…ÜÊ£LzëN« Œu-^~ –o_ÉüÉ|x{}–üôŸåKÔrm Ûø?˜xÍMLØ…eKfóì÷KðXÈ™õyïÑÇ阛ľ_—1mézž|âŸò¶Ÿâ֧ߣÇE㸱k$IEzª‰Ý»VóëœU,Øpúñöm7Ñ$'Ëɽ<2÷W.s+C›§ø—RFcð½ãû¿ÄœÝnfN¹Ÿú™z\¥Gyäíù}ÿ $IMj³.¼wû-4È]à?7o¡ÏdLû:U‹‡$Iè5þßitä5oÅ{=Ρ#xbö|Æ× £‹}É3³–auyÈë8ˆ9Ž'©â#}‡«¯¿‹Ë»7ö;§¸ñÉ×è;b"7ߌ%‹¾âéYK±¸<Ôí8ˆ/î¾Ì$•5ˆ ¼ã•?Ïá©Y ñº½”ëR˜pÅõÜqaWþ¶‚WWn c«F,ýuÛ Êiݱ?ïÞyyiZœgw1éÝytk×™#{ç»õ{èØëB^3˜…KgñâòÍdÖj΋·ßÊv }(ÆáíŒëCòóK²êòämw3¬m?¿¼ÊFUZ§9xoÎOØÔfîºî6î܉u+góÑšÓ$åxèpó-\qá¹|p`ž·lÄ¡ËäªÎµÁqœ+¦¼ÍecÇsyï– [xþÅiìKíÈÇ·_†X¹ôK>øÃÆ—OMÄqr'W¾ú>ù'Š!»OÝ~7ÃÚøÆôåÌ7Xë®E]ù¯.ý“¹ï¾Cì„,£õ‡†ßûøeæîvñåÓSÉöìãò§Þâ¬7“^KzZÎÒ³tè2ˆG¯¸€Å‹¾âÙo–bqúÖµj}l§wpÃÛß3îŠÛݾàáý·žãpFO¦]í»öø¶UL|ÿsN•{hÛ¢µÌÞ´NL?,ª¿PE«™Ëçòܦ6ŸÌÿ‘½6 “¯½•‹ëk¸ïÝl9a¡ßy1}âµä¦jÈßúw~òÅV‡ƒ†].`ú„k¨kVá,?Î#sçqÑÀ±\Ú½‡Wÿ‹+62¨oGæ-þ‰?Óó¼a¼rË5Ô56m?F¿¡·£~˜;“o¶¹øü©ÛÐK°oíÏÜùÑ2î¾÷~†µ®ŽÓLzfýFÝθn¹|úõ;¼¹`=’W¦Ã€1¼;qz”îþ‡¿_I«–ÍùyÙwˆ&cYxßL:^Ù§m§wpéSo2rämÜ6´3³g¿ÃÓ‹wQ?+­!™Ìdå6ÁÔ[î¡Sžwá!n}›ÂfÈæá[nã†^-øæ›7™¦ŸOƒ(ús5|½Œ;n{„.u“Á[Á;ï¾ÍŒ?v‘’R‹n­êQb‘¹rÔu o“ª7—1êöóøpæs,µ´æ»{.Eüòów¼´pÓ|¶u’ðìáÆ·¾âî;Ÿ %G™øê;:]F¥ÕÁÍoãž!¾ûÜÆÍïÎäĉ2š7oAz¶ áÍfú½×„—Y?ýˆ­EOîÒ;’QgxèÕȪßË©C|·q"³Þu7×´ÎãɧäLÃþ¼{ã°êK>yç9¶˜ÚqÛà«é­6Ó:SÅ SéêsÙmýF×Á_yà™/1åu®hŸÅÏï¿È«ßïeÌ¥myå™çÙ_C'ÜË×o?JJØ^[fóÐzÐQ÷ÚP*çÊŽuX¢½ë‡ñcQ}}¼²êíÇÙpÖÃû~&Ý¿ÏömÛ•é‹·úöгk¹uÚgL|e%‡w¡ÒîÅœ¤F­7Ïäµ—ìã¾_`ÈeqÝe­Ø÷í{Ü9s.—_y ¾ø,TÒô-|ôõ«4Ö'ŒË¨¾Ê?õÅRÒÔÜÆ¢… Y²x)?¯ÚŽ@íæ«¯f1óûå>PTªU5Ó^õð ÜôÊL–.þœƒß¾ÁÅ7?…R%»y+umŽÙ˜ÎE½ú’lt–`{¾‘ݻֲñ]»hé÷L]°‹Çz’iÜŰ ý#sðØÓOòþa7>÷,/_Ù•»_žÆìmg}FAQ>Ìùž'®ãúk&0²w{J÷­äŽg1ø²Ûy{êŒÐ }˜Á{|ó&Ž û4­v‡?Â[Ÿ¾ÇûOñÐíwÑ¥žÌȇæÆpí¹¹GCžûô}¶œñÅÓÿؾ–™ËþÀÔkÑ“véZ2²êsõ¨átlZGá..ý#êõËŒ'¦pÓ°~¤øKiÁ1ælÚL…Ë?ÃÞJ~Ú¸‰M§K}ë£Ö‘ªªàÑO>ÆšÕ™ç'ßÀ¯«æðÊ¢Uþpá1>\°ˆ}~ƒS Ö%ÓPëÍäÖ é7p0y:p–póc1}{!÷ÞuÏÞuGv,bÈã¯P½ÿaÝFê¶ìJf”$ ¯×úäZ\Ò¯3ù§£’`ÿ?0rú\®˜p_¼ð0'7ÌáŠO–BZ}l{™úÃÏÕ—nY½„OßCÇ6 8°áG.™>‡+&ÞÇ/<ĉõs¸ú£€N#ùÅÇ ÛWÎaÀc¯“ÞeÏß/×tÉáÎÇáÃ_wûŽ’³|°àø~]扛F³ü—/¹ö½o|sê±°l×n{ç}*²Ûóâ=7óçÎUt|àAV”¥òÂ=÷\¹“û¾YâßMŽ1òñ')©Õ§=ÏÀú2W>÷"'lPsèÄ1ÞúôžY¼“Éão§_33÷¼3ƒcAí†mi×04}6ãG]L6MB¦oîÚÍÔmÙ€.•“[yò—õ¾/îâ¥åËø|õJ œ¾ÞùvGÒsQyÊ>õ JrºññK/pA}™+Ÿ}‘ãþ18v˜_~ÄœCž4‰º©J*m¨µÉ4ðóƤwÐoà`²’AhÒqÑpÆ Âø±#IµŸáû…¿pVøvÍ¿Ìbäô9\~ó}|¶>’«”Ù7r´ÈZ-CëwnaÙ¡“>=¾•¡Sž`¿±/>8™!­ëðÉ’%,Ùw”šìíÅE§øhηÜñíÏŒ¼âFnë߆gfù}·?8QBÚ-xhòdž1ßýð9ï­Üâs)+Ïðþ‚…ü~ä¬#-䋟çsÅ[ŸÓ¸Û0ž»yßþð9ïÿº%tÃ=´ýÁ¥]ÚP[òòÝŠŸXvÜ·0‹[ÎÏ«~aùŽ]¾¥Ûü¬ÿ“ŽÍsøcчÜüõ<öàã|ôèDæÌ—©ßûäW®,eÎ狹4í9’‰#{‚׊Ýé%·v.`aà²Þ]‡¡=Zдuw®5‚+FŽäÚm˜³|!söÀhÒ‚·œq=ÅJG>|á9ê“ÇøgŸç×S¾Ô”ÍGv1oçÁêy·föæM¸|²õÙŒW¸cö:®½î6?–ÒûøjÙoœ* Mm9¾e#Ge#Ã:úc,3gÁlŽøõÈÂeKY¶|9¿ì<às×üÌ×;ÎÐ"[ÃŒ™/³ <ƒ÷Ÿzœg&]OÛ:>cÖ[v˜áOeE©™º—«zµdþê%|¶}?^? °hÅbfo‹’áµ°iïüôkkëðøä;iî9ŵ¿È § i®–÷¾øŠ}Å>½í9µƒf-¤\J«-éÜ4Czm†œß“dMÍ Îª-¥ìøŸ|õÍWl;RæãÛ½²dùgÜ|û ¿ã ^œØ›…ï<ÆK|ã_Yµ×¾<“e‹>‹¹×q"ã»×á›ÒظŽ ]‡ªh”“§¦¿¾ù0F5X}»OcF`駯áE…uý× ëß—V j1ä¦G8^ƒ”gé1f͚Œùþ Î.~^þ3nº“Ö—ÞÍ[Ï^Çʹ¯ñü;+–åÿÒ Yutœ\»”'lÅm+Ç”7–}»>B#éÈM);#ÄKSë}aìwÞ~rzpY‡ÖIÅО¹|þù—”|öYçðü=[6Q¨NgxǺAA$I¨¥ ËÏoOHþÏܲávRhŒèÖ‡U¡‰c[xoë.¹ö>ŒiidvèICé{æoÚÄØŽÃÐiT$gäñùÔ§ÚÒ¾;}¸›ËÝZA·¶éÖ¶CÄ8ç®_OfÝv43¤VAZ‹ž,}æQê¡~ù¾X}Œ§~ŒQ-rs½|ºúOJí^@Cª)…ÔT-N MóN ÌR±!§)w_2܇T;€ØíVÚµlKÇ–m£¤dò2Ò…U* yé¤ü1!áåD±k¯ºŸ®õ!dË¿ø”ƒ¥¾N!‰Z™¤è}ÛˆJ­&Åh`ö¼o˜òñRîb:S‡ù ÿü-køfÛ)Þž6“±Ý| Ë'—RoÊ|¿íZnêé_+w+÷—0è¦î5‹ &éдèìV¾_ºŒ´f]Öª’JbDÏf<¿tÜ|¯^=†ö¯þÊÖ’[è”o-ú•.ï¢K¦ŽG_šç»®¥ïºKz5ãù•ëpßq:½$ •ÎÖüdþduÆÜÛ/÷yòÚ±{ç8žÿi9´&)YCzJ-Þ~è®èäCjËŽþÉ]Ë~£„kÉ0Á£aÜØyoÂHšöÕ,®Ó¯¦Þ(Þö#¯ò{×®bC—÷îèƒ1%•Q=º3mù',ÜS­]2Ѫ%’tbÁ OÒ)ÛÀ…1ÿÏÙWìæÂ†­¸ Uî«Åä‹G„¥°jq`žU©L:€›—ì`Þݤ§ÕC‡‹õ‡íŒnœÏ¦‰¯¹˜³›~dÍ ïMê‹)%•Q={0mùÇ,Ü]¤®™hÔcó>üøÌãÔñóöF››TS€7x<À$çpÝ…C}ÿ.ø“'·îàÂk&óòýA¸ùrñÒêuEº>IF#y¤ê5Õ2”‘šN-³;YøëÏì–óØ=õ~Z¥;“âO>Ê÷"²¥„µGOâVëÐH>…`wºh\».Mr2ЪÕ3êðÙãÏ2¬E6ìSñÜìEŒ9WÇö yþ£…”Ú|•ùm_ÌÛ~0¹ü”›¶ÙI”•öé9ŽôŒÌjùRéÔ$Ry|ÒƒLÒ(ãOfs¦ôTÈRýºa=˜н¡/|Øs`š~üköfDƒ¦l>ULn—¦ìÜsÔ÷Îë×Ò¡ÏZ¦ª¸vîRÚuD§º ¥qIË ¾\³™W.ï‡1Y‡LÜ}ýݼ6Æm±Fm0Syb/Ï¿<Ã)]ÙþæÓ4ð¯c§6ÝèÔÆ÷ïÏß|œrMm?öÍSÕ¬_Å’ýg™roÒÌéôîÕ1ëWæ¬;À€ÑíI5¦R/ÕXm(©õÉä¥ga6€·€—–­ã²«îäá!}^WÅò»^‹ˆœÏ]·ÌúhæÏ`ºê‚Ü÷ãVì« I›rö;©Ý:›_Že2íùqí6^x)&µÄ™ ;vGjM*£­¾çÆU+YRbÁ̇Ô<èÌeǸë§R¿!&‘•ždŒ•ÔxÝ‚‘—\Éw]À¥).L·½À¬-gxðÚñÜ=ë&¾\³“gFvbéò…XsÚóâè~ÿf\Ñ?—_ÚDº?’ Ñi ï-[Ï-ƒ;ƒk ¾½£{JaÌxÇ¿×vlPÕ`¯N&_ÒïÜb=öÉæ³=¥|õÃm¾t…cëø5_æÆç&kg~\¹•úý'°àÇå€L…]ÇuwOÁ²õS&<ýO4êÍ'^'›À7ïéfŸVë|†í=ÿÌ«7l<ÿè{ìÚôOX—ÿkFçÉÃvš ½…?ÌÀe)U>–q#DdúZçS¸ö1ô‹±;í´:Útº­âÆÁÂ²Üæ]¨­–=ƒÓäUIô¸eC‡ŒáÁ}Ç¸é¾ <Ö¬/ß2‘«º·Àqº˜Ì´6üñ3×/·À”Y‹öµ|ZÏåv‘‘™C׿|±ZÝóÜ ‡yèýçxgÖÇ<ÓMÜ3|@ G\”³bÇIyÛ]òê6¥®_ÉYíÔÉ®Cÿ½Ï8èô:4Áy™’T}rxÜÖ4Éõºóùøë¸éãwÑ-Å}ãÆóôåC1h¥j…U}'­Z…ìOwðzÝxÕfFvêæOTªÄ+Ôè4šj…'Üǘd¤´ð—¿¾‰Ž}ÆðòˆÒœ²”œŒ\ú4oXýYÝ–ÉL׳ïD>øÎÓ[6rTÙµEÍÛbAV'#Û=xI¸Äin~ê<²·ìà’Æðíú ák³˜¿ë:¨X¸»ˆ×' «§¹ùÉgðnÙÉ%MÚ Á×1I ö'ž:]Á ÁíBøªG·V|¶¬ :|‘œl¦I^ ÷¨EÝdyRê† d*ÜZz6iQ=§V—›nÍ[R•­gQ©IRûdâ°Ã‰9ÅÌçßÍà+‡ÊK³­HóÎYì64éB§lßÕ%6'­mU*ŸÝƒ$ÜioJó< ]{ ?~Åö½§Y¼í×]r-Þ+øíÈvZžs#d©X¶¶˜”43_ÌžÁWv^•—f [’¡ó‡dÔoRmp¤™Ì”ñÆK#"£Ö‚=t½áŠ_È\ï‹ ¸làLÆÅi&<ù .áÁ#;Ѥ­o}„ÿo§%I£FVûægwÅYR¶õœ~2˜“²Œ8Onç±W> 4)£ÆÇÕ' ЏíšI<8ò<ÜiéÙtkîCÄ Ë+Ñ'eлesÿ +1ªuhU¾ w•ŸäÙ>eã‰rê§è)@OF¡"ICÁåvc4f2´»ß!µ•¡Rii$~wù‡uÒ´ûhª§3µÃ[f°ùà ³ÎrZ[ŸOÇ6å‘¥›É?sš_öœaÔ•·ªSpþÉø'÷áözq¹õôiáãMÙëE¥M¡GófÁÚ˜zµRyóûoqUzùþýϪ Î`šùÁ³Lø| ¯N—!m}²{¶ÀAfª‰ï~ù†ÕKÜTJ‚µÒÙ¬ Õ3UºI«C# _Noå)öÉIÜÒ¼iàÍu:ô "(ýʧ7ó4rt5bªoÚž¾™z¶ÝÅîòœµ;ój‹ žÝ¸“³-XÊÎø±¾9žpÃ$~å-Ú]sýú àíÛ&Ѷ^*»ËJÈ­Ý^Í9¯f“ƒ( qòFí./=êrAuõrÈÌ2RYp zufLú|òÇ:žÑ’Wl ßÀë©“ü¯ï³RŒ!Ù쥀‰Kwö!ƒù…>ÖIö9cÂîßk_ŒÝeÇ ÑѦc´½ÖÃs7vãÍ»hЩ%ß8އðÜUýé»~5)§}_tñÀ:¹w>ëOzxô[ßÿ^J‡!÷òãOÓ}?u>ý=ù Îá}¼àrøÐï±— ò}Qy OJÄÖÿ'N¯ fIZIéñ¯eÙÀ(³€1o(ÇþügY!µ™T³áÜî9Ë¢]…œw]·óTFìå”Ù%¨Ò+AK¢®äÓªú¬†L{æ-®Xÿ+/|ù%W?;ŸÏ¤u½LöVòìÕãxdHkÊJ-$%™Ðû‹…dI…×ã¦Âê!Ûì[V•Öă7ÞÇ¥}/âµùßrÿËÓÐ'¥sÇ Ž>caßVÙÕ<Ôµ}¨G§Ráq:‚Œ ¯×M…Åwo!  ¬ˆ2—2I† 9Sé9îFÎïÕ—?ÍãÑ/£Òxqì@ÔT’„¡ª‹ÕÊ™Jšªâ$Ô’ ÂbÒ@ª 4K(éa»ÓNjz/ìÍK–ñØíyæÒ¾˜’%ÊìVŠ­NðH¢Ò‰Ûá"ÅØÕnØHz¶´J U§Bh´aÚÐrŒO—m s÷Ѧ¦pÖZ€9ã5we)NI‡9ÅŸ-djÂM½Û2{Ñ\ä-NÌÍ:3²uàÅâ.ÅœÑOñ:›Ýå3®ýÓmH†ƒÅÖaTœ-Ãdʪ§½.¼uºlXd &oÙ4X¬¶ CD…ÝizW$ßV—êòRjñòÔíS9?GC¹Ý9Ōʠ<¨T*D¿"(uY–e4Z}Äæ´`ÃFÒë´ ™çÜöèœùý!ö2øô¶¡^}’GÍc·ªœ^}}h©ºÒA™ÕËS·?ÂùÙjÊìn_Ûª1I*ä‡ÓNjF]&^Ô›/ãñ¹íxzôyAky–Ñ?ʱŒÎüùò#˜5Õ)®ÌýøUa}*l.dY`¨.rñp¼Ò‚®ŽÏè4i TP&Cšß3;{º­¦04ïÂWÛ#¤WË^Öd®Ný‘=n*­²M*d¬Vä¸6‹çº5ÊpR¿ÿÕ,½õ2,e¥HZÆß³,nµ$cµU½‹ï Km\1ðTû¸úKtžþ,RÛÕÊŸ2ᓟ™üðKÜÓ/à´$¼œ(³óø·ss‡L**ìM)¨¾¹ñÊ2jµ¶úýKl唺üÉM#fÙÁÉ’Ò€RX„ÅâA­¸å>½©â¡nÁzÓÌð>xcé|öé,´íãÎ31cÝ+<úÁçHY Ù©¾ÏlÝ›µ3Û1{É2îÿìSƼ$øó­)´0™()+át dø§âÄéì:MˆS°$„@­ Ì“ÊéÆápâ¾U?ð¾ûl5ÏÏô°ÕžÂCþ=D.VPZ¥ÒnJË!'5 Àd￲×zÉi1€;&Fe­¤Âbå×Õ›0ä5 g‡Ž˜ÔpvßP'à`M{ìI “ûF7E¯¡˜tËí»nYÌ ýð^çnpûA—â’ ðö @H$(_üS_L’Àãv+™D9RX¼V W\?ë®YÌøeú´lRÍJ*K¨p×üÙg·oâ”WǸ.­B>oÖ´n{oý´¨ú³}«~bIa =šø<ý}GRP.Ó¹ÇÞ¹ü"(:–|R^G†ÔOæeK)“IËÉAoÖSZa©ÞHDØ[?ÉÁ#ghÖ¬-3QɺãùÕß/Û¸mF3ºÕ tIˆGø ‰€²¡êÆ÷ð@ÔÔëfçñÓƒ¨à${'½A3¦ÞqÝÍ‚Ç|á;ìæxI)ÇNUâ.ÉçŽçßàP%Ô2Œ;Y¼Uð[_"°œÈB üƒs»]ؽ*žºc2¯]Ñ›gŸ{„—o IçN˜U%<÷Ý÷Õ÷~þÛï©2Ü©q•ÎÂmÇè׳{D¾¤VãvØÀá€ò2ŽØÎU?Êvk6/\1Ô}»¶áäºe|µ§]f6æŒT,¶ŠêvR× LÁŽŸyfÁ\yÑ%~äHÅàÞí9¹6ôºJ[Nªº—¼N_¸û¼níX»d6kŽúÚR•ÙÀKK73 ‹ot ‡½‚¹k7ùá¬bÞ™·Œzm:-ìrùæP 5ÛEèîEtíÖ…O>¯,]æTRs²Pá¥Ü꨾Ê~­ °&òO„õ¶·±pÛQÎëÙ#tžõyŒíÔ%«WPhªEÃt½·âı-,;båê>>t²kçÎd»óyeñj0§‘–“2& AXö-N· »žºÝÇÏùû·³®°³ÁçœJ²ÿ $OrH¥Õ |°}óFlÚ .mZM=¸cg’íǘ¿3Ÿ®]Û.“>¹z¾_þ+µëw • •‘]Z°lñVX1eç`LKÁRQéç|,‹0CWP\ZL»N}øôÉ)ˆSëè{ÏÓú}¢kæ1æ¥Ï3f¯é7¸}Ö°sgº¤º˜¹d¶$3)µrP È(€Û+sðôId›³û72ôã9”2Q¹½`hÂ5­jóòWŸ°`Ç~V¬^ÂàW>¥L2cêYéÓ›M#ôæðN](?¶™_YÑ©5d6¡µ>üy=íÛõ!W¸ìÝu¡23vØe<= =ûöí¡èÓ­;®Üð·l8|ˆw>zƒ‡l"37#ü X:Ë)>øè=æ¯Ùéç#‰4“ž5ÛwPd÷ýèëSá4qQGzÛý¼ Hu2õ»´h߇‹š¦Eàäï`ړϱhåž8FgÕ€d¿¾ýN„m¶›oϺüœöZ=¦¼Á[¯Oç?à“Y_Ñ:Œ}‚>~“ äLŸ¥¾g·O/nþñ9žø~'“žüÂWÌëÔ¡Ö-úÍÏ2.1M­þ<:¶ –ÝkyîÉgY½=?¶¡- „Ò|H$æÿ"2gp¦¨Tá;5Å.((òå /¬]b÷ ÂÍ/~ͦù}PKÞèØ•G‡SûòçêY¤ÔðéË6m ;§í³BÍ–¬ÎçñòµÃ¸sÖ›9¸:i‚+gèEc¹®—Ï ^øÃ‡<²ü4Ãû´`ÛÎmô<†‹š&ï?x??þ2®»™ó:5eçºMŒ¹|"Ï]u!N«…‚¢rÜAnðÉõ+¹äíÏé0x ¥G÷’Þ¬“žWÆÌ]»¦ÝÇEtÆ)©(¥Ø@Áì Åå¸ýÕ¤^—“ÓÅ¥¸ü»^¥¥’ò§Oïk`hï~¼ûÎ÷4ºáoÞ7…~IuçóäuÅG8œÒœÏ.ôyØõ:v iæ\rßmÔMÒÓºnÌ¥¯´ûk;…ÅŸª $(*+¡Ä¿‰x=ß÷þ¾œ›ãÇŽsÌ«bܵ°÷Ô|èNÒ«<6¤sïžÄ¥o|A¯Âdˆb–ì:ÍÔ{§Ð£¶oS¶ÚÆ+LîÖ1L›Èh5ðÑß±{ír*­åœ,« ½N –¾zšúÞWNbϱ§¸ö;ø¤GwôåG(‘ëòãô'0j a‡ntM×°¨´—÷îTmô]1îVö=õ÷û¯«-þü6uŒìÜrŒç§¼ÌØn9”VS"W—ËAAq±ßéÓ2¼cÞ]øyWÝÌ3·ÞÌÄþ=qÚÊA LîÚ1B~z4m'gÑ«µ/qÏФ u4^JÓêq~+_ŠGjË^|ß$Ƽù-v­¡Mm#;·ÆTRQJ‰7 vXm?~"À§}¼aHÿŒ‰iGysÍ:’ëõä…™¯aµTP\VDãÃøúÞ+¹|Ümì>ú¸o}zvG_q„o]–¾ý©µ[sK«z<ýþóÌþÎLVVmºÖËàhI™Ïê1”Ïî:ÉCß|Ÿ•ó騱3£{¶f¥µFvl~ùóTÉŸÛIaI îª^´ÂKAQ1E¾÷½lÈyÜ2çzlYNšÉH»͘>ó:7lÌ¥-R)-.¡Äé©^«S¥Åxª6PÙËÙâRJl®êçÏ[·‰z­ºÖàmãÖ45ØäLç¾ðv—¦õáÓ•ôéÔ¡š·ïœ4™}§ž`Ðm·0¸{'*î$3§?¼pZÉMaq1nWÐFí—”°åP> èÌÚן£íÍÒæ~^¸ƒEË~¤Ømàô‰Œ²•âòJNÙuçÅä»’}Kxø©G¹´ ‡aý[)€N…e>^‘Ý>}Qµ–Ö’"À†³Z…úþUâðíµ^üšÍ‡pû –¼Ù©+f{Í÷ÚSk>b« ¦_<(ê\x·~– ýZðA·&lÞ¸‘kïÿOøô¢”Û‘©—öá™o ó¶w°åï&_?Õ‹“l\ú6>õ5̃8¿CäA1Âã™Ó÷¾—ooõ|gËÜ Ë2 XQQ!Ìæ¿þ$ÇC~~>yyyh4µ­ëæ· pe7æ‚"|‘ßÏÞوAÝ;"ÛŠ˜¿ô7wîG‡Õ¿ZýÍG¬úó(*s]ú^Ä>­ko°0ü¦[¨7xï]ÙWÑ7ܶá7lÛƒÃ)hÞ¶ × „á+ÏœdáŽ]ä•‘]+ûõ&$¢[vš¯~ÛÀi«‹Ô´uíL£,3'³=¿’¾]:øóÂK+¶ock~j}cûG^š/ô`9¹‰¶w¿ÊóO¾ÅUm3CÆ÷çží‹4úµn@Á‰Ãl?e¡o×öÕ`/=ÃïOÒ±m'²’T8²‡=¥C:¶ÄxÙ¼v5Keh¿¡tªgdݦ ¬;r·*‰Kúö¥E­€‡}öø~ælØARN#nèÓ‘m»þDʪO‡:é8Êΰjß1Ú5oOtƒïÞ[·àI­CÆy¸ÊÏðëÞc´kÑ:izÿX+éݹ¾4.' Vÿ†l¬Ç%]|a¸Sÿä›Í;q ƒºõ 9ågöç/ñÈj Ûf>MH™Çζ=û9dsâôxð¹I© îÔFÁjY¿f5¿çŸAÌtmÞš~­V{tßþt%sq»¦‘×ý¾šßOžA$™èÚ¬ï:W9 ¶î£Iƒ–´Êõ«cƒù+e÷ébêä6âª}ªs(wü8‹1_-å­¢¬p/‡­jÆöLÓ¬$ÿ¥e,Þ¾Ÿ¦U÷6oÙ‚7#î|Êvïá]äÛ \Ð6Pm^xxs7:u0¬s'R’$þܳ•b‘^Í/gòYwô]Ûu"Ã_»wûz~Úqî]zÓ¯u#æ|þ¯¶²mæSD¤ëU–°`Ç>Z¶ì@ÓLŸ3°kÛ&JŒµ9¯YèfPxxs·ìÂ*ÂÇ´-„‡|ÄÂÉ‚ß~C“Õ’uM¬?x+*Jm6¼BÂévP+³Ã;4«î Zµ®Ìt ^ײ³|·úNK)LÔKáaö9œ×,¨¦µ«]˜•Â÷\ͺ´þl|bB\£³jÜU²í(;˪}Gi×¢=uÒ à±±jËn2ë6¡mtðÚYñÛol*v0üü´Ö;y÷Ç_éÚ»?ݧ²pý67lI«Z)µêÐÙ×}Ãkgͦ?1ç5¤CÝl¼öô¼á!FÝúS(œê²'»IŒhïãeWQ¿ì;F—ŽÝÈ1Òd?ÿºŠgÊÐ¥¤Ñ»U{º4©«â,+ö¥}µ|^;¿mú“Ô¼´¯ës2NØËâ}…\Úµ3Ί|6Yn'å7*•ÀîV1 }GšfùÂööSGøvý6J\^R³ërqçöÔJóñÒþ­ëY°ÿ$mÚv㢦¬Ùµæ-Ú“c (Z[þ)’óêP¶u)õy‡/_ýŒKZ¥cÉßDÛɯñü“o†éMíÚ¾‰’¤Úœ×ÜǧÅÇY´ˆó»uÆä—Ï“‡÷²t÷~J,š7iÏÈn-Coâ²RQâ %7“W_½öfÿÁTt6oÝ„'­=å«‚Ÿ·î$-§ÝÕûQÎÿ0½.ǘ¹,ß{„¶mºpI§æ!·Ÿÿõk\?绾x›¼Xùœ. ý;´ÃÖd<<¦°ÍZùyÁ2Ršt§Gû<%ÇY¼j3]ÎBý¬$òw¬gáR‚Y{?-‰Ükûæ#Vžã^[‘¿åvsÁÅ£HÕ$Hïá홳8\`£ÿèëy^û°+í|õѬ޴: »sï}7T§Ñ¸­ÇèÜ¢yã¾dÉ+‘EEk?- Œ¿üÄ^VlÚCï ‡S˨·•å –ajÒžíóþ¿0ÿ³vTVVBEE…øOÛíGn·[ü“©üàJÑaìµbÙ¡Ê¿õ8×}û¶èrõÝ¢Ü#TMN1aÒ5â†þý¿ùPÐã±òÌßwžožt¸áÃ…ÿ\å®8.¦LC|¹f‡Øvò xôõ'=‡ˆ÷VíùÛýðêoE»q7ŠýåòÿÄZ-\2WÜóæWbË©ÓbéoKD½QCEýÛ_~=¹öÛ·þ:½i/oNM¼±ì±ýÔ ñöÌ×]ÏOÌÿ£f×W úõ×~¶ ºäÙ+òöCgÌ{»²½¿‹Æµ²Ä_ÿñ?±öÖþ òÒ²Åg«öÿÏìxÿi»¬¢¢Büc ‰þ[¤ÏjÅ÷Ï=IÓÆ¦¿õ8›öÁÜnzRÔ‰5 N½˜zçc¤Ô©ÿÿõ[¤ÕkÀÈ~=IÅîÊý=ç95¯ÞÿWiµØÊO1íëIW ,^5ï=õ ·œßòo?öœçñãÓ=h˜ò¿Q‘ªò°zóJ6Ù‚Ãn£[÷á¼6áFÌ~=Ù¬Ï%Ìíføkô¦VÅYɧsf1÷'5•/Ï<üSFÔ°ÈEmfp¯óiY;;êOÎ>N£V}˜2|@|L›ÄûßÏgPß^ÿk¯5¤ñí¢_èÓ«Yb+L„×” %èÿs’=x¬N4fcb.þæ$l6dIƒ:I÷_x¸ŒÇbC“l‚ÿc㶆§u&è„þáõ„õ— %(Aÿ RiИ*÷ÿ’’“ù¯$ó_)Kœ úoSBþ…$;Xn4Z5BÒ¢3hÑI½ð¸q:œ•WFŸl@§NÄ»” %(A JÐ?Ð÷NLÁ_G/M¹žÌÌLê4hBûN½ÙÚÂ¥`ãwÔÎÌ$3;—6í;0íÝʼnIKP‚” %(AÿHJ ! s 9í.b÷üLŸ¿‰"[hÿ®”¦½xõÝw‘+Žsó=O±eçñĤ%(A JP‚” „Ñ™ s£.½Ò¥7œ0búüÍHaÀ²1»17Žo Øyêž§5‰Œ›%(A JP‚ôϤDx=ÞÄM—ô¡uó–´iÛž#®gÝΠ“ŽJÎðî³÷Ò¹cg:wîÌ7ŒçÕY‹CÂÊ/òË%IQNª­< hT‰åHP‚” %(A £óŽŽüþ%ÍšvããE‡9ÔXF ìÌŽß°hÁnß,‡¹¸CSn{ì5ŒMéÞº1ß}ö ÷½òÊæet$S$¦;A JP‚” ýƒ)^B²«”ëF\K¹º»ŠöÒ:Õ÷ùso‚ÝåàµÉW²ð¤…–nfÂ…p4ó±Å}Îí6¤8Fi‚” %(A JÐÿÏ”@:£PÙ¡ßXS ã{©Úଢ$oåa¦}¼¶£î­68d TRä2ž™À;” %(A JPÂèüŸ"GÑ ê6T>ª®²è g€ ; ùÜUìm49^x=t&(A JP‚” „Ñù?E©»°lÖ§QÌG_½ÐVVýÙ‘õ?°`¯ììôPƒRøLʔԔ¨&§ è ‰ãñ” %(A JPÂèüŸ"c.<6¼5ë–¼À/}U•,_ò#[w—aÌnM3àË_äÐÁ|v/ûŒÆ½GSd˜M!˜¥JòýuðàQ*JK°ÚmË N¤¼¢‚ÊÒŠD =A JP‚” %ŒÎ¿;É7‹§Ëò?‡Ã†ÓåBxeÜNvgè÷N§‹Íìõ‚¤âé¹ó?´ïL­ZµhÚóV,þ>ò.‡]qœV‡Ù+ãu{°Úíïj·Zp»< OP‚” %(A úGV¯¿2uZ´—ÍcãÎ=8TfZµíÈù´`àÓÙÚ¦7¿íʧVë¾\>°3¶üqŒY}Ðxš¸‹E‹†à¨,Åê´j×'tÀÆ<¾þy V%•V4™MHÒ@á–¯h;`vI2NqËɼóÒ×­}€'æîGvf»×ã¦ÁÅ·³mÖ4§¹'(A JP‚” ÿ6IÂl6ÿåòx<äçç“——‡Fó×ÚºÛÖ­`ëî£È"4H-IÉ)Mл.¿þ¼ ›GD„±U’DÇÞ#éÔ2ó¿¾8Ö³ûùnÁ„P§–Þ}ã<ù›Ž–WçÞU…±A+Æ^Ð3‘C‘ %(A JP‚þkv@eeå?ÓèLP‚” %(A JÐßËèL€` JP‚” %(A úË)at&(A JP‚” %(at&(A JP‚” %èÿúÇ%WÊBp´¼—,£–BOø©ªbü]uPü“€B¯ ÿKDÜÃ÷O ÐÿÕéCÁc‰?®X¿'â ™ŸÀýDÐ;(Ýã_ýLœÓ¼ø~-ªÇ!¢®¥»{èU¡ïWõ½¨!_ÄúˆXkåY¿‡PœÛð±V­MôY |£ÌJ¿‘Âf7¾Œ(?‡¯Wä[Ô䓚­C|þ®9çÅz’ò]j2¶`•j¨…¨Áãÿ>G*$Ú¢¨ÿ¤)¥ƒª® Ÿ¯ÀçÿªÖ<—Õõý©FzôÜtE|~Pž+嵈- ÄÔ…ÿŽÎ1e6T{ýëóÌ áï9eÉ©™œËþ©¤—£éÏšHc´ý!øžç2~ß_Bt* RSª{„'ŒÎ¿Ôè„R‡ƒ$•„FR#Kµ¾V’ä?-H’¢*Ëj!~æÂ˜KÝ Â÷I„)KIÉÂ8ªú¾B„< !ùÆ þïüw‘¤ê±IR@Ì«‡ã¿VHw aÄøƆ5}¯ªNPR‘BøŸ>ß¡ó¬$wBøÇ).åûU/ä» • z^`LT¯£T½ÄAJ½úžRˆÒ ¬‹’®Uÿç_ƒ0U(ª†e áŸÀ\߯êQÑÔ»M†Ü·Š7C7±Žâ»ê›Uÿ'0"ôu|såŸ;IòßG ]äò‡ÊBøf_õû*ž—üBX¢èÆ]õzTÿ¿Í¡D/‡Êj•¾¨ÉV-„ 䯢êqÁïé{Fµt )rwªâ‚¿{/áçSIIN: )ζU=„H¹L—¹öŠt¨~ ÖaR„Ž R¾ký¬(±e•N(Y)h‰}óЯ#ƒô}µL†ÎWïH±Œ«°‰òÉoŒ‰U¼6 /”ø8r?RÒeR`_f- í=nPû¯“¤èãV2U%ÂÖ‰pƒ½úûXú“`e,ô=µZUK‘*?h2…¤TñE@TϽ líÂ÷çp} ´„kŒÕø~5ØOþUõo¾%÷±ô“8GÙˆµ°5ÒSá‚!Bå¡&º" ¿«pÉ2^·ûokŸýNßjx…@@þEVZX%†q„^äXÆUéQdÅ Ga«ÐF)Èë’Dä8c1a´ïE å+bÀBiCQ˜O9ìFÊãÑæ?t`U¨qõØ•ÞUŽ£Õ¢mfñæ0ÞwA8aèpò ‚á‚ê9Rð •ÖE {¿h QVÒ…’ú¹™I‘l³— “M…µ‰˜›0—‡Âo&Š{OW>™ú-(ŠìIóQ€‚`=K†BáÛÐõ‹ë¼Ö@î"xIDç)Æ*Í‹M6‚/tcðñö)lÿÞÏ‚åADáçGPý&ô£, ¼âï}ˆö?·(uAñIQ3Æt)†ç¡„D¤W"Å€2ª&8 R¾"¾gÍ€5@âysáó"…½oxü7žWÍkŽ{ˆÀD¸2– ›†Ìö·[“sAÖ%ð )Œ§D¨2« £…»RÐ;VÍ]M ¥5޶fR˜A¦dèKÁH“ß¹ æÅpÏ\”fH°(”¢l*BÁX«zfˆB–Â6òúÝD|DL„ 2ž>A2/¢¼“RúHM)ŠÜJa Š57n•Þ#šÁ&Õ`\(èÁC„ œIÑ ï% ܹ¤¢Æ’ÝXï%¡ì¸D}¦PÐ-!HAüh†’Ž"ÞöŒhH˜}ªƒ´BDî DÛËž+ÅÑÑÖ^г&BÄžƒXh¦C§ÇEEÈjØ…-°ÌçRœw–bðeØ^!À)ïˆ0 …Þ3"í/atþ‡¬M¢é-Iyczb"¾pÅB ƒ7T.ÄAž–ÆøŠÂ…#ºÝÈSÚ@â…â‘R$jËJ2-En@ÁJPD1º$â?;*š)ÕlWR¸DQÞRc$êúŠHä$–±æH‘ˆ™Ǹ™_Ý!“DØR‹HCSR_†ÌŸÊçÁ/'Pˆ½™lH Ä6\ +!ûJò¢h|ˆèpø yPŠ>V ±‹3FÂc]‰_ÃŒE^”Bë¨Æ›P—ðó¨‰ÓT+\ª¨Ò8‰!J‚*År"E|‡AÄ0‚¢¡å5ÂçZŠbÐ*ª{]§EPDD&êu â&Å2¢$%gMÄ6ø¥(ü¨˜V"…þ6šmÎÁY‹&k!ö€j+íBŠtKR(xÍq’b’kFçÌæTF^D—؃çs%²ÁJQyõÇAe*¤„¨E{¶PFå 4”$j tc庉0$Q  ƒ Ê' Í$†ÂGJbÈQ×SiSeÔUúBŠŽ<Æsç%%ÄY ª‡ˆâlă{B®õÐ…ˆÍ»!†H÷.I>@’×4¼¨~N @#’§B 5}EDM±‰ø†šC¾E,¤HÔ` DX²bÐZ„oØñXE – ƒ×+ä·âÜ6TIŠHa ~fÈ¢à˜KJ†—ˆŸaˆ£_(Çñ#Q„sAn•Œ¡€L)íÙÕ²!EwÆcåG8ZRç#OK (¹¤4HÊë¤ä+¡îaF¡ˆH”õz´´5QýÏnR’kQƒõ%Ž¡Zý aò$”…C:… "¼®d@Šr¨(’r¤"*Êàƒê‚Ì¿aÕzýór:Còt¢/|H®qYQ˜WµZãÿN¥ kjµ^‹V£ŽÔ'UkR­WÔŠƒTk}ó©V¸^’Tc[«E­V*©¬º‡oœÈY«æqM€/Š,VÚŒ{œµ¬àšþõøäñÛé6ò:Ψ´d' «Žçˆ’j…B<ËXÄp¬DãC(lQy:J¥v´±H1¸8])BS¡¤8!^¢ ¡ÜVGin%…çE-â…á¡f=T•Œ[JŠeK’r§Š0@(ìóÊÆ°‰XÄŽ ˆ¨Uô¡h£¤ÄÏBDÕ«RˆÑJty‰Æ`Ò¿~~×_MÿÈæðRF­£÷UåcȲ@o4“¦Sa÷€Y’ì¦ÂfGHIOà T8H*&£D…­«C ×k-¥¥…œM.ÆèLf´²/çÑ#«ÈH5£2Eeþ6]iéf´ŠJËH¤¦¥ ñ{Ñ:Àât!t8-叄Р³ ‡ÛƒV«A—[nTSa©Àæ’Q©%eeàu¯¤¥§a*œn<²L²9•””•YHd§§¢*NTz=f5W–aqxèÖw É@ÝÒßxeöÚÀéO~OX "C-qhïïìsÁýÁ°öͨS¿9EååØßæ 2²ZGNŠ ðVmð€Ía¥Òî!#5Ù+P©}seuyIJVãtÚ(·¹HNMŤ» )*¨ty‘tjô@aY¨õdÔ¼÷àe|2o×NùžÏŸ¿ €R««ËHAëîBRi1%ÊmØœ2žLsv·ƒVƒÛëÅ#T…eeH#™z v»• ‡ •äKCÐêd&i©´–c— hÕ*: ÇWÃÇá’”YЪÔþ–y¾ðšN…Ý fuZí iÈI3!XlNtI&Lj(¶–cw 4j=Yæ$ìn/­Ú7NÙ7ÎK.—„JÏ —‚”rd>œ, ÆT2u6‡Û‹6ÙD’ ŠËËð&@oÐPVQŽ,ùHsJ*^—›ÝáSÝ’Žœ4#*Àãg+`wÙ¨°ºPûçÜ)dt’ ,pº½$ét”[,è´j­ž²’2D¹;Í&3náÅfµZ™Šè†µŒ†ŒB•ʧ8-.:­ke.!Pi ä ¸8ì.ŒædÔ ËÊÁ+Pë’È5pÈ`³;Ðêu$iTTZ-Ø]’Süüé…u$Ê2’Mdé5X\\^£9 -p¶´ SS1úyܬ‚ —.-‚ÂÒrcZ:é€ÅéÆ-ËLÉè%AIi9áËéU”÷Š2lN'ZC={  ç¼uŒ|ù6F?ø.×ÜÙ…_ßšŒÁåÆåD›c"€Ñ˜,fÞ­‚>‹Ý ÞÏ5T/âDÊ5oŒ¬ú·Ù½¤Œ~ÆŒ¸D‹jÔà@¹á`†#2²ŽšR â„à )‰îdˆè!h¡žë¶¡tŠL¼5­iDBRnÎ1OŠk­wªÒ<ý{Gœ&Îÿ›X{”ð“ˆDkª<!‘f6q`éL®}=ûóOòÊÃ×ѼAúöêÅÛýJ¶ÙDVzžC»˜tùZ7iJË&M¸ýůÐëS0êµT–”SZrµZ‡^­æŒÍÎÉüäŸ*Ä«1’žää¹®æá>Çh6¡ÕÈ6K¼vßõÜùÔ‡Í&²Í*^Ÿr#÷¼>‹‡÷pñyhݼÏ»€}5Yªn{#?n9Æ¥ïÓ¥n=Z5mÁ„Ç߇$ɲ§\Ý—VÂlN¥ôè6î;œ– Ó¡E Z5¨Ï”韓–š‚F­";=•c»~çŠá½hÓ´ mš6åéc4§aÔ«)+«À œ-µ„ÊD:%dJívŽÙç3FÊ+¨tT°ëØIŽ•!«¤bSuô—æŽj ä¥Ù²ü[FöëL‹ÆMéÞ±W^u‹6œ&Û¬çÙkÆðúçË9uh C:7¢YÃÆ\ËT*e=9f¿~¼œÉ/ϧaï |òüe8ÜVòKÊpx¼H’šÌô4¼‡v1éò¡´nÒ„–Mšrû‹_b0¤j6a;¾•k®ºžµÇŠøeæ34¯W¶-Ú2õÕŸ¨•–ÊÆ÷^dôÐñì<«"ÓœŒ@d2£-=Àø1£øò÷38¶ýÈà>ÝpñÅŒ»ú6Ö,#;%€¦™LXö‘?Ožä•)þ|ç£d§˜ÈK3±~ñÇ hÛ„¶ÍšÑ¦ISžñÆT2Ì&ìÇ·ríU׳öX!¿|ègË6<úê<Œú4 z):²í˜Ó0Ù3è èí…L{ì6Z5nHûæ-hÓ¨!®yÙ`¤—8k±RPX@YE k%'ަ°¤ÔZT:=yiF¶üò-—œß…›Ò­“oÝ\}ˆl³ ÷©Œq ‹×bíç¯Ð¾^=ztîÆÍ¿…ÎlÆm±Pxì ¨AE •v;'òsúL!²™Ž@WƒdF­7’®¶ðìµcyí“E>°ŠAšÒ¾u.4‰S&#µ¼ÿ Ý7¤MÓ&t|9+w—’—fB§Ó“–,øæíiôhÒˆ¶Í›Ó¾i#:u»€]%‚l³‰U¿äãϲS<>éšñgí4#É:'Ó§ÞBÛFh×´ =/¼œ_v¡Nz™f¿¼û7Ü5KéI&_~!ÍÔ§[¿1,Ý‘ONjFƒ ùÔ¹ã Z7jD‡ÍiÓ¨!—?òêÔ Z5Yé©Ûµ&LÞaLI#Y¯Av»8]ZƧàÒfpÇEmYùö½|¿»˜L“©f¦¨9y" ²§½™å¾±Ò Dœ””ðÐn4¤-8T¬Ø/7š«“9Q:*H±S*”Ú¸ )v—šžÜ-‡>jx\ÔÌh‹[‰ÃéˆZ !Åy®Ú79^º1æ9V7!B›å+¦ „öy®:C]1mGÔeþýÃÎ8‹ ”••Q£¡dÿ&¾ùáswlÁƒÓÑoà vmÚÀÒ5;Щ5hÏìgL÷.¼?{)WMœÄ…]êðÞ”kxpúÒŒFf>s3õê5aù>'WͦI½º´kÙŠ.íº°å¬‡dƒ†o?û𙋖bð‡@u-‹¾øœÏ~\H²FƒN£aÇòxïþ ´jÝš¶Úôn—Φ?VpÜ¢B‡ƒEßÊ· £Ã[IéÔ•¶ T|òÌ­¼ñíÒ“M‘ji“M¤x ¹ýâ~¼óÃjFÝp3÷Ü{ê¥qêÔaÜÒŒ)”úƒ>Ýúòý¢}Ü|û-t®'ñÄÄa¼øíF2Mæ& ~ŠÞœ‚Áz‚K[6`ØÍÏðÖuý¨W·.][·ã³-§É1›ÃÌÐ&ÑBÒkNbÕÒsð8m.`ðÅÃh•kãûY³tS :įsæðèÝ×Ò®M7ö‹4­«cÖÏsõ£¯¡Õh8³yß~óý;µæ™÷7Ñ©skö¬›Ë¸!—²ëŒL²ß½ý.àÑ—ŸE Wº«Ñâd³ MÁ~.ëÞ…÷g/᪉·qa—Ú¼7åZ˜6“FƒÛVÀ·³>çîá:áqÚvíIͦÝw ‹É´kŸÃOK>áùó0hô¨U2´j¿ý4ŸÏ™Or­<’ zÒsspìßÄ·_¿Ëár;I]õÆ–¬ÑP²wC€?_\D¿ƒÙµi‹×ü‰N­aý—è7ì&þ°Ôជ¢}‰Çn¿„ŸšO²Fƒ×^À7Uãœø8m»õ¤¾f//Þw)ßo:M¦Ñ¶ ayª’B£éà…WéÈLÖ3ã±kyøÙwiß{÷M}ç·¡ü`>.YC’#ŸQ-pþð ¼{Ë0êÕ«G—Ö­|×s8ÍFrɬúâ1zÇ¢-§|ñpZÕò­ûâ?òÑi4Ø,g˜³à+n¸°'ý®€v=úã)ØÆ7ó~#K­æI¨ß¢;¼°föÔ«WN­[ӣ뜨ԓ‘bˆ‚*ùS´Z’UVVþø=Þ1–nÍûS\«mSmü¾b&Å. I-?>{£n}G£žÜzûDŽ.ŸÍ¨^³ûŒD–)™M_¿Ì•w>Œ£A'î~h*W^2û,§‹ÊÑi4Ýð‡?;¶â™÷6Ó1ˆ?÷«ÈHÒðÀùÿùhvþî¾û&ý<›!Ý»ñóŽB  G7¬æë·¦0 WKÞš·—þœÇžÕs{ù½£'=IðìMóÂ;óè~ÅÜ÷À½œ×6cù”Ên2Svèút;Ï'ï·ÝBçzðÄÄáy÷^k9Nàá—žAÌ{ï|‰Th¥0È(áïh±æà\ôð߯Ëu«i_Ƙ¿“†¦åä+b¼W´îiŠˆo˜…(âœ@Ôã…£þ\¡!ù9´E ëÆ+"ЇÖ49¨Ý‚ËT4öˆÑj+LqÕ¤á}MóˆÅñøDé€%#V_qˆÝÄ>^ÿ¿7EPEUïK)(E„¯rØâyCZ¦/¬ZQ‡Õûwq^3»¿üœr»/°ûîK÷ñk‰“Y[-ŒëhÁÑ;“·Ÿz’‡&aô “étÞ¥¢iÿ›ñzå)†ô¬jþümÿ.ú6Ó±ûËϰã+vºçÚ‡kug×¾5´ÖÃwÞΘ^õùìÉë¸ë¾ršÔÉCçç3óÖóèÈîìü–ÚíDZbÍv®ëU;z¥fÔÊü@(M¥Õ.~þyМçß›N»4àÖÛ™‚J ¥Þ¦¾ý>û÷¬åÞ©Ó5i*WõjJ±ÅJRÝÆ¤IPQ°ƒq×=‹¾ù6mÿ‘¶°T­{“è’É4B±µ”ûçlâåÑ]¸õ¢ßxs…À¨Ûž§Ý…»˜rÃ=$õ¼'îF™Å ÚL²’¼TÚ½ hR>¾¨@F°½„‹oœÆ·?Ⱥ7²x˧hLZdëAn|ìš]x'û—¾ ÀÄ P»ç ¼>w=LêͦWpûÓ¯s×ùõ[x((­ +·ŽŸ?ÛWóçWS®äš¿aÙaõÎ.äÕÙ«4i?ϘÀut£A—‹™0åŽ.œFƒf€eö^À–³‹é”µyøÓE.ƒÜŒrV,Ù òÖkÏPàÎɸ½nÎX¨R’˜öàMÙÍy¤JÞ_æö+¾#Y#au $•D¹K×¾?ýsÕ¬X¿’SL&E«Ââ–•[¡dtÅ #‹è-îâ^µãˆBŸRÆëÑ*ìã¶a"òèM)z˜_H!ý¾¢ß¢·ç ®j‰ö{¹ú€AÍ[5‰ª4QƒSüˆÞ.,ÖzE-æ CBÄãŸX¨2¶vŠ=EãpÊE¡1ÏyÑ[šIQÞ[)5!V¨?$ ÁwŽ«\Ü–0:ÿ3†gx®F ÿ\Íʺ<Örž˜û ç5ÓQZ^À‹ßÍAݸ+ˆJÖ¬_¨˜ÿá#üêõ¢W{Y·»lÇ9Rèå¼N}éÔ f=w3eõÛrõرÕ÷®A„ÎøAƒFÚ°øìaÈìÌ'_ÌÜX³z0kö\:76a+?Á‘ A‡‘ðÉ=Ãp ðzµäJ€Ê%䏷ӿ$»nkî¹úBžýê{Ôæ\î~p —Mïöõ:ð–eÇÊ}€‘™Ï߯G5¯“ÝE€û8gÐT¯Õ9A•ï§´©\téÚÖ±ñàËŸÑï’ëÓ·•v+åvg Ø!¸uˆ?A±–䢨 '¼ËË£»Pa³sõÃp¾-«ÕFç^ƒéÒ«oÜ|imÏgl,V–ápúCìQû8¨):V‚±Ã…¼÷ñƒx<ÒzOä›Ù#i›;—ÿH `.þ“É÷Ý‹Ã.c+ÚÀÉ­{€Þ Ÿ|/,þÉýðù˸}üm 1Zé>TÙ^Z¨ø0ˆ?ûŒ/~Caa!;W¯`ÂÝWpÚæ¡vçáŒí‘Η+Vc¼¥€™ïçO§4ßÈëÔÊAÂËêŒ,nšr “_ø’:WÝ?…Ç\ÂÀ®íÈIÑ"—aÇʽ@²_Þ5Aò~ŒB4Ñi°ºÝþuvƒ.•z-S; ©tB†Zíc.â OŠFF e[u/Ö#)ö©*¯Xݸ­¸€’ÊrN——ãòú*Cƒ™NR« îJÁ.› ´ G V;)õÚ3nÌ¥Ô7ªqYʱ]@™’’õdiÀëVÇ mx6Jd-~ù_ü6½êêyõñ»éÓ¡·>ô j d·Ê0æ5Bë8,åY\zÇ]Lyä’lnln9zÄA%!ÉœÀé¢Rß\ž=íÛ4Ë˱¸³Ç7­Ú´ñ92]\ÄUÃÏÃàtRèaËÇ%ƒÇá3XÏ–ûdÑ#û» Dmd(ö°Z¡^Ëv¤e6;ÙízqŘQd¨ è¤§kÕIÃãpQQ^ÆÖÜvǽ\>¼%V‡}Fðû– ÂBù8P)FxZŠÅs1®u´4aóÔyC„£ÊÕS$¢gIJs£°J§pÅ:Ý,¸0ºª0íoJÿ@¤³Š¥Eh눫ª ®pÃWPQY ¤¡–d ËÊ‘4rM9¤fgC~¯½ùæ°•—Páðb2Êà²QbñUSkµz2 _èÜåÅíµÎH¤U±vŇüzÈEûîQªÕà²ãL* ÃÂi'$¥¦ R«Qö’ÊjBÈ2ÞðtŽh ^%átzÈÒ'qå·så·s`ûJž¼åÞi<½¯Áu표 rV3Þýà݈Ù.()ÁjW“’ØlRÓ}ÆM’A‹Õæ¬n$ù%ADiÖ,˜’õ8.’ FV nÿo5þÿ–Ú}¨xeê+˜úêû©…Ï´ËNÖã)ÚÎ’E‡Èé+©€ÝæÔHôhøø/ vm@Ãú-`Û:ŽäÕ2Pn·"!áu9ÁT sv6œP^wpÀæòóME-û¼»*È2)—îÇ /~Ãä©u˜}ÀÃÄ·ž (°¹#Ž5¥úæS¯ScwúÞ¯Š?++->þD¦°¼œäÔTêä6Ä ¬Û» ¸³1-°iÕŸqÖ(§?d_i© ŒS–ýþZÕóeTš$ÔÂKþ‰ÓÔΫƒd­Ä%D”ö€"D?Ë.'N’é6àr¾p9¯9Ägo>Âï¿Í ­/ä³ÇFàdá3¢NŸ£¤ON%UëgQág©-°î/W­»IO0l­ð²©Õ*ÊËË)P«|v‘Jq3¯¿;¦CnfÚ%ýòÞTYÞKK°ÚUþÂ?ƒAƒ·|¿ï´Qh+r5à´{¢#ƒÁ!Gç´¶X ñ¥(¡Òç1ŽâŒ@Û JQFи£µ5Ô¼/&ÑÂÈ(…7ŠRíï¼ñ-Á"¿XÖj´ƒÆ¤áv‰Èƒç~"_ÔmIyüþ£†E´SÛ„Ra1‘§¤…7z×Ú*âs?*I~ÓCD]~¨†ÒqÓ s ˆc'ο éJ›R.ˆ¢€Jþ 8ÌR«2Œ£ØVùéõó€Jo$Ûàæ•[†2 W/žxs¦TÚX îþÿRS§ÿäÎKÇ2cÑ*ò­^r5a€Yµ»}q9iuQ?Íx™"ˆÒÌyo%NèzQ×Àºïó¯ûþu÷ŠUƹì‘¡)²GF2d“]6ýô)¿®$]ïåÇyß°ùÐY2RÓIIIÁl6cNIÁl2a2™1›S0™“QùÛÛÈ2x…; üT:¼´ì1ŒmSøàþ¡|ºl“/túÏ>>•×¾ÜD­=ÏÝ?‡_ý’ƒÅ˜Rr=x0Ù@…Ûé7–õ¡üy[€?;¨ õ%WQ_ Ž¿Žßò+IÅ͌ۯamÜ|ÇDÔ€ÅîÂNöò d¼˜2 î]èËocîÊMº¡Q³v ìQχ´Z*-w)Èû¼/^çÖ'¦S¡ñË»²¤!M‚Ÿ½ÉqW]y&Àá càðqMŽÂŒ‡¦)¡˜áÿ‹† 3JDøÙëBA1ª›#ƪP]ÍP‰fUIRXK2…Ó{ÂÛð\Å=ƒÈª÷xˆmÄZÅ8Œ£&Gº*6ˆk -Ä-/WR\ÐÈ™•‚æFÄi .Es¨£„ùñ+Ü«¯üm£ÀÍÑÀ¢`;¢A¸õ"ý­-´xsø5é²lõÿC¡!K¬vz\ú¯O¹•õó¦S·NÚ6kÂù—\ϼ‡H7‘¼l^5÷?÷! “ Ý£­[4b̈Ñl:Z*÷>ü8ê‚toÔ”WÝÇć£_ë ö<â_v‹ö H:Ø{—)ì^OõUjM€Õ&+§‰G¢Tjr3’øåãé íÞ†öÍZP?7»¾_A¿žå².yX\n.»o:OÅâ÷'Q·v}Ú6m¨ëîáçý¥¤˜µ¨%#iýˆãÔK{аQC^™ñ ÙÆdªÚoÊ^¯ß8 ÊDN­bíò_س{7³–/Æ©R£SK”:<ô¹v2×mŦo§Ñ²EG^þ9Ÿ7?ŸA°më.@GÝÆN­þŠ&õëѸc?~>”Ä£Ÿ-æ†>¾¢ä”ÀÁå=šP¿^}˜1› nzˆoß{—ËN±ÓK÷1¹°¥‘YOÝʲ]j¥¤¡Qù<èR«ž—>ÌëÜʆyÓ©›W‡¶Íšrþˆë˜·â)zPãñÖÞ ÑK9à|ßY+s;1áêÞ\v×½43CY¥#D· FÝ—Fš2k[Ùþæé²×ßžJèB”›ìvPêVñäGßqÍÍyúª4jÞ‚NýGcor‹¾ÿœÀbõ…™½9bœN•ϰR©u¨…MüÆéÓ'x}æ§œt«0%iÃÀ¤ “uªÇ/‘–S›½krûðþthÑ”æMêÓcÄý4í<œ‡& Ááqa±ÙIoÚ×&_ÍU_Ò ¯íZ´áæ{ÞÆê†>WÝX÷–¡ë¾uÛ. î·’]š0ßNxVœª4žxÿ Le»9¿a]Z4jÂ¥—^Ï¡Eë´pøÐaNœ<ÁÉÇ9yò'Oœàø±#œÞ²Ù+Bí{}à‘m€*22RX·à].Ð͚Ѥ~m.¹{/šÈ]#Ûcóx}ßt “÷K¯»‡Ÿ”’jÖ¢B P“—jÂ~lwN~}½Ü6®6—=òDpIŠ‚p)Á§Ž‰ÈˆTˆñR“#£õ/´å;I14+”m•µ Ž;ŸÃþ¥4‡"ʪ ¯•ÕBR>)êûH1æCy|R°Q-)x±:D8"úœÆL]¢6¤šb(KayÁ1RŽbR’>IDèq ¨³ô76<¥ŠŠ a6›ÿòy<òóóÉËËC£ùë¢ú!ØSTLšFƒVzþrDqZUŽZSj5jÊNïgסBZuêM¦QÂå T¸ YF¥7‘›¬eÓú¬Z³‰J§š&º2xP_’¼¬.7Ùi)œØµ•_Ö®át‘›6zѯogTÂCš)‰ßWÌgË®S¤5ïÁ ueç–Õœ°%ѽck${¶­§\•J·ö­^U ¡J­ArT²qÓvÒµ¦e^nìl_¿MFÚ6¯ÛëQN-’$„$‘œd¢²`?k~_ϱ#'qJI4éÒ‡ƒ»£²[(w¸1SÈÒ©ù}ŬY¿»ÖDÛ®½¹à¼®»‡Ç‹N£¦¢ä8;wŸÀi­Àî‘iØ´+íZäâòzP«ÕØJΰm÷Aš´íDí4#.¯'h‘0 œÝ³‰Ÿ~]OÓýØ¥6§¯WÆhJEï)fÎüŸ)ªtÓgøºåY¹d rÝ6 lªcPýºìj<—˜ˆìV=ЭA…²SL¼×ÜúÖ\ž|ç#šeH«Ûœþ}:¢ò8(ª´#!‘žFñŽ¥4ë0+íùnå7Œí× ‡ì¤¨Ì†Æ`$7YǦuþuwù×}p_ .–Êb6ïØCƒ–¨—eÆ#@rT°qóÒš´¦en.·­Î€«ä8w¡Eç¾ä&ƒÍã©ÖƒGy1ÛvìÁj­Àå´èÕŸfFd ÊNïc×ÁBZuìE¦I…«>xeAjzZ…¾›ÃîýG©Õ  ýF_A»T-…+ÎÊb6oßCƒÖ©—aòÓYÁÆM;Hkê§ÇƒÎ˜ŠÖq‚‘±ÒÞ—#ÇV’-[©pxBJRP>è I¨ìe¬]µ†½‡SfsÓ I .5Š\½‡³V$!¡JN"Ë ã÷¥óÙ¸ssè6híó²P©“Ðy‹™3oE}.C·:FV.]Š3=®­á²³aër›·£iNj€¯ªQ!i¨•jâÏ-ðûï(vªi×k£ú´åÉÉ—óÂû?é%S áEkìÀÒ­«éžçeå/«‘SѱUž0¹’eÈLOÅ]~šyó—²ÿÐq2ê6¤Gÿ èØ4›Õ…^«bÛúUìØ¹³E¤ÕnÈyÃGÒ9/Tóçï|DóLiõšÓ¿wGT^ÅÔÔI3srßN,_Á‰ -:žÇ˜¡=‘\vì.™Sûÿäp¡‹N½:“¤’‘$%§÷²÷`mºô$ìÃY^Àêß~çБÃXÝZš4ïÊ%— Ùã Äî@Ÿl&K§bÍŠyüî—÷vÝúpAŸ.¸í•HIf²4°kÝBÆ_|1Šõ|±ñ×tÍãTiiõ‰HÊ`N”/ÏeW€BEŒp|¬Ï•B×J¿qª¿k2†ÿÇÞy‡GUm ÿ·§%“Bè ˆ(ˆ¨6°+VÄ6°÷ví½‹½w±cWì"@¥÷Þ‘^ôɤL_ß3IΜ9çÌ ÷ÞÏë;ëy|ïKæ´½öÚ«ýÅf$a²Æë*űÀOs±Ž‘‚i°W’¨p 7]O–‘¬•”aZ‚n6z’HžijƒÞ°1* Šó´ŠÅ·ZáHß« ž­ ®Ýµos´1‰P Ñ«uŽ$Õëÿ-½¬ ¼^ï?PéŒDXUYEÃŽÓf39ï*ÑòÖc8,¸s (rBU½‡†@{#m6D#ˆrÒº ‡ Í“kë¨÷…bÕ°Ñ\~lâOTx=øB”=ƒâ¼,š|©;ªkÈ+, (­®FP´Žý{{U5hF^ŠDÀ‘A‡Ü,|aµØPDpЮ0‡!ʪ½-¹j†L7:m(· Ÿ<mVÖE'רmÑ‚åˆN°qÆ]S‹/6õ(2³òi­i*Þ ~ªj°7MÝqçÐ&ÓAmC,Î?*3,BvA…*šY^]E ‘HWV.ű†÷¾H€ŠZí ò¢ž§ºõôËÝϰ±îËhË¿„¨¨ñ¢lN:äçðÂe'rË{“Xô³—#úêŠÚašG&F”Žù¹,üýKÎ;ÿ|֔حw?~î#F Ú‹òÚZ"ÊAëüÄ}¯k áÈpÓ.;¯¯OC ê±vdÐ!/ _ÐG…ׇݦKw^!Ev¢×6¢4Ö¬ÄDpdfÓ6ËÕr`cF€2s £ôYWKC0‚]×1\îlŠ3[v¬1ࣲ®¥Tt:Nv^_žô;1z ú¨¨kÄaw•›Kù¬qôx*ýo~ñÏ_Ce­‡P$ !"‘KkW|Q›×߈§ÁÍ Œ%å̤MŽ›¦+„)¯ö"n;‡ýTxü´o•G„0;«k±;³hŸ“AC°‘ªº–ç¶ÈV…H„0ŠÖùdjøtm]=KÍbݦÒèG‰_”ÍQÈ‘ƒŽ$/ËNAv6"ì¨ò`³Û„i8ÁO‘«eƒÕ5„.7­s2ãéý¤¢ÆGÇÂ\^¼ìDn~ïwV-ôé­!›Íé[X@ž–ŸÔyð£ídr[”ÕTÒDgŽh¥¾?$ääR cÁÕ ^ê}!ìv1?ï™Ùlœò%WÝz³–¬‡üÝõù×\}b?Êj£…YÚ1§‰J’AnI”ŽTøIÇFCEÏ4jf¯š Ìa=='Ù7¥ªx[U—cWËV@JóØÛX튒l™÷šDÙ Dí AjS€Ì”Q1ù_ÃwYL2¼ßdÊ“Šæk7+E­pØl;¥óŸYHdÅÈ”2íÄv×nW<”ÆîiV´ã[• E˜*O-ú¼O›Mif£ƒ×S‹—xFk·ÙA‚TÖxâfT{jÛâBÊfƒHÒšèïMÊ¥ptì^ÓߌF€iˆ×¦ Þ­‚Oœ!m‹%Ú+ˆ¨ðâÜ÷"ëc©°+új)õÅã½ w6›"ì«þÞôlmŽª»Rø<1¼ÇîiB›Ín#䯧Ôßò|;Ñ*dgV>­í.BNðÕ  ÎëÁiÁ#€Ãœ”lн‹¢ÜÓмG͇VÂl«©eÿcÎbÁšÃøòÃ÷yöµOY·­‘½¢³Ó‰Äö=>,b·+$è£Ô㋞ؚ+¹›÷É›n³¨óPÚ´-s¨’ 4RlŒkÓ´§ú<Ù qÛ”"äo¤ÔßgAÛmQãI‚>Jk¢m»â¾Óãi¦ùŒì\";W3ìÌ‹é>ì_ŒyþFÌd—ÄUÒÚˆ†žK·V1TÊáež`ÜÙ´Åò˜Bþݨ¦{¡,vnì6„4k1˜;-±³e¨®ñh„~„°~GnÎQÊjkÂTÄÚÙì¶–~‡š<;»²hôRÚ¨—6ìöSîñ'ð!›ŠªÚvGpQ²=À]e5 Q<ÚZ˜™Í¦¢çU'ÄšxNƒÇCClPh´‰»@}mŒ—)ìJÑXç¡Ñô¼cxÞ›¾9GAÅö54’Ƀ¯|À°sÏfŸÖ™”U×Tª%uÆTJ,Ð2õ $뉅7UÄüaÆq¼W™yfM¨÷¢ynÍ<±Vʯ•¥RT¾“áðW©Äƒ¦â©Õï¹Ùœùd{m¶’¢ÒmöŽT=‹–9¹Fß©À¤ÊÝ\WÆ#H›ßm¬ *÷ôïœÕù÷tÚ[úr‰ÃHÊL’̽MV¨ë9—ÔÚ"E M’|·áá3h€›Ì2þ3á¥T,½dßžÀ¬½ ʦpfùò•D2ÛÓ§GBáHK¿4§ÓNuÉÖ”„é¹ÿÞä¹"„"Óï ‹•S@+gôçº@#žzT©IfÄh÷F¸íеL/N* ZR3ô²D_¥ÓèM6‡ƒP}%eõìѧ'î@#UõObR >Å`2oB>ż¯`*¡©] Ë&}ž²nšŸŠg$ö¿N§ê­kXS¢gß½ÉÏ‚a1÷&õi÷8/ŠUøT·¾ˆDpÅÒmà ¨¨­Ç¦l‰q+åÌÒ£§«ÔN…ÆRéÁ,Þ:1ž׃1šJâ!LÅ‹))ò+ù¡þŒç2E…|Wu+cB%‘#ôÎ'‰,LÅÓª’| UXÝJÆîBŠB †”ŒŽb¢)ŽP Ó«uÚÓù_R£Z½_ÌZ%ÑY* 2•ƒ¤í›ÖÜT’[nV‚רí‘Þ¢O ?&ãÐÀÚ:ßU¡žÐ2ÅBøY†Ô’LAp„F±1àÐ(Âì¬öÆÂò-ÌÉçRÔãNÙ+R÷‡4žT¦kW Ì›¨b¹¿vmE©Þ»gˆ?Ù5flÅÜ iU3]$•™JM«á`[Vûô錿®–ê`$Ö*GgÁ§d0©DO—•7Äp=’ȨIbL¦rnÍÖbÑ4;aBŽ©ð¶˜r¢_ FŸ=£!u(*Kxw‚§EL¾1í*ãÍH‚6É\Íf#ÔPÇÎÆ–|ø¸Ô£džÊdü/Îcn"°!± ¸^I5:Kºž—b6?1vVXy>ͼ·f ëd†©<2É“Ô+HÊBAQ©É»„Ææz#Īf\['‹‚dƨ‰wºyr“Ñ÷«¼ŒI¿C{ÞÄü̘½×´Ù»…†Ýü.ƒR˜·pKâÁWÚQ´3øGŽÁ´~’Ú<]}_¹Äñ Öîy-1 Í­š½s±6,Ö‡!>ülͼ „›²påMòÐ+>JŒGSZY¾¦–«$÷Ü%;ܦ« »*b©6›JxŽÝfÃïm I«æÞ©(%‰e?.²«ó”XMK‘] e]dѦ¦Ù[)5mF÷­L­)…£¶F‡W«VM‘ÍBSfÃÇ’šÑc5vN¯ÌZÒg¼$Þ+gêq²hŒ_»ÒÐ§Š¦½$VKü~ ‰“p”5£¦çºß­Îº‰a(VÞ&RØãd‘’”r--ÜÖÊ‚ÏY’‚Beq1î£hYR&<Í?‚YN¾¥b—Ìm¯SD‰)/Ë‚£ýcïbÒ",sFw«2ãÉc™”XxXÅ@~+cƒFRtXEHHæôÑ‘’÷ÚÇÿU´`ŸÎTˆ¹%l¥ rÀ Ósš[(ó<•&æ£4Ýç¦ %®)¯TÛ¤Û"‡IaÐÂÃj&°¤`Õ?YÂt.¬‘•ÌÃÕÍhÚ©ZyJUOSñŠ¥R F·R&:Á@×y«M{xš(Úñn@ ½j>\?X\ÅG ”NKQ¢Ó&$ÞfÔÓ‹±óBG‹zÅ)ާµ\¯ÌrSp¤Æ9ƒaêÊ V6ñ¥ " ¢áVi:&ù˜š}WcC©d‘$3œtQ†ÑSeÞ“V,”v£(‰‰7\¥H(õ· ­ÿ#•N¥ $Êý~±–IÊÔõop8«€LL-ß–æ°WJY­rÐLÃsÆ9ˆ éc*>$¢tkˆû[%4áÚ8Ù¯ýMZ¼À*ÉIÖ¤84?#fÅšyGâÿdðýºWÅeFè9Œ˜¬¡éßVß`è!ÔöZ³ Ý(…A¥ðÝó1Àµè¯iž¡c~ºµ6Ëe°7‰ A}+pnªnEid¹öl˜ã)áw3¢´ôòàÔ0OZtë6Ò'¯­òh@[ÃJ%Í`y‚^!Ð*)&ÞI°¬âŸcȳ$‘ž°àsVß4UDŸÒ¤Lóøâx€O1Ô;’ð3º5 Ññ0I¤‹„ˆ—Ñ þÖ¾nZ£ /1DƒÕèùF{gômJŸQ&ñ^Y9Ǔ޸ý0Úe!Kà úóeTÿ¨;§z>.HÂz¬ÎñzÕJE…Ô 5ïlÞ3ÃPl"ÿ (›2š¤•Î3Ø•b¯V­IDã%QŒ'Õ&q©H<’hoÉJçwµü×è™XHØdý(’6žKá7K gø¾8¥()Ò(®==ÛK½¢Äü™ÖÜB¥ˆ ã}ѯÝÜXÙ•F©ìŸð×KŒ“YL¡¤ï†?WªoäÊ7¯¤xžS=©–ûîJ˜fWûǤR®»ëï7?Ÿ»œÄ–ŸM•.¬è7•j«ý„]ZžŠÖ w¤pv’µ'IµŒÝ˜6­å¢ú“ûªvžÌxf*¼6U¾‘l½Eø3òóÏœ³5$J4‡RñíãÒJ粜ÿ̬4¤! iHCÒ†ÿU°¥Q†4¤! iHCÒ†´Ò™†4¤! iHCÒ†´Ò™†4¤! iHCÒ†4¤•Î4¤! iHCÒ†4¤•Î4¤! iHCÒ†4¤•Î4¤! iHCÒ†4¤!­t¦! iHCÒ†4¤!­t¦! iHCÒ†4¤! i¥3 iHCÒ†4¤! i¥3 iHCÒ†4h tñ4¾øæ¼iT¤á þÏÏ‹”íÛËÈ+lCnnæ.Ýë«® ¾!Bn¾›ÊÊjZuìL†C¥©êßaŸ—²mä·ïLVVrrm¬®ÀSÓHq×ÎØÿ)[ô±sk)ÙÅíÉÙEýŸ…†*ÆÎ]J#ƒ¾mŠi›•CvÛ¶d8ÿì¦ U[¶vçS\\ð§ž°sÝ*~Ú¼ƒâ‚Bö-È'?¯€üâ¿DÛå%äuH¶ÿ“£rËv² ‹4ÔpäPÜ:?émþz/;*jiÓ¡ngš¯ýàá+çÍy>Æ—x9¾cN!iøŸûÝwßýPFFÆüE‘H¯×K^^6ÛØÁö3ù³øî§ |ñÕ7L˜0‘ñ?ýBCf'zukÃG?Ä”Jï½þ’itßçH|yGsì¡]vé5_~=Š›Ç¬b@^¿ð {ô;nN­ Ä—?L ÆQL—VnÖ¬\ÈØ)óÙ½G2›4¢p ßÿÀ–°nmŠþÿ(vå›ùbÂïwíAŽs×÷fæì_ølú&úõÜÇ¿yk+Màð;ž mϣطSn¢nR²’¾þ{ë=è”ïbÜwï2âÅ1 |2…®¿ö1KMfúê öîÖÂuü:i?Ï]Èäyó™¾| Kv ÙèüT ±tý®»Šöfà^íÿqLhÉ¢ÉLÿ£‚½»v jíBN¿ûA>˜µœ²?–óö/?3zæRíw8ó]Rq¯çæ›®ç«jg¼O2ŒóŸ‰TÛ‹èÒÊ ¿ûÁO½Æª ›˜?w&Ïýð=‹Ëì =l?Ô_ íÃî|‚6=d¿Nyÿ9×oä¸!/·= ~{‡—ù8÷Ð}’Þ¶èý8äŒËè?üFz:ùå›7øø‹ErX¿û9×Ü?äŽ;îäù—^ã«~ÅVÔ‰>Ý;5ÿ¾félÆþô_ü ¹{LÇ‚xc,\¹™Ç‘O<É[oÄœeë9èˆÃÉqÚMßùß\ß_…ÂvíiÓýNtÌŸâÙiHÃ]/À?5¼^ÇC\ÎÍwßÃÌÙ3™2åw~Ÿ:‰u%5€0æ¥xÿ‡©¨P€o-;ü¡]~K6vʃÔýx‰›ï­¨_3‡+^{ƒ’`”ÑÍšý —¾2šÒÆpËE¡žõ¯Ì\ôÿ [Ó'Ç¿FÿHXý9r˜7o ¯ü8 ¿Dÿ½bÙ|f¬-ý79i„­¡µaãý©Û¾×¾üšÛ¢¦@$È6ˆPø¯¾9È#/½Ê+‹7ÅÜ>;¹ÿ×x`üd•—³hÃZÞùø]\w 7¼ó-áÈ1H˜¡ 5¡Èÿäi\³qão0Çó˯ñÊ¢ÍùxâWL¨.fáïòãKÏñÒYǰ­zµeS%BU(@e(ù9¯_3‡+_Ŷ`“:YÉÝïa¿/fÞ;o0þ™'9jŸbfï¬Fý%ÚŽ°5 ÿg7ÀåÆ Q PÛ$”™šW5P_×[ƒ?=ØÓÆ}Ä /}†ÿ?L/O\~‡ ¾ˆ`«^œqòÑl[·„3Ö5ÿþÛ‹qÜ!'pé¥óØó’ŸwÿÎÙ?±wÇ®<ñõ|Ž:áLzwp0ù‡)ÔÖY¿wÚ¸xáÅÏü7…D‡þüíO½Œ'yvYÿ烕iøƒ¦Òi·tÁ>#îdöìÙÌ_°Å‹qûA„1›73mÔÃQÆË0ÈÒYÀeW1{öKÛ7s²È,Ì'Óa#SZÿyÎ\ÚvíÏžQ/F^V.ZãTšëlŠ6mZÓ:+Kk§³~årÖü±žÆz= °rÉ"V¯ß÷×Ͼ`”é®_Ã¶Ò ÍZÖ°|åjj*=†ëøzÎ ;îT:dFÉ!TWÛüÛÖUËÙ´qG‹wfýV­\wÿu#nfíã7ïî®ýà;‚ÞÂþ`óu5[7²léJ*˪Zª@€Pm=¢aÆ¡Ú:"(GVv¹YtiSÁV-ZJ£Fi/ÞÿV¼ö2÷m €Óî$?#ÞVUZJ½O#ØÅÏÚåKYº|éÞ†¶-çF7×tlQ!aÅ%§ å³;nãóbɨ×yþÌCyåõçx쇹ñ^¬kX¸p‘&=Ñ_O}AV]õi˜p_ ¸\¸u–hCéf-Z̶íUñ^ž€Ÿpƒ/¦+—°zé ‚Á8m‡€§>ö]5¬Z´„:oŒÎ">V/]LiiMâ·Ö–³xÁv”ů#TWKÓn_»’ ëJ4:¥7Þ{_úOM&â¹!“ëN[”‡Í›Jé²Ï^;ÜÙ Ü«;íª…Fj«X»b7”l^=k–-gýÚM›èO)²\d;4ç"5У ìÖ­°;mdÛSd£QžäTѳøðëãÙ¼y,ù±…ûê}Í×nXµœ Ûª >ºŽE3g°a{UJï{ÇÙÜóîXFÍÙÌ—ï½Àm÷?ÎÒ«xîáó›¯Ùý¸!ü¥—EßpÒ §ñËŠèýoÞywÜõa”Ÿ.þ•!Gö¦¨¸;w 0§-¼39îY«Îàâ!Ó¦mG:´ßƒ#Oʇß,àÃÇogŸ½÷㈎aøí¯à•?'"V/áŽKO§uQ{:µoÇ!Ç â¶W?`æOqæi·QU¶‰»Î?šœ¬,:ïsãænŽJ ’ÅœzÌüx’öÙÙä·nÏ5O|І4XÃ?ÔL"A°; Bqa/7}4ÇžÇwOÝËÞrOŒM3òòa<õõ4l„Èî6€ñã¿f¿âÄ\ºžÇæµvºwÎçÝ{:±[Ž–›‡øaö*úx.nÍ!Á="-ooز’›_yƒY¥Õ(_=­ú ã§»‡ã|¥«¹ä‰YYæ¥Ñæ¤3†óÂEC°¿ýúÏO\N¯®…|ðýDn¸á^>s ¾ò,ÎZE^8Œ£¸3cž|”Ýr4Ì·v-s·ÕsÙÙ6{žÞxó)~ñµ£{V?͘K½Êç¾+/¥mõzîûìvFàÔc‡ñúµç’í€O&|ʻ˳™úèŒ÷ãêZѹj9½¯ºž.¹šëÄ;¼Êsæ QŸÛ–Ñ?ƳY>a·~;•7^~–îÙhØÊ·<ÅÑg^Á-'÷E);mò³˜0éKÞ[;Ÿ9+·»[>ºýVìÖŠš?æpƨï¸úê8wߢ˜Þ!;ÇD¸uäÍLÙYÈ·ÜMv¦² ¸ìÙ7ØVëÇ©„ڜμwû­º{|jÜys‘¬vœ¼WKèÜf·Ðhoª ˆ›ÿuó–­äÅq?róƒÈU!^û^™°›-Œ´íÃÄÇï¢ch;Ç^õç]q7×'æ@[Ç17dØåpí‘Ýõö3¼4a1vFÚõáûn¡Gë,¢ê–j¡Q ðÞûoðÔ¸™gePâ 3ääsyá’ÁØ5“&rÇ¿³×ž™¿h ë*ª)è¼Ý}};çáÛ±”ÓŸüˆ^=ö¡|ëR~[±‰Ž{ijCŽâçß¾eôµ83ZóÈõ7qéÑo]9w<ç¿þ ª1D¥ÍÍíWÝÂuGõBŒzóIÆ:±WŽ¦Î¢2Ÿ}Ï ?Ži“¿å›etn_Fßë¯cÄIçñȈãðLV;ïUï¼õ £7†(l³Œ.\ÆÈÛîá‚VÙ l([4 ḩ¯q×÷sée§ÄëãØc†ðÆÕçâ²ÁöÕó¸òÕwØQíÃ[çá„3/ãå‹N[‹á ÃÛ—rÐM#9ðä+xãââÏíœUôë=·þË9ëù÷pòñoðãô.Œ}ðv²\ì6=Ê¡RþuÿSüº£Žü mA7Þp3 ܈ð˘¸ë‡É¸ ²!˜Ï“w<È%ýÚµþ˜r÷ëg/sþg‹x÷ñÇ8¥ ;ßüöë×<7~)ûïVÄÔÅË)©®%ßÃùýþ[ɯXÁ>7=Ëí·=ÎÕ‡íU`·-æˆ;_â¦Kî䩇î"§uk"{ÞÊàpVÊ|4JøQ~ñÂ×óéD'³æD&\t_Êz$§f_|÷;áŒ<®zb ¯ß|"6`ãŒ9{è9lçPSáኧ>â;†™¿nûÎ~æK†>øWÔ… V±÷¾½ÈÔéŠÝöé”óÓšN¼ã„¸ßÞ¾íbVgtdãÄwÙ¹v~w;ºt*JÉÅbº>Ï&¾øöw"y\ùÄFÝ|b /]0/&NgÌs·0õ‡ÏøeÂXŠ~žÄ‰½Oâ³_%|ÜÛ€—úbS›<6òqº;ùâÏOyósvÌý–}>“­ù×Ýÿ¢0²‰‘O&·¸ðÌ~véÌ~ìË‚q3m‚—Ÿù¹»ÃÙ¹Ãûôca.¾ì.öî(Ü7ò)æTñìõç²sžûS&¿HC`ON?ó>ÿô+N=âD*êVQèj`ܤï˜qÔoTûê8ýÌ3©™ý3oÜ3‚N»ïŽçôKkVi0‡ÚÚZùo@0”M›6I0ü/¼­JwpJqŸòäãÉÓO=#/Œ+~©‘> ÅÇ]"""M¿ ×=;]DDÆÜ}¡€C~Z_&Á`™˜t8úJ ìâDv,‘þg—Ï—”7ÿíëÏ^–öC¯uµRzÝù²„E$¼c¾ì~îɉgÉ£c§ÊÂ?I¿ ‡ ' –«ÞüRæ¯['g^u¦t»õe ïX.{ 9^.þp’„Â!ùøƒ'Å1ä"Y^¥¥GGÞ( 8Jö¾åI™¹r<þòÃÂI#dUUX*+Kåòû®—½nzB&,]"«¶—%àù”ç¾l:5²aó*9ç¦K¤÷-OË„ÅKecUT.øEö:ç<ùuc£ˆˆüüÓòö¯ eG}ƒŒÿê QGŸ,?¬«ÒâÓ× Ãn—²Ÿl.Y'óÖmŠ‘µWF\0T.3UÄï•cO$…W>&ëêâOvdgü¹ ù<2iò—Òú̳äÊ·¿•I+×H0–§^»SzÜÅÔ•È;~*Ó·—I¹Ï#çß2\Š®~BÂ""ÁͲç ãåÌ7Ɖß_/3–/“?vxEDdçì¤xÈ92}«HéÂ…þÊ _L’° OùáËׄþGJ¯›”ñKþi“ÆJÁÀr›?‹H@Ž~’ôºó•æëǼñpÒ%²ÆùS\tÖk× ?¬k‘KOÜM`/‰J ¯œÑ9jGïqüÅ2yê49§¡@W) n—ݲ÷ÙwIc$ SŸ½QùhÉvÓ÷}÷Ä ÈÁ'-§ m 2¥ãƒdöüÊ„k·Ž}FyeÆŽæ¿…½Û¥G¢Úv‘3N;Eúöì!8räŠk?P(ùz›ÖçiZ_—èúzèÖ·¥!ñÞi/_.6rÄçÉýç €ì9è&‘È‘2ä¡IÕ"‘UÈag>d"4äø.™{Ȫ->ʉÉ+q—þtÛ™ûÊö?±¯÷žÚ] C~\Þr÷Uó„Þg‰ˆÈÔçn@2: ‘1õ`òÓW ï.Š„6HANydüêèžåR¤½†\+IÃÿ üwõ2‘ÚÚZùÇf ç:(ÿc)ï~ø¯¾òo½÷ ‘˜s·0ZåçÆù7m™Ñúõ‡P܇ºÙ3ø`ÌlZwÊcû¤‰TïbÜ`îü¹x3Ûr|ïÖ->‰6ìqeÕ*æùŒ¾ ¨0|eŒž4ƒÌ¬Ž¹w7€gíB~ÜVIÿ=;óÝâe,ªÏ µÍÇ”UKš=´¹Å»ñårÖaÐ>Lj“LGˆŸfN¢¬N°_ò2ìºÐú<:õ8€.oB(ä£]Ÿc™øÀÕôï¾;ÇìÞ·»Q<Ä Þ{2¬O/ºæ9ñ£!묌,ò³2Úvfï<Ùym9n¿>ìV˜‹R6ºeÙ˜¶h:T62 OÚæF½VWE99-•æÊN«ìrœQ'¼„ƒ”Ô„¹ýÊÛ¸ïÔÃèÛ³gt*fgcC4ôçrÑ:7—ÌXæ¿ÃáÄí‚§F½Èí?­ãÝW_cXß®lš?“©Ûyï_7³§ötí´_ÞpžmËùrÁ¦ÿNýFfmªåÄCLi¯Ã.;™Øê¼ü2s>íöìIcùz¾[¼‰ŽÝ;°hV4ôþÐÙg°}ét¦–F)ñÅ_¦qÌ©ç±[&|1a íöêEcYì¾ÝÛ³tár€=#º9vWÔÛþÉ„ßè~Ì0ž:m Ú¶åÔ!—së1Ýyeb4OÙåRäåµåûGrÖÁ}8¨ïá[Äþ‡É¥};7ÿ¶³¼ŽÅìݱEñÉíØ†‚üLJÊˀݣù[óæ±Ý–Ëà˜²jqŒË׫¨G.™ø¨à¥>' €ÃÉ5úv;øzg¿ËwKVs„ &on䳄@£åÑûÂp¸¸vÀ~8ˆV¢Àf‹"©ªÂÇ€»Å}Ã=»Qþs4ç̦n·;® OûÂ6´‘Ô… -|’A·¢‚^j!öhÛ"Àë#àrDï/ ‡qgg1iÒ÷L÷…Úá˜þéÕ*ª7ütìØ6±×U44bs8Ɉý»ÎFI¨9I 9Onþ\¶Ûr8µïnqç³. D‚Í×Ûí¶XzC”"OË-ŸŒ¥°¨öFpç‘kŠº+†]AyÅkÜòÄ}<0z^¾î.>¼8Åy¼=}"áÊ:¼ÿ5Š3ŒÏm§>GÐJsnêêQÊFCCK®eS¥§xxé7xgÎ&ŽéÚ‰å^Úçw"„Lg1oÞvW¼úG_y!ýžÂÇ7_AÏ6¹(›“"[=Oýô¸÷àƒ!GY7‘ÙYn´z²³C «i8êX:¾ôã–¬eöÛ˜W“Í3ÇþïÌZŠƒúzèÒ³¥ Þãm²Éª6o`úOï²æ§,ªƒµ p8gö2龩bõOK(>ìR>{ó¡Ø÷âÇlñ%fÖOüi*îΡ“¦O‚p˜çœÎeý ¸÷ê³xsÊH‘]÷¯xëa7ƒõ‹wì¶\íyôÅ{ÉwíÇ/ÏÜÇï?îÍ]=M¨Íì]厙ËÀ3GñìSÏóâÈ›xqäí\ýÜ7Œºe0Á²¨Úë„´ˆ¡$½ÿMåôuU›©ŽØëÈøˆçö´Š"ÓlÜìÓ¹¹±Üé{C0šÓßg¿¾Z.ȺFè’Ÿ4¤áÿ Ò©ØTĘs˜‡á`ÔsÑWëcX±ü³?Ï›·ðÛúZ†^ŸÛ²›#ouež=›ÚäÙ¬oÑËk™ânÍ•×ÞÃyCÎæùÞâþŸç˜þ1`÷ö¬+«æ‘KFpïI}y6Šp(H}crÍÛ{öIÃvĉ¼ýã—\ýÒ›´iÛ‰‡Ïe$˰9”Á3ý÷Ž{–Íf#ð·( Jiˆ=[¤) µ‘¢)Ž ‡ÃdfÄçõ?ú$– <‚Æã¼§^å®Öm}Ùì »©òÛ66V×1Èéhö¶(ÂÔ6Ô…Ķohl °h7žºð î{çCÎ~¹-_Üp6­ ”ÔU³©ÂKŸ¢¨ ïßé¡ÁÛHÛV-¹›ßÏO»ÝûÒM—Æ GÈÈÔý±dÏ›Í1§Ý3?— _î¬üòìu‰çìÀuÇÄó?|JùÌ ]÷?ŒvËB4ªÜ™GðËs×&ÜV߈9ë¢R?·ÐÁ¼Íeñép·QÔª[ów†CB®»%§¹¦®ŠjÜ:Ø}ÍGÂf³á h„»H³–Ñ^ ÒSÏÈáÀÎ#ΦlñJ¥Ò*K!”33aç¾›3ŸvÝúÒ53¹b““9ñÙW8è*>½a([fòÓµÏRQQg/êÈý÷>ÎÕÖs×Û¯sÕ³ÏqÈþïÓ3×ÍŽê Î:þ D*¸áÙûèSüCûv‰?·ë< ½¶¿ËÐó iNœ=a 7™Å'¯¾Æð>ùèãG¸z|=Mu…»÷;šßFÆô)¹àñ§¹üíÖL¿÷B2TˆrÉá… G0iòç|ûc¬|î^Ce¸é‘h»FùËË‘¬<¹=¸î¨ýyãëÑÌrTsøÀã9¼sÖ_à¢bèñlŽÙÁ¯QÄ›þ :Fó(ï5žBIïæ5ÀÙ'ŸÓò¼mãù=7wžŽäÖ0is„ó¯=?îÏ‹mòS8^óЋ³wyõv; ÖgíwgP{Œ_À±×§¨K7ÎìÛ™—Çþ@ñ©×ÐY“2~Ò9×pÒ9×P±v!ž~2oÜzܤm›¼˜‚ç¢7|sÏe¬ ÃqíâûªF"a “Ö»º&=l*77ÿmÖØWùvƒ½b9ñ1ÚÖê¹O|p3´oÄ Ý‚šdÛußbIn<᜴V•KøÇ†×ƒAðûš_Dú ØÜ:%ª˜ú½ÑŠíkî¸ßêϹêÅo£Œ/3-¢rúhlY8Ÿj²8§ïqï¾Ï^dH5÷~ô!Þ†¨PÿÅL­©gÐÞ½˜¹p&SWn#·Óœ}à>ÐXÖš ïÇ¥};p߇o1ss´2½jÛ&Vm) ö0þPQ-¬qËòŒŸ²[v+®:ö²mJê›ÿqîrÛíÍ>­íq"% á‡5JCôÙÍ*¼„ †BÍJP8& iž Ì\±’ªÚè»j¶¬æÇ)3Á•Í©'ÅYvÊê¢ O†-†òÌX¸‘«qúÃ/°½^Ѫ©›@$L $ÔTò‹ …6õ(’` ¹e‘?è§¢¶ž ‡gÌ­#ørô³\ñöOÑØAè•çãŽwÞ¢®1Õ\ýî‡4æwã”bžÃH –—2èà°ÝFYÙvªJ¶Qºn ¿Mù™þ·Ü‡·U_ž9g(rÌá”Îü–»ÆÍŽ ©—%«×à‰9ØÏt, fññ”e\tâà˜WÀι'Eé¬o¸ëÍ}¬¡pÚÁ`ˆ@cŸ§y(+&|ÊGs¢mdÏË#WpúaGØb'è¯åÅïÄ¥ðÈg?Óï´Â~ÁPˆ¦D>Ôîy(Šw€NïŒj.yõu¶ÖEÓMkײ©¼PDÂ!üšvF‘p„@ D¤©J;¤øcÝJ6h«Þ#ÕL\^Ê CJÀs8$ éA%! ¡ì€¯’ ŠÞ]w'ÃSÉ+_Od‡'DcŒ:§OŸÌ‚­Õïµçî»õ±Gy½ÕHnþuýíÜÐ+‹a·ÜÄ/«KãÎm•J<·"üÁ‘ˆh «±;[«*!£€þ»Q±y5ïLZ„Ã!ØnØÁw“§'‡9ˆ}Úº)©âÁ&!JË«éuðé|Ï]8eà½ÏQ£• ?~ÂE÷?ÇÚ²è232ñÔîäù±Ó(_3ƒçÆ.fØÀCiRëF :ŽÆõ󙸪Œ‹š:˜À÷£Ÿáü ¯be…qŽHlÿ›–`Lòkù(„B!ÀOM ޏ˜Þùpù'2s ›7.g¾®ëF3dÓøõûÑ„êw2ð°!dt8˜»¯Éðã­¯gîG£©:õì„¿¦¾ùûöêÕððéĨ'î—å¹ïÖrÏ»ë^¿é*®¸å%|fžäpt}Ò$'üÐÐuÓúŒ•ÏZõŽFŽìõ"9øöëÜ–L`ÞGrÖ72ia´ë@ëݾ¢+ÆÖmÀ«w]ÉŒy‹xûæÓúÄ·¸3m(½Åî¶0}é*V¯YGcŠí—rÚô¦Ÿ¾zå!¦N™ÇøwbÀiÿ"¸b†›ËáÌ[¾™Òkyñɼ‡áŽh‡|3o#½]®³€GN‹ Ä®à„½§pé÷Ñ6§Ó:„ Ë)µº‰Ø@ d4}¥ÍFHEhê;ÓÄŠ›ÚP)Q}l ¡Ç]ÀG·îä‚—Ÿ¦Uq>O>￟!O¾Ê7—'5¬«q0ú;éÕ*ê>ò¬˜ÏF¿ƒGÜWGR6ÚðÃÔ ü±pu ^*Ð÷–\y=ÚE÷hÐé—ñQ…‡«^{’ ÷ÂU¿Hæüü܃QAÓûŽïÞž÷«ÛpúA½›=Iǹ”vÖDïûu/\õÛˆdìÉ”× #ÃIH"%J£Ãκœg7—qÓ÷óV.¬[³†Á§]ÅcgG…\Pl·ÊaÑŒqœ±d ¶o‚އòí5Q/„/!„ljÁ¤„P$ŒM#MôàXµéŸ‘0ìéW9ôº•ìÑ&›õk*xñþçèZœCˆN-½Ø¢4a„çq2_O}.¹œÇo¸ž›N8œº• Ùpðhÿ> vp¤ÙÓÚäE"‘ ah¿7½¼3’±ßµ¥w—ÎÛ·¾ù2ƒÚßÍêßsýóïqÈîE¬Þ´;®ºž~ù€ø›ªò¨’ùÒ3ÏRrí œrçýLzñ9Žè–ÏsçÒ¡óþ çÖn³aSà×tQ8*é8ˆC',à€K.ãíé¼{6/ŸÏ…¯ÿÈG§wåé7ŸåîÏ¿#7RÉ&z0ú¢!Qv(‚MB”ìÜïÏìg Ïsæ mùõŽóY²~NXÉe—^CœØlNZåfóã¸O™1e,Ë7¯§Ë!gðüy'7û"»|8Cz|Ê·¾=8ã€n–|jÜ·ŸðÉKþÀSìÝ:ѽjsfÄæ‘°‘˜Òå{4ý¥Ù¹vêꀂ6L˜üCN:ƒ»v¢ënÅl*)å¾ÑSéß½£$*æÅ—nᄟ§ÝSɨ®v'0gÚ´‰yvç½ö$ƒŸ|OYÔËÿð‡ómÿ‹™ùËK¸pÌåwr̨q\~|ܳ;7–qÏ3ŸòØ¥ |üò[Ì’xò¹ÉTFJvDÃQ\(;D"Á„õ™å,¶)ì,ã¨ÁÑs}âàá®7éX¸?ÅmÛðË;÷óÕ;/Ó±s'ü;¨htrë+_Ò^y}ùòÑë8ã¾×8ì €,Þüì'Fßx:[7Ç·kÊ-ìüÄÑûí3ûVl_@<¨Ø¶•j—=>È pçÑ¡m1ï|þ G »#Š|O¼ôÕsÞçéofPغ=àcHßd« õAq÷Ó|üÈÅQþ¡ÜdÙáóç/fâGùT–{èÒï¾üêKÚ¥Û†¦!ü3«×C²aå2Y»Ù¸¶oãêe²fó¶hÁ ¿^–-[&Ûjêã®Ù¶h†|;æ ûãDY_R™zÕº¿Lœs–ÜòÝ<óÚúÍkä‹qcåÃoÇÊÌÕ[âl¨“E«–ËS§ËÌ îÈìùseìÔ™2}Å:ñ4Fñé­®”?6o—€¶ô5ì—×ÊÓ§ÉÏ󗉶€³|õïR|Úy2n­7á ¥;·É†UÍÿöÖTÄ=;Pï•5[¶J] Z§X^¹S–—”IHS¶¸sãjù~ê ÙX-õÜV²I~™9CÆÎ^ º‚a‰x«ä×Y³dæšhzié6)ñDï 4zåÍ[¤¶!Ø\á\²m‹l©¨‰Ò•îwoMÚ"ýÕV˼u[[Þç)“§O•ï¦Í’ŠÚøõʽ²ç5Å:hqíÛ·ËâM›eîú 2{ýY·½Ütw®[-?N›&?-X"›Êkâ+ø*JeEiUê÷…eÅæ-ÍÿÍV,–¯›$3—¯ûûâï?‘݇^$³×îKæÈw3çK½á îy‘°l-Ù,[+=-ßQ¹CÖ–VÄœ·R&Íœ)?Μ+ 7l“Pؘ^½5òÇæ-Ò !FOé&ùqòTY]RÞ‚ç« ð,";ʶËÚ-g.Ðà‘5[¶H/ö¼H£Ì_0O~^¼*úïšR™0c®TÖDüõ2oéb;ušÌ_¿MCda)Ù¶U6Wjö"Ô 3—,—õåuñ—Ë€sÎ’[¿oÀ¼å-[¤´¶±?å;díŽü4Tn“ 3fÊ’­Ñç¯Z½X&.^'©.ß.ãçΖ¦Ï‘Jo ÿk¡í¬Ù¹U¦®\-Н­’å›¶JC0ÒÜýb÷áÿ’¥;eÖìYòã¼e‰•îþ 9ñÜÓäìw~NÊ«*•O—l3ü½ÑS.Ë–-ol·o^'ËV­o~§–Šˆx·o–eËþˆ;{⯒É?“¯¿üV¦ÌX"þ$UäÌš"o¿òš|þmb—ˆÊ Èo¿ý*ÓfÏ•eË–ÉœéSeúеºòØrùéËåÍ·Þ—åwÆýôÛ+ Ξ²Ùoüî”×gÒ^ ~ÇVY¶l¥¦z;"Ë–-“íž–=ÛeúÄŸäƒÞ“?ûJVnhéæÐtž|[×ÈÄ_ÆK”3{Ä rúí_Å¿«¬T–-[*³fN—Y³–‰?íÌrQßVb³;ÄårÅýg·)ÙãÌ››e@éª92ö‡ïeö;b<±DÏ[&""_Þ9BÀ!ï~÷«LžwðÉ7_%w9åzs>}Cè{˜L, þ½ñüÅ”¿×¹½âz™»Ýÿ·Ý×÷Þ|X8øTYoÚ¿-"Ÿ¿ù Ðÿ™±µ!éó>{ô é~Й²µ!ðàl×ȧ$ƒ¯}R|Óž>Ë|SŽ=ù2™³6ªäÕíX%§ôí(à–q+«RxB@VÍ›&“'M’Iúÿ&O–YËÖ¦ÔÎè­+N°‰™«Å³n¼ò¯×–§EFZéÜe¥3í ÿ7CvÇ~Œ}r² 3þÖßÙçøsøæ„L\é-Ó€“Çîy’¬‚üÿéU´ß{_nºèº¸ÃÓ š¿ž³;öcìÓ}É*øûžˆ>}ð¯á]ȉèû´¤5æ·ç¦od@'wòçqóîÜ›ÂÿRÀÎÕO¼OÏÞ½þ¾ç¶kgÖÎù÷x—:Q¶½„PvgžígNéU˜Ò¹êÙÿ0zþÕ³ëZ±Í­ ލШ°ªŠ4¤aWAÕÖÖJnnîüE¡Pˆm۶ѱcGŽ´®›†4¤! iHC<X0ck7•׺q$­ÝÿÝ/ðUogs™°[Žd”‹¯›7R¼[wr33Ó[ö? ÿm½Ìëõ¦•Î4¤! iHCÒ†´ÒùŸW:mi´§! iHCÒ†4¤á? i¥3 iHCÒ†4¤! ÿqøÇŹ#"¬­®!cS ¥ îµyø±Ö{¢ŽˆèˆF‰Ý£ôÏÐý "‰ï”h߸¿Çî4÷ ® ¤eÖ¤öñK‰þK»´Øb—Ø;Z$ ”hž÷‰ñß,¢ù@ý:5xŒvôTqxƒ{šžß´>Q$¬JaVšdÊÓ’]+ã-Úl:9º5kñؼ"HNÐñ2ý5Mg\ûº‰Ãƒ–·6![÷f=)4ñE+ i¥ùŽx/ƒÑÈ-NˆI´üJGÏqgUâù›%yÇñøx~§çfû¨DóLÏ' l+·$ +ánƒ¿È£#·NÝ´,=ž’ŒøªèF“œ5åƒh~×ð¢æ3ŽNGÐî£ÄIªæ)†zŽN»…Øv•)¤•Î?§tÖäØì¸TT‘Ñè*‰Aiô‚ù»PÅ+2qÌ^Ï8•j~–ö} û{ó}ÊR˜Å¡¦{”±Ðob+q‡MijÑo5‹Ð*°-ëS‰‡[%޽4{(ÚÄáZǼ58[ËaÔ>/މhï×î«J>‰¬‡˜b×´Ö|*¥Ãf”v1¶˜bœ°‡1G‚®e…ö'ÕBÊå¢ãÇZC^kÅÛ0ôÈ5ƒ=SDÏŠç!Òüš–½h9Ë-+ŒÛ'1 KñÓ·›h<†;Ñ:R44¥t¼8r”æ:iÌ@¨‰DÙnZéüo€Ëf'ÛéÀe³iò¦™@z!fDÏÚ ⼞ÚSÓlQZ-Ã÷0îl1-Åáµx0ægñkÒ„ó™ä®™‹ÄðÙzK×Ä‚Õ9 ¼Í÷Ú;«+b´IÐ= ñÛìÈn³cÚ1ƒX£F+Ç iF«˜êð®Äìñ:¯´˜0#1ÃFƒR†î:tÚ‚Éž[ QÅÓ˜hqiHëSI~'šÓ_j tS¡û8ûE´^MÝÁ2|Žõ~™-Dkü}o`ª§xüÙ´z¯Cå )í%E¾Å¡P:o˜ž>Ea¥·Ÿ¦Œ£Y*Î ÇZÞª xIk7:‹Z%Dä2Ä›è”EG •ÝÄY5V·и$¹Éš|[¼Åqôc´/ñ¬ÚÜ‚±Ú˘¢¨÷¤&ò*Ñ(Ò-7+ý>Øÿ„¢¡R¤w#Þ‰ßpˆq/K$Ω¢bøw†£c«S¦›´ÒùW!Ê("Ò4º1!¾¦3±4œA4'=A ˆ…Â#:‚½)­y–Þ’Àq,u³Ö!q^©„ï7“zI"”N ‹§±–AFÌž´ÜXÏl­±¤‚3í¾ˆ±r,˜˜Àbü½Züi¬ÑfÃŒ9}kDsmÄäÛT<®šB~ î±Ø«¸’ÖÑ]Ñ)KV8×>'éuƸ46Î4tfÊÐÅœÎÍè” A18ÛZ ""xÕ ƒo7ZUüÏê›ÀD“2 eòL#A®çb$ð4†5IèN,ì‰H”G‹J<«*BüùësÙì“D:¶2ö%¹1ª=gMi@Z\ý-g˜È„wëó^tçÄÜHDPœ7ÛD&˜9 ´r…y±Þ`ÅbÿÅ€ç$|c kvæˆÉ·âuú3ª ´þ”pj®üËJ#zP:~‘@(ñ4¢'‰FnÉMè>Òäƒý†ÖáYH$ñÂI™\o¤¬XúzkD$^áL`øb®\)e­¨)•èžÔ@ed¥r¨LCIP~,ÌâDó^ë%#A!ÆÞ‹T¿Ûˆ¡ >C檬Ÿoô½J‘D‹Z%ÁgB2¥ =XùÚ\OCNalä¤b…Ѳ‘§Aì“2PèÄHÁ1 m¥Ì½/FxTfBÔäBÕlö?CvÁë¨sI¨æ°œ.ö­ÄÚpQ&´® x‰•W&´oúéÊPJ‰'(Ì_¦LöÜ*'6Ž-ˆ!OtF–˜¬ßˆgÇí‡Òå“Xð11œ´•ļIF@¢»Æ ýÆPÌ÷¬i]Ê'¢;’¢¢(Ið¡®²à'V¸43ꌌY+ÞL2£ƒ˜ºÅ÷¨ο©l2IwAéÞm”ô)ÖkE§Tš1•˜û*f®´ÒùŸptj”0eÀØ•pMº7ʘÁh™£JA 1U^%!L¿1?ÌFŒX™ E’oJåš1O#¯Ž2f&‡Ha”T•OIT@ô‚T‘˜8'‹V&LKRdЦFƒ‚§0(¢!æ^e±ðn˜1D#ÁŠòiŵñh‘DÁ&bÌôÃ(ƒ©`Aâsд¡)ù“Þ+ZhòÀYyq´© Ê@X(³¨€˜+¨J¥& ¸X)öVÞ®¸ý–µ›­ÉŠ^ @1~¿2Qh”‘gÍçš/4JÆ ÃFvU¹W‰û£WüŒ aeP¹ª’yÑäû˜Ñƒ¤†KŒAe$›Œ*1;ʯfމ¤ùM:·Ò)kfIŽÊ@UIø³X(óFe±·ÊÂS¬Œ #“ç+GƒÒÉ^#¶ý7U6ÿÁžN­wHÅ{DÌ­9eBdªåJ»Ñšë•Y~Ÿ˜¹Ú.ÑýˆµB£,˜ B’(©©(šÊÌ¢J²6•ÄNrx™‘Ò(¹MÕ]ÊB›y·%ži)Iñª8TŘT³b˜Ì«`$Еλlh}‹µ7ÙÊû—«-l1PjÌ<³)z“Ó›¡tIâ‰5bÎFž•èm4ª·ôÖ)R:£PLŸ®‚P’x,Åȳ˜ÄHh.¡:Þˆ¯ˆ÷9Y@™(WÉM,ŒM+o›)})s¥], #ºS&¼ÐêµòCidF¶’ÔÉ<ÎÊÂðµò\k+ûÄÒ ›`ÈIb¬µsÀˆlv¥fg@v‡ê ’~R‘eqïQæt†•3By| †mR§M‚‡ÕhÏþ¾Šç?/§Sk Šuª§ÆUêª9ßYÒ ’å+Š¥ñ”PØf˜7o"7ŒÖ–àA4‰žXvÖÀ ¸¤æÕ´Ú}SRC¾¤HÞpøÿ?üC')Ý&X3Ü¡T¼!‚uµ´QhYÆ#N© žaäI1 EHœPVÞ-e£ÈÉ´wžçÄAÇrìÀ3à0¾[²ƒ¢ìlÍõBD   ŸŠÅ9qïî\qãí¼ùÞÜ}Ë ¼={ ùnw\ÓmCF©ùN»;‡ÖŽjn8|w>õ5Ån7v{&ùî,^ºíln»îbn¯£0'ÇøJ’½ˆËSLS%2½ØEîÌL¶/_ÌÄI³ àŒzpј„‘rs²ðnšÍÑðѬ-eç$r³¼ £z2ï‚™‡EYWÑx5ðÙÒ-/q­I’Þ%ÑS ·;“í+—0qòl‚âÂa·ë”ööŒlÚçfòó§/r@ûÖœyÞå¼öůäºÝ¸¥’¡GìËÁ‡ŸÅ¯swС »2hxŠ2ŸìÜ[…lͪðÍp¬,< "æaJ£¢Ì0ÝÀ 3Ϙ6u"•ô—dQ“8cóÔ‚¤aA f*¸SÆFW³Ò'b­ 7;ËTbÞè”#}Þ¿UŽ·~Á¤BÝd*D²beåñLV€‚×Ü8¤nøÝÉ,K3šKh÷£ó~Ф>7+B»LÄ‚è«Ã•².r2Kiè R0( Uz[Yà3ﵑ^¡S8[4Š¿·§óŸ§tJKŽž˜m^¼Éšp€D@Ùì83\(åpàÎÎ&+; »C¡” W† ›-ÞâÀátápØS+z1 ¥KŠ÷Äþis8Ètg‘ÛíÆé´·L¯ˆ­Çát‘•“MV¶‡]!¦ÜëåèKofî¢E|ðôuD">ªC Ñž‘MNÄËœÍìŠ,>[ðå•ÌY¶œóhKy]]¢f´5 ™PÙƒaüÞ`ìUaÀżÇ=#Ÿ`¿Ö™Ô44Ĉv§wN6Ù99¸3Ý8lv$ÖÇÔár5‡ùn²²³p9íqÖpÓWf&¸2\8Ž?´Q”åæ—çnãÄc.¥šlrÝŽ-:µÄŽ» ç™nv; ‹–‘`·;ú›-µÜ[iÙ/§#úíNW&Y±÷ØPq\™Ydçdãvg$Fw(›“ŒŒL223p¹\QÚˆ.— ›fJátºbÝ"Íž\§ËEVV6î,76›ŠkC&NWY±ó‘éÎÄÞ´Vl¹ÝŒöN<úªT6¹YŽxO«#“¶YN>~b'¸™Míæ› sYýÅ 4z½„ÛBIi cF=Äêé_1èà~|5£Š¶ù aÓB>e”Óf•;fØ/Q£($(§Ê¼À¥i}J™{H„†n2…©!ña:+¯‰˜Dm’õYöu4P ¬ò8ãÕ-„ªÞ»¥LºrXVî¦îO\«$: šé'zÃÄ\Ó·ƒSb^ì÷§ºO(/—ö7“†³úõˆ¥†oý†]²tçÊŠ.¦}>Mû`&¼UdbàÙL6‘Lo€Š…AjØvN’œW#‡VŠß“4äò÷€dŸN+‹×ú¶–›ÝI¦Ã…ÝíÆ ””—±9q;í„E‘ew‘é²áñúš'8]nò3íÔÔ×V`7ËÒ‡â“RRK†….s ÉW°½|'u A2³s),ÊEê¼C"ÊFë‚<D(Ù¶œn:´)$¨§Ò맨{Oz8m×÷Y!ñB*"ÐÊí€Æ2~[^ÃÑ7~Ĺì ÀAûô¦¶ÁK­?Ÿ{i`D”Žù¹„< "Ó ™nN¥ð64ä" ªZÞÆPÌÓí¹ZPX@6°mÇ!ܹùåfR_&ˆÂåÊ€°›Ö9ÐÐPKee€Â¶­ÉÍ Ræ©C dæPäPxêëiô‡pegãÎtà­©Ážá Ðìä€7Ž7õ¾zê “WHvTUâ­o$+;Ÿ6­r£œŠÚq9y…4Ôzá }ëêjiðE°Ù”E>f‰W& ¡uŽ _¨‘582³pÚ*"àpÓ>Û…Ç[CYM=îÂvtÌϤÌã!SŒó È*kë†#däe“iPSÓ6'Ž öHˆ†HT±cÇ‘_‚„#ŠŠŽy4†ì(݉=3›.­ ¨môRÛ›yøÃ~vì,#¶‘WGFV&¡ºìY8p—Ãg¾›úÆzê|B›\7k'Ì÷|JñqW±râ´jý>¼  yí:röÕrÄñƒè½Ç@Îz"ë¶Î¥sV þøñ9õÆ‘ ‹üÊÄs«Vb–¿ª¹P¬åyBű^©1ëÏ+©aLrË“…fðÄ+f™þ#Æca,ß'-è5kú®Á•Š›î“«à x4QDDvV!l3Å#•TŽ8O¾2醠Rôdx‹•J®pš…x1i;h6¦I‰¹ÃÅŒ†­Š¨¢*õˆPœÑ.)Dôû­ËV©žI #ÐLŸŽÓ'D³Ôtõú_ç4ªªK°nÅ”9¶ÊËeõ¯pÊaƒ˜¶p!7œ{Û¶£ÿ¾û2úû5Ômù–CúÆ×ÓvÐ!?‘™Ù9d‡¶sö1‡q×cßÐ*/G‚e§â\ïÊfCÅ,zeSؔ¦lØ”Íܯ±€²³²‰ìÜÈ—žÊní;rà¾}Ø«ckzq ~{>Ùnm ò(Ÿ3‰!õb¯=÷¤{§öœyý³„ÃÙeeâ«÷â*ªkΧR62BɶMü>y>µ€½l%kÖo`ÒŒY,Þ¸¿²c7ªÞ×àYlN:æç2õ·ï8uÀž´/È'¯¸7ó‚Э}kÞ}ø_ìÝ­;ûÔ—ã/Ie$—‚Wóšó²ó©Y³ˆKN;”n»rо}èÑ&î|{~.m²¸ëÄ<øÒgÌ™ù#};²[»6 >å¶x¹Ýdæ’¯¼¼xïµto[À]w£wýrÂ9¬©Q¸ƒ•¬ùc‹« |,Ÿû‹V,dÊì¹lÛQG¦ËIn~!YRÍ=7Ž cqkúõÙ—=ÚÒÿÈ ) BAvÊMžÃǧ#¯¢cq1»wìÄ·¾Ë‘G–ÛaÞŽEr¸i›kçñëNæ’^fƬÉôëÞ‰=ºvãྱp[-mrsiŸíà«Q÷Ñ»c;öÝk/vïØ‡—¿™F›ü|œ6EQa¶ŠuÜyÕYt-*dÏÝ:ÑgŸ¾œsõ3Øòri\3Žc<’1 vÐ&/‡ì¼\6ÏçäÃç‹9›i“—KÛ‚{íÉÛrÕãQî\²33(ÊËcɯŸsÜþ»Óc÷¼o/:æpÿóãh—Ÿ‡¯|K ŸþD|îô’•KüÜsñ¿ÀÕ†±_EÎ’êê}~l6… ¡®ÆCI—v»`ì»÷ÃÎyÜûÊ\® ìÚvcVy²©TËîŠÇCâÔåÒ*í™·l nöo¥S\Ur¡oåÉKÒ‡2i¡#á]«>¢"…RbíÍ2À§iSxMg$Ñ5 åȲpHß‘B×ÎNYÍD5êw«=‘X9"Ì¢]ÊpU*ž-C#W’ç~&«ÀWÖžQe¤Çí“XÓ°©ò×>&ÑS®Š$*æXзYê‰YêH’ÔŽ¸ý7ëmô÷T=èûÉÆM+ÿµð::f‘¬BLÇÈ2m6ê¶­cêÜß8¡_?ÞWɽÜOýö&Ï[C×îýYµl6w<ü,eÃípÒÊegòÇOóý”Ùt8`?²lаHü,·Ø·…EpååÑ./7ú_~íòòh›—Ûü_~†ƒp,ÄixXì. 3<}뙼ûqæ™C`ý&¾û >eÃåˆ m[F6yÎî9wŒýƒÛF½Ç§Ÿ}ÄmW M~l8T#ËçÏæ¥{.æÐƒiuØEŒ8ë(¦þô>§ßð’™I‘3Äõ' âæÇGÑùèá<úô“ìWXÉo¿ ®U;'Œ¦Ï¾ûòò·3AÖqÆ!}8pÿ4àPÞÿf%­³sÈ·û¹ä„cxâåO9å’Ûyÿ³O¸ó–K±×•³³ì™9d»í¼~áÑŒxð-®¹éVΘËÏ_Ãë¿­¥Ðc"¤cƇÝÝî`Û²|÷äÍ6àhÜû ㆠŽ`Û¦Õl© b·Ùÿæ œuícdu1£ßͱ­6sãÐ#ødÑvŠssq7îä´cåé·¾âÈ‹nà©§¡Uér~xó+"6᪠,˜1åÛkqØìdØl4Ö•2uöLV”Ôã°ÙؼxŽÆ¢­mxëýwø×y‡óÖC—rçë(ÌÊ"TºŒ“ÇÒšÖ¼5úCÞýÃ&_±)ÅÆFÅð9ËŸ«)Ìp°~É|µ¹†£/|ŒCò¡ÌS5bˆ!Ú S^r·±3æ…7Øìƒ·Ã8„˜J’¾Ue·X#•LHªÄæçI½_Ê ØÇ`ºŒÒYÕVÂд§¥Ix^,B­ÊÄx¶Š$%m¤ŒÛVY)>úv¥Ã—ÑõFmÁâðd”«m®‹iX_™çîZ fÃ#ÒŒ‡‹ˆ¶-VÜ;,*ü›• “ö&FÅYVSÄ,Û@¬{†&k]h*¯“¸øÒ"”™qgAƒBò4d|EK›b°·»Ê«¬úÚ¦ÖM‡×ÿÍžN‹ð:$oÝ„Œü‚h˜´x‹ÖÍ`<øíá‘T•o'#{8ÙŸûÆ¾ÉØ OqÚî9@=…«û1Ü~öx}u-–w-( ssXõõÇ\ýåXÄfךBDl ¹õQÎÙ¿35õ  Kas¸€ ‹,vãì+/çÐNpÊÑ'°½¦;N^xâVVáàǵ;8¹+pÞY¬™÷½ðk½”În‡±e$@°‘Š:¸â©ÏzÑ<Îr!=N¿WﻪZ/­Úu£Âë%lAë®ì\”o—¸r{±pýJúø÷è«ì¨¬à´ ®çì àsW ç½´Ž0-Sm.'øKY²¨:Êe—gœ|Ò`v€ ¢m—lXVÏåŒæí‡/ zÿB~úø<=ɦoŸå͉szûû|õôE‘µŽñ=GÕÖ2:w!3g âó®æÙŸjxòçtuÖáõ…iÛuo"ÀŒ·ŸàÓI‹~ÿ>y6gžt[·„ñWײ^2yÞf.êßφcø´û)ÌŸ³Nê‘8SZ[ñ34 Š;åœ~ûÃ|ûôTÌÍóïŒÇ•Sá ®¸ú5rû cÝØ78gÈ^ØÝûñökcñÎÕ¼uÿ5ü¾´‚Û?žÃÓ# oíïœÿÂ:‚€ræEß“ÍñŒ6gFôºÜh!Ù“·_ ªˆ¥åKé•#Î:e?+F½òO_{< uÙ ôís<—ž7€¡§'L„µ^ºœt)3=‡Ïᅳg®áý)ŸÓÕÅg»Ý{ÖLù€S† @H´ÝˆK= 64B~§œv(sÞ^Äæ:tuAcÈRP)Qqaצ™Ú‰#F-ø¶YûCº“Ž&!K}« 3Áeöl’+±à‹ú‹ FSòMMŽh´èØBͳ”CÀlÔ¢BA‹Äé«’rÛ¦$ Õõ©˜ì§Q `\¸¦ }Z*"æ^E«4#Ü¥4ˆCëiÔu¶0íhÖrÉL¡N¥—½›ÊÈHäʦ¾ó2Z‡XŸëdú­‘÷½‰ÏˆÁÐ44A²ÞÁj×qkpø·°ÿs:[Ð/ðçrj€`€‡¾~Ÿ=ò ¢¦Œ—gÌA ‹ñ—<ðŒ=“çžü˜Óߺ’å?¼ÃÌp˳‘lo ň뙠ˆ©k Æã~¥þ|‹† hœ‰}ç"Fü™9Ü÷À ,¹ìftVô9ò,®¹úBΠ.œp)›Ex*êkúgUÙh?Wž~€Hć·ÍaL™9‡ž½ ذô¶¶ 8íì󿟯’Pë]À׋½Çñ<8â F\.7>_4W•f53Ö7/¶].Wf”)¸³ âaÝœR î~:á°W(À¬2 XŶìµçÜÁá<òѳ(5Š ®¿•ažÇ{â"‚´ß“~»+–w+Ž:\ƒOø£…d{tËŠÀœK3ozäv¦³³¾'íg2+½#¾Ò½Éû"ÍÕ©’Da1®fJ‚Y¿Beî ’70“ž…úYõjœq1•šS‚¾ÐIi&¶¶•³ò%›]m–!1ÚUÉ”p‹çëµ6Ñ(ÎV½m­ÆÞ¦êe7£#¥\t= ke=•wë÷TßGÔ0´kžÜŒ³T qšÏƒX{ËÅl&Q²Q¬V6—QCw•ÄÛhf»i{}¦fü۵SªÞTƒsõ7m›ôm¯ÛÅT¼u F…a·6EøpÓgÀAˆ©nСßœ¾!ß¼÷¾·®ä­gwon:ç|!_¼Â©{Ÿ×¤ý3yùðc±;2tŸ!´‘Ÿ—C­ÏgL•"@ˆÊú~s8×_y•7_ÿœk§|É‹¯ŸÁ˜±Ÿ³¾¢±ª ÷`@¿>ø<õ4"\wÿáäw¡µ«o£·Û©›DÑ"èl ¼€­¬‚0àkˆæ~–ÕxbÅR¶a£ìääf£Âêê|ˆRØ€ú² ºµ7NÀdkönª¦ÖqÑ„%$ì§¢^1↧éÓïxÞxåEFÍì/G3ê´óùrÌtÊ̤Î{ìNPT;¡æõ±Ù@Yé&h2ÅÎèäf8¨ªŠzZ]-ßQÛàOf6à©‚ÝEžËI](8±+‰µ¯Âá0@úØ€  3‹|Àgf™xC‚¾ ´ïBP× »¸»ôÀ”TWÐß#8°Ï”UVawíÅÀcN¥Çá'C¨ŠªÐcÐþdAhí‚­•¥€hâ…±ôŠ ÀŽ7Êv¸µ^ÈÝ£?ý÷ë‡Ç[M0bç¶Ã uû½pyýxò ùáTú>¯=ÿ2½:’^É9÷Ä›«±ÈÂÓà5^(t×㩚99àˆéáÛÊ‚°g|Š™ÞˆiRK‚€‹|W>a":…ÈDé=wf¼A™F½ñçq1ðX')‰qSrK¯‰b<áËh°…µŠS!§ÐŒ¸4Ë'Ó±Ô*ðàRk£ˆ/ZIÀ‘îAq×ê5e<68©B":"æ×Z\‘D91Mï’$žÚ$^GÓžŽÊÚ3n”Ú , "‹â1émmª¤©$=kSéejW˜OÄ‹·`›­[y` ÇJkz…jxH”©É”DÃéV)è-f†ë®HI+5¼. á—¦^mb”+¥#0óÀyëêV؉P]ãiÖkÃY.n»ïN¾vƒF\ÈÜ©å ù:°³Ú6³œ Eqv¿}4’¯y esèä ŽØyäçùÜ{bJk<Á'qÉÉáÆzêr²èÒs O¾6'_ý˜1/Þʹ·¼Äȧ~á›'†Ðq·N°%ƒï~"ªÊª«ð6„q»³›xn›bò󲩩ö‚ÝfœÊ–àq±áÎtSµ½ÉÍ'ÏíÀk¿$®èÅåTÁa])^¼ùr6‡áè6yñç;2(Î7YÔÙ<”pƒÆl7û<Ž×Ç+ÕóÒÝgqësóÄ÷ðÚÙ»D;( 8Ë…gÃ$¾ým+݆^L¦ qAÕ&@q†_É<zîcp9ȲµA£?RŠË Yö,2ò[Ò.NìÛ7¾_Ì+Mâk¦¨ 6ì@ˆh¾.€×SÛŒ¦p$lœÂ`Òì@ÙmàoÄ8mа¿ž²dççÓ©{O\@Fç¹ïþëÏ€w= (Û° Zg»ÙøËÇ<ùÅèt:N@"¾X(½(Æü¥†Þe! uŠPJ¡\ímMÑÈ¥Þî€IDAT1Ó‰‰GÌ4œDàëIPiÂáIY«já©(Ê"kÕ€\/5ãò1M¦æˆEüS0é“hÑÝDY+ *æe!ùxÄTöC,Öd4a‰T³bz6SL,¿Å"”ŸŠSǬv*Uò â•¼Ô Œ<ÕºÁh&'ñíg›‚ú^VÓôyÖV©FÓË, ¿ü ‰t»Ç÷t ÙMR‘`Taj.æÑ>ÒU¡‡žv='w.`ú§ÈîÌ=7žŽDB„õ‰ö:æ\éõÒãÄK™8uã'Oæ—¦ÿ~ŸÌ/“§ðëÔIœ¶_{ʽu$T=ƈ)³ µc9çîߟ;ßøˆÅ›J +;YÑ|¼œ¼¨=që=‚oûõÊ¢Õ›ÙYµ“Ï^ÉÉ'_OI›Â¼˜§5¦tÍÿñgfϚɂÅëq¸ÝѶO@¡ðÅ<Àú“W”—ˆ©ïÒ·wo8t8¥þ s2 ÷è xóÞ+ûë$½h7úøCþøGÙÀF~0‹É³gS„Öyy”¯œÂ¡žÄK~Çªí•Ø. ²¢^ÅÖù™@ˆ¬l¨X¿¥73ý‡8¸ï1TPÌËÜÀû ‹¸òáwùeìXzíyËk k«‰={u¶óàS£©n ñã§oðÎ'ß޽ávºÛx÷ºcxxÔ7lß^ÁÄï?àÎ;asäfE+îƒaѰ(îüþ(îŠòó)ÊϧU~>Eù´*ˆþ»uA>™N;!èo€ˆŽLëë|´Ùí ¿ðp¦½u=#ž}­õ¬Yµ€®¸ˆ[ý ²»²÷©šù ¾ÿ5cǼJŸ“.Àow‘“i'äu‹zb?{þu¦N›Ê°~=yæJ@6x;w>üÁÕ?ÓçÄ;Y¹¥Šíe›yå‘8éÊ»ÈÈ/`õ˜G8êôá|:q[<>2 ÛÊr6 œNÂB€½šðùôhªbø|÷“o‰û2ŒCr_}p?›€vù9D"‘„3‰Øh—igóä÷ùe¹—Ã.»„¹P_Hô¬Ýh!FaB«9Ú†úF£i­z5ê…–FÈHsÔB™èAºêÝøO]WÿͦBKãQ)xQ1ÈK)”ª¬Ã‰ üQäbÆ3Ò„¾‹fëSf‘0‰²®Ø7\§²úfÍßõsÍ“†‡ÍŒeŒ"IuŠI\_[«.Ú®JƒhÁ¤È.É x«â<«UmqŽ¡·O™GÄD‰­»ŠGSê¢+ žbÁp̪ÞIfZ(dÒï×ʘIÄOÚÓù_Õ>­-£±^€-Ö­ÇeË4 ½õP˜Ëõw_ÉO×>ÍqWßËÁyÊkk♢à „Âd·ß#ºîiúõuõuÔBÑœG£ê¡0…{àð—ñô5òbN!yŽ5^=ùrî¿æxêö:êrƼZÊ¥×?À½¾£¸(ŸòÊjzz#í ÝD¤>ÚX=»ðÕ“WðÕ“pôÅ·óãè§i ù£ø2—#+ñI´ÏfݶuÔÔz võõ6œùPS×@û½eô£7pá}/sÚ c€Þûô>ºë v–úã%ùÅ \tÂÈèÃô5sé^¤èÜ£;Á³¹é¢_È.(®§Æëãø‹æ_'t†Ð6ÚîæfʼïèÛçW"õu8;ïÇë_}Êà^yÔB\{ïóLœ²œŸºœŸ€ý‡Üθs»2xøuÌÛ²“súv¢*ä¤ïçÒ)‹yïÞK÷ü=ÔVîà‚;FqÕpîÅ”ßÇ3bø%ä»°;2±Ùœ8i)¨B@‚T†\Üúæl­<‹—n¿Œ/½Ìº*¼á î{s8!›ßüŒ9ƒNæ‘K†pÆ£orpé¯<øÆzª¡÷îÇr×%‡ñäèÇ9òÇÇÉh3€×?¹ƒ.½‰ZÔ zÌE÷ðòÖmÜpÿÓôîú:EBeu=\ó4ùJÑ}î¬üñiF|ÿ­Š‹i¬(§Qò¹õµoè×%›j¯2²8éÆû¹lÊbÞ½÷R~xþn¼•;¹ðŽQ\9p·â‰—îçèËFrÉͯ1é…ëèP˜O™×K8U¹íNí³Ý.çœ ¯UÌ“w]…„Cµ…G !*1ñé¾XxX’ôúµêci3þºt1›(d 5“ì"¦žª¸F <5Ò$ŠUâ¸L«â'I2>Ðp]bìýSbèd6,I–çùV$Œ1›»­Z”S±¤ŒûÂJ’ö@¼7ù4Ä•2Ÿ§š¼Ï`nBA]*y’É&#)«Ì;/$1/Öy–º0wJ©q!v±þÁzÐD²”#üyõ)C©xÒÖcgÿÁèÚÚZÉÍÍý¿( ±mÛ6:vìˆÃñŸÓuC"¬ª¨¤ÀaÇe³± -˜›Í†¯®œ•õ´íзK‡EW°AÙìdØC¬ÝPB›ŽÝÉs†i GbE0&Íl·I›é²ˆàÌÌ"ßmgùü¬ß´•Z_€]vçð#ÆôQVçÃæÌ¤CN&›7­bîüeÔtí݇Cöë¯ÁK½?‚Ãa#äk tG`#>˜¼¢bvk×–p$6*ä§´¤g^Š‹²b šyêNìÁz¦ü6 U¼'G¼7a#á° \´Ïv³hÎ 6WÔÒý€ôiŸÇŠK e´¦}›\Â"Ø•ÂWSÅO-õuÙtßsw\ŽÎŒÜv? æ/dóÖ-4ìtÙ}_Ž9´g´Û@Ý&ŽìÜ }Ïà³çî!Ô¤ë~Ó5vÔT…ù¨ªÌœ»fGu8Ù˜·ôœÅt(È& ‘WHŽ#Àô)ÓÙ´­Œ¶»õ¤ßþû@¸‘@$B»‚|$XÇ”™sÙVº“Vm:³û^½h[A ±ž’íe´jÛž}˜£/} ±Ût!"ùi÷Ø›¬ÂosÑ©m"DÐ:Ü#‘Ù…Ø`æ´É¬Û°‰ÌÖÙÿ€CèÞ>—²Z/­ór)Û¾…‹þ 3¿ ƒëÍG7æÂ—7³°j½ T€Y“gPY ÿá'Ò%æ.YJn»¹3gmÝ.Ö®šÇüÅëð9sè¹ï¼gG*k½¸òr‰TmgñâålÞ±»+—^ûLÿ=ÚSáõà±Ä'ŽLÚd9¹ëœýxê‹¥ ¼üa>|ôvvoëf‡ÇƒÍ™E›,'ëM`øˆÌ[UÁCïÍçÁKú±ÃS •ÓT™n²~€Êâœ&Ë×Kö7]fsƒs3…†”Y±Ž"Gv”á;›ÃÿfйV3kˆ­WlÅÚójÉ›“5©O¡3‰>m¡ÉÃ'z¥SÂ6 k¦úmff©Ò$ÌY†Uõï³Ðˆ-$-¼U)(º–÷ Q$çŽdžUg ³oS‰ g*¾,+YnÖxß„NŒ?IáœÄUA %BM(L¯¢V8l¶¿…^Ö^¯÷Ÿ­t:m¶–è•ò”€ˆ.w>…Šš:þ `3pqG"‚##‹¢,jëñÓf’1(eA”FžÁEDpefQàva×<ÖÓPOƒ?q(6 òpÇ® ž:/¾`8:Ö0"Ø]™´ÊÎl~…?¦¦Ö‹²E'àˆÝIQ^6¡°ŸšÚÆx|ÄpaÏpÓ.+ÚvgGM5alØTTAVN­r²p> SU㥰°"ª= Ø”"‚™G¾ÓÖ¼ÌêÚšhè[A¦;‡‚ 6Í:ªëk »òhØÈþ9»S5ä*¶|m#TðSÓÐô­B$"¸ó (ˆå Ôxkñá MnõÔú¢8‹ˆáΦ03ZEJ=¾Øo6l±C¥l BAª<ž–wÛžŠ–çÙPê(ŸEM7Uh¬§¬Qzp8# œdµÉÅï( x½‘¦=QÍl]-;ãÆG)÷xâÖkCô5Pæ‹§¦ýµÙA_]ôw}âp€rO ž$D¥ÇC$"äwìÁ1Ý{™’eSZEm¬h ›Òµ«Œ2?›Mú)óø<9*65¥Îã¡.ª9Ò>?‡lwyˆ=Š×¿!ÚãTó-å±÷6ï}$DM ?Ú°cÓž5x=4x“lO‘M)BZ|ÒQ@„zo5áüBî~ñ+κbï¾ü4+ò]4ƒëiÕk ÷Ýt.ž{*=вc^TiùF³–"JRSÀRQœÌ”0ÿ+φ"yh_?c[’T†þM”0#ê³˘’çX縙U?§2ÝÉ*<·Ÿf…V-™tT|zƒeÈ• qRTÊ“å8ZyRcûÛÜ=%Ù”*³ v«=M™é&òCžH2¯›Fž¥/`iHÉ SÆy½˜àÂ4r`.7àe†©%*É·'‹¬æP+’¦âÁ6;#Òì£ ‰…×[áPiOç×ÃÙLpVy•²ëÏV$¯ÜTFKIÎÖ‡N™„(R MY¼÷ùã‚etØLBjf“P¾&cóó"ÅF÷ÝwGETÕ6$¦Ch¬,˜{ Éà†ZRÉ¡SI’å?¢3’ªÐ•¢$ñ×`â13MG0Y?*µÜåf>§Ý#Œ÷·yMŒbe1µ(•s :#M™lH ~ÓIAFˆ0K1Ä™TìñNIyH¹ Œyj­üýà¨t¶´ÂPfŠ•‘u`£å`ÅY…bb¹™yoôþûÄßõ-£´Pºô­L ÞŠL%1ZþЕD(êï1úêO… £ï4!hæy±Ðêõ¡meÄ”„æ* hBï½RV Ž‘1¢Œ…‚•—WY¥wšàÕ¨!r²êJ-“ÓsQ+œ(¬Þ®ôר–nGú1¡¨Œú«ø£ªÁMËüuƒIôÑ \Í7ó8Åçl: *1ÿL¥Û“Ê‹?ƒ*¡¤%¿;î,šNº1ðR¡ûV]µw žÅœ&E ç©Ç¥-z‡ ,e&ý•ñzâæo+ƒM§+,ö^¿WºsgÔ¼[’6m‡£tƪq)‹sh8h€øo5TT•AÐ`DjÓôU|Ê ÷ î% ƒÔ!¥Ì ‚æsŸ¤?—2oz£ÒЪžÏ'Vw?ÙJyB4í¶Lp˜(2ZïÝ×??ÎÓržã'˜þû%ñH$"Ñ<¢ˆh{Í$Œ“xA§(¤9œå½M;.ñg¬éñ‘ÚZK‹; £‰“{Þ¡U|µÞ•’Nði'ÅI˼é¸|ýá@ZÍyG-ß•0½E)hnWÞZD¤åÓT´TóÔÏ÷Õ tÞ^#ﳑÇÁ1“ûV—ï•àÝÒöņiþ? @5ÏL×å‰Á‘¸|Iûqyi†)V©b¨PÆ84cM4¦9+ í{ô…’(•Öó§³câ½s¦Ñy~Š$btk^OÅóŒèR$Lpi:ô«UèÄ"qQ´gI]&ú¾‚ û®ù°ýûôŠ|<)iz³èôHÍ{"˜4$ãÞ›JŒ=Ë yƒÚtƒˆ©ç7!·4Nèk­DZÞ*JOƒˆŠÄxÌGêgÑk½{âð&üj£K½Š.ú@¼'PŠÄ÷—oN²ÖgqÑßtÜÀ+«b‚§i›åY”I06Ê#bèS±Ž$Fé`-b*ÒrqDšÕ"1ŠxEÄ8Ç2bàEÆŽuýHª×%É£®–d›Ú½ÉîÛ•¿K ëÛUØ•^¾æÙðï/äß•æ’"Nÿ Î’µULu챤À6SÅQª{ÊØæd÷§‚ÿ]Ù õVÿzOfnýØ•Qß©à4¥‰ÿ†}HöMü zO¦Þ² ëNõy»â¶°ZË®¬Iíâ^%ã‘ò'i2U,Y´+÷¦JÏÉd´Úų³+g—Û󒼯W¥ø^£÷ØI+ÿUpÚí8ICÒ†4¤! iHÃßli¤! iHCÒ†4¤!­t¦! iHCÒ†4¤!­t¦! iHCÒ†4¤! i¥3 iHCÒ†4¤! i¥3 iHCÒ†4¤! i¥3 iHCÒ†4¤! iH+iHCÒ†4¤! iH+iHCÒ†4¤! iHCZéLCÒ†4¤! iHCZéLCÒ†4¤áJÖ,eá’õqc+7,ãÙ§ŸeÕVÏÿæ¢Â>Ì™KI‰'½ÁiH+» ‘@-‹ç/`Çï.ß[Sº™õ«6ÑPUÁŠ%Ëðú#iŠú7CÈ[Áòù ©öøSº¾ºt ˯ ðÚŠˆß˪‹(«ðþßÙ÷òM¼öÅ—<óíLš=—õ«ÖQç“¿ðDaÓ²¥¬Û¼ãO?aÅÜé<üÙ¼ù˯,X°ˆ­›Kù+_ô–³|þ"ª=ÿ4±nérªË«Ù±~k7íLé¶úŠRæÍ[ŒÇ—æk½q(ýN¿1ŽNæ¿ü0·ßy;O}·üÿÛw5VncòoS(¯ íú͵ë8üƒyàÍyé nÆgIŸõ¡42þO*¡F¾}þn»öNr:gœ~&§Ÿz:ï|=$O_t!·½þ1í³é{`{wþ.¿æÛŸ>æÔg¿`Íü d$³·5ê䜗W?ú˜ «*X²`½õ!•þ°Æjôóù»ïñͼeÿßÐÜö/½û.% îÀü4áS®}å ‚ÿþo«\>•#o¹“¯—•™ð¿y\ûÄ3Œ_UÈÆOü‚£îzœ-ÿ†Ã?mÊwŒúajŒ¦jøâë1ÜýÚ(nxæyþõÂKÜ>ú¾ž¾ð?¾?¾ËpÛ¼1yõ?ò¸NŸòoŒÚüïmK§qÀU7óìØß˜÷Û®}îiù ËvÔÿ"¯ç¾‡ïáº/&¦ Ÿ6Ûò¦Cʘ÷_dàO1iÚt¾ýf 'Ü—½ûã_R:+—Oãˆ[îà«e;ÿ³®ÛÌ©÷=Ê7S—2æÓ¹æ³ )ݶôÓG9è ¾LÞêàãQ÷qÁEþGιf~÷ ×\t>gžq&ç ư¡ç2nMeloü¼?ò2=ä öÙgoö:è=­Ú.5«xèÎ[8gè™ :Œ³ÏÊÕϾO2Õþ?±>Q "~”æoGß4’7^{‹[‡î÷ÿíÌmýGwS6Öü‰EEP@½;­@µàóË(>7T§‘ñRé´5ðÔ­ðܨw EB46x©óÕŒD=KçÌdñê­¨˜ßàÚuT´såv„ñK23(ÊwÆýîY1‹[?øŒ:W Mæ¾¾¦\ë ùõÉGŒ^°âÿº~ýý[ü~NÇŸ#‡m[Ö3kÕZšÔ¼Ù3&2vñ†Ë·)åÀïÎ hS†¿j«™³t9Už¨ÂowØ º\Iˆ©z<öÆhÆl¬ŠþÓ_Áã½ÏÛKþ ”›C(dæäñ »íF†>õ ÿIg•RÔgf²«ÿÉã¸xÅ >˜²ÌÏïóù¦Êæ¿|6é;V¸z³ñã7øâÕWøì²Ó ªiˆü›Dg¸ˆ8íI/õ,ŸÉ­ïJ}ìÜB}ù C.¸É¯¿Ì/¯½Ây»ó‡·õi;•IØþÞ€ìr¼¶!Y©i ‰'ê’?–0körBÿI©©âšónà¯&S_[ËÆ­Ûز½WŒ7Eê½,ž³†nÈ­wÜF«Ò¹“ׯ¾’Q·ÞÆŒ÷Þáóë†òÍgopÏW“ãp»rþ,ÆŸJU} Q UlÙ¼ Ñ9`k·oggY•æ¾™ñ÷Åè9Çé$CÅÓèÖ•‹ùqâ¯,X±1^àyk©*>³tõR~û}*è ûØY 3‡*·òëÄß()õ4ãæ·ß'²$f”iÁ»u?ý<žek¶ÇÑOmÙ6‚±s²`ÖTfjhÓï­âåw_áâ·>å [ðÔÄ3õÀ¦%lŠäqûã£*hÙVÖmª¤óíÙ¾£Œ°g§´rØŠW±e=Ó§MeÖƒ¥gS&MaÚ¬TTÇhEÙÈp:ɰ·à0Tµ?Ö&®sìì¹ìÞópwσHˆõógSêÌ¥›CáõxÁ™I÷âBâLÌPk—.â÷ISX½!ÑsY²z'MbÑòu4¢”¢ì6²]."šëþ`ËNcE¢¾¶šÚØ_¿t>¿M™MmLF«ÊÙºya9W••S·ÝÇ>:Ѫ}ròs(r9Rb£vgfôcæämO}Æüùgî{Ùæí4³)~f¦†O4ë,;72öóÏ™¶(5/}íò_Yêƒ/V–0þ·_™;gsgMáøÝ £8Ë*âŧñéË#¹äÂKy÷ŽaQ%Èáj~ƇcÆÑzÀE,ù;SgNcÁ¼¹|pïuI•ÎÛžúœù > ·i}[¶Ót §š¬/ጬþKNHfVm‹ xsÜ º÷Úlýî%º´- ÓÝ9øàãY°µ!Ñ+[ÏuçKna1½ûìCaa…}/¡!†è{.;Šo‘)SgÏ6Ù´*¤çDz`gü¹úüá[håpRTÔ†Vô?rãþ¨B,]ºŒßfÌ`ÎÔ ,]¼€éS§³zc|êÉØ×o!Ï•M—={²[q®¶{2®Ä…ØÈ¡U~#“Þ½•Œ¼|ŠŠò9æ‚{¨Þeï•Û.ã3îdþ¤ß8¤S1EÅ­é²[W>[ÐÂ_¦Oø’ÞÝìÖ½­ r¸ìž·ÇSïÝ{9N'­Û´#/¿#;•u @íjúï} Ï}±XÃÖsôÁñð'-‘ÍS'pt¯´ïÚ¢¢½nÁxNîÓÖí:±ÏÞ=ÉÍÊà¬Ûžiòz°déR~›> €¹S'šâ3 M‡¼¶Vþ eÓ¦M ÿ o«’C²ï%|HµœÚ¾µ|ÙÝ""âßô›rÝ3S¢¿û¼rÅ þBv+É+ÌŠ÷•ßÖW¾ÅWS-¥å IHv”í_0¬ùÕ/g_q\úáïÍùú³—¥ý™WȺZ 9oø¹rùG?D¿|õ\9í²‹¥Ï…—ȾÃÎCG~ M«Z3Wqžì{ÞùÒý¤!2ô¹ÑÒ{å÷_½-û_x• ½çÉp´<ðÍdñÊuß.ϹPú3\¸êÙXë[C¤r¹pÎpùxIYów?5òFé{ëC2ôþ»¥Û°a’?ø¹oÌXyùµçd÷Ó†JöI§Êñ½.Õ±G½òÎÃÒýÚç¢küêEÉ>s¸ì}õ5Òù¬ äÅ_f‹HHy~¤t:k„|ÞpéqÑu2s³WDD|ý‰ì?ìbYÝ„“ºõrèY#ä¡o抈HÙ¼Ÿdß .’sŸz^ޏúriÊ`)8ïZùmÅv)Ÿÿ³ô8û|yg^Y3ŽÛž~™ì‘výpéyùýRRé‘u‹~—ýÎ9Kö=ÿb9ä‚Òêœ+ä»E[öv☗¤Ï¥wK3¶ê7Ê¡ÃN—}ú³îʰ\yÃ’yá]â 7Ƚß)Ï8Oö9ïli5üfYQÞ R½Vö:ñDyàû¹-·n_&½Ÿ*/M^'"~¹ïñ;¤óçIŸsÏ–¢7˼­UÑWo%­N=Cùv~l‹jeäS÷IëSΔÃ/¾DÚ>MNò}©D^6ök9äü‹åìûï‘þ#Ηö§œ"Eç\+¿­Žb¥±dŽxée2ô'ä¤ë®–¶ƒO•®Wß)Ÿ~÷½\tËURtÚé’}ò0yò§9ÍŸ:uügÒiè0éwîùR8d˜Ü÷Í̦%O޼Q¸ý9çÁû¥Û°³$ã¤Sdø«_‹ˆÈäñKÇSÏ–Ý®¹V:œsžÜõá8C]N~äu©‘Ïšù¿Éœ/ý.¼Dzœ6D®|ûÛØ6y墋Β“^#""µ«§HëcŽ“+Þ¯ÛÏøsÛ¸c‰yåépÁeÒëÒËäÀ›”j¯_Þý€ô¸ååØ-›eÄÕK· /“C.<_rN:Kžýy^ì€äãQÏJ·³‡Ë€ ‡KáÐËåÍ9ÛDDdçÜqR|Êéòî¢ùè¥ûÄqìpù)FÛzûõÛ²ïWʹwÝ!ûw¾´;áDÉ¿â.Y[ߦْä±òÄø%Í×{×Í”ÝN>]ÞÿmÔÔÖI]("uÞ*ÙYåI‰‹Îzízä‡uQtÏ•GIëv'H}ô ÊYÝ ¥ÿqƒäÐ÷–˜oIN½ãciânó¿~[Úƒ´jÛA9冗“¾ó›NÈ‘U‘¤×VlY)»¹ý‡<¨á§[¤#È W¾»ËR#~}õrV÷BéwÜ  Yßà;>–°Éý;—È>.\rßc¯Ê7ŸŒ–}:JÁGù÷²©òÐÈ{åÊ“@¾[(Wî;·§€’—ß++W-“÷F=*_ú¤xbŒèŠmš¿åèÓþ%wÞy£’×ûBñÇ>ì© @<éJù䓯åÁs@^Zà ®‘Ö9nQ±g`³‰;ÓÝ»ë[äåG·ž.€ ö/ùqÊÿÉ2ä„Aòæò:‘P™ hßJÚvÍ@nù¨Œ¼y˜rëksRÆ÷-—Þ¼–}O¹J^{æäÁïW‰ˆHÉ¢ïÂ.ÇÊoS——ï•Õ£~i~Æ'ýγ¯•ϾúF®:P™V%"žÙȈÕÍ"ä´û¢Ïlš/»ƒÐª—|ÿÛTyïù;¡7Æè'X!ýsÚö”ïÆO‘‹çÉ3·_!½òVtÏ7O•¢ìäøü»ÂW/©­­•¬ÒyF× )èÖ[®¼âr¹úªkäÆ»Þ‘¨ÈòH_'\S:ò³3DDä­+OÈ“5±' îb÷¾Ãb÷¦¡’y²ßY#dìêšx¥s¨‰ÒùqT™ùà‘‘É6T ¯Û^.!‘`¹œrîirà#£c Ë\É>b<9aµˆˆüüõB¿Ãd¿ÛŸÙ«7‹7’ÒY_ ‡!Ÿ/©‘-Û¶IC ÷ó-FÜ ;š9i@ä&áðArÆ‹ŸÊ¶Ê*yáÍG…ƒŽ”þ7?*+¶—Éo_¿%®ã†Èo[¢ªÂC/Þ"œ}·Ô‰H·Z.¾áB9öÉwäí¥RÝàïæéÂGÉ?¯Œ2“Û¥¦!zïÜ/ß“ŽƒÏ‘žN¼k¤çà3äæ/f‰ˆHåÂ_¥ÏY' g\(ïL[!¥¥k¥ûIÇþ?öÎ:<ª«ÛÿŸ3n™8à.¥J ¥-…ÒB…ê-uww£N jÔº»Q£¥PÜÝ]B"ãzÖï3IFÎLò¾÷¾÷×Û›ý<%˶UÊúÅJ—a‡ÉA?‘3.=Yº^?¡ñþ7ž¾],§\#»"ÿžõÂÕ Ð©õ÷²Q=úkÆ•xå´îš¢=äÜ;eã¦ÍrõQ]J5ƒ/°^ò@†Üô²ˆˆìøüIä¹™›s~óö³‡‰É`@öq¦ü¹¹*ÕЯß.×_|žºÿ~ȱw¿š*ùW~/6Lb6Ù¥XnzæS ¶p¼Í¯³@©lôé?Û)}œòÊ&PçY„îÇJ2™ÅV~!€|±´2ã§î…@{YR¡¯'o>e€¤hðwŸz€Q¶©"²æ+ ^÷t“caÞžøe³ˆ– ëÖÉoo< €¼ôÝlY·n½¬Y³VvVj 8´}¶( }FÜ¡?Ðú2Ä€"¯LoXÏjÉ9ö‚I-¦¯û¯#€ »æÙ„a¸RÅ ã¦l‘«†— ƶ’ŒPNh‹Ð{”`þxK9æŽç›ÔéÊ/ßv‰ˆgAB·ÿ™D ˤäü'fˆˆÈÄ˰ɤ\~d©@?MçG·Š¤pÀqΟy Ȇuëä×óÙ :SAç?6{ÝjWðTV°pÙjþúëwfÏ[ÝQg‡<—#u+É®mo;u uà‹&rÛ}O³G1\:Ÿº1œlúܹ„í9¢w~ÓF€¨ˆbÀ”Ÿ¨$6 µtïØB[¸ê•÷X¹ÍG÷²Œ@Íš…ÌñEèX ðÄwSxyávlJ„´íÅX…‰ßÿŒ¯ÀÍ–ÅK¸íÔ©^õ¿m‹Â3?Ïâ„Ï¢Pï¦/¤]ÏNüšôÜÖUëQƒÕ 1†ŸýñýËõ‡öÅf·sÀ¡'sßñ{óÆ´™˜B^^;Þ¾}‡õlO‡.ý¹÷”ãØ¹a%»UP,föxᎠ®åÒCûÓ¶´+ý q8GÎ8в¢"ú÷î‚I´5X¼p1C!âÛʳßMaQuBõL]«má‡B>JöÁ÷_A§¶ÅÓ¡ŒQ*|Qìö<Ê‹mˆâ OYmÜΤy^üANÜ4ÏÅet²‰›œô)oG‘ÝB8M„h{ɇr$Ãú–0oÎ_ÌÚRE~a15uÚ¶ó>ºÂŽ¥\ûöçìð˜éXšˆ±RU ó‹YµòON¼ç>ìƒÏdÕ#—S”9cîÂÎ Oð­Ád£oÇ2â1•²¢rº·)Á`Pˆ&h_í8þèáWmaê/¿ã1¸p „ŒùÜ¥O~ùŠ·þ\DÛÒ¶´-°%HÝD;§…×ÞyŠÓÞø§žx›Ø;{œ`4JqÛ.|qßôïPJ÷}‡pÓ˜ÁÌ›;8ã6-œÎŒ*€:žúmgž|*íÌÿUiªí™ÚóòW"&K¡f7”Ë_ï™VØêLyí±ë©]ðEáˆÏçÃ/g&… ØéÖ£ûõï–˜ÏóÙÚþ/Ät[×…è;æ2æÍšÎÒe«˜ýë¬‰Ýø%“&™ ~0Yü|óËoüöíçX{äâ+nÄý/&C1s1ö?˜äPq›bAER³#Z¬^LÕ"U=æ ¾¹ñLæüü>{W¿7E»Í!Ïfg÷–M,™ý¿L›Ï‘‡ å´„ðˆÆU\.N[Ó«‹û峇î"´c1Ã.½”1ã_fwRláÌYWÃ胦FÉÄãä» š„C H¾;Ÿü„<© ‡ƒ1U–*J#ã jÓw¬mûóŽ÷Ó7²ÑW^Á€ÛgãnO ìn$D§ ‹É€šˆs5†Éâ¢WqiC(þ¸`3™å‰ rÅf±á©ÛɤŸÿ¤°s/FvjR{jÃttÓ¥M^ãoæ’|òÝ6ªj›YVÍ™C¥±ãö-׸™AKµ;+0Z @LHÔJDñòË´YÌš6”¶Œ;þ0â@é€Ã8°(Ê‹–ݲˆ*7?¢![Æs÷Ž‚ˆÇTÁ˜PRÞú(º”¥ô¡¼s{¼uõ ¦6`³YQ“òu‹œ´1¨ãš3X)¶;çÔ‹S^Ð4W¨ŠÙ¨Íq…Çâp°nÕ<æÎ˜ÎôµUŒ=òX–k2ЮM Qu{BA0Y°š´þ#q%Ój[5gU¦ôyŽT$Ö¸¦JbÜ 1†¿~=™}ƞτïþdñö*‹—A{ÿ¹§_Ágå÷^¤ÃÙgóøw³´—˜Œ:ó˜¹p6_/ZÍðÁ‡ë ¿Ïg-ÉàÛ@0ˆ¢(„ÃÁ¤0[…F½æÛÍÝNÏÇ3eÞvÖ†(rÙ‰D¥ˆWヌÓJÃ\tǵ”]zs¶V'•M¼ý×o¬ð0â þÍ$öv«Iba¥ØY"øÞ‡I‡/ç/cÛ¢Y¬Œ”pÃQ‡üWRô°g“q†ö]:7É ¯paêvjÕ&6¬þ‹Ù¿}Ç7?-çäSNç¢ÃºäübIÛ¶”¶ïÊw>ŽcûÂòÙø’“pòÛóÀsÏñÁW¿²â»˜ùñ¼üG“Áhqж¼”GŒáǹß°ð¯/©1‚ö]º¤Ï‰Yp¢5x€C÷”š³i;Xœ-þfŸ£Ïeõš5L¸ûZ ë~áÆKÇÒï ‘¬®ÕPÔn©s¸bÛ2 ˆNÀÜÚ@Oz%éð-¾ßähŠ{­IÄVG™µ6U¿ê”lùfñ Ç¾5ýf¶Ql„X´å©uñX¤¼‘׌6;·§Ð¬€©ñCÁ^GpÂÈ8rø0Ž8öX&M~›oß{TŒ{ªÁr=“Öój½6V+4öMI1̱>À–pjjê¡x/Æ;‚á#†søQÇñøK“ùì›7)Mk§ßþ üÉçžÄô¯ßᬓåèï%\ZSçKÌg]+ªl¦™þ©3ArØí©4ÕtLÅqÍøôß÷ x7ðç?2 å÷®f'uÕì¨Ð·!ƒÔàg™7ÄAÆÈ389áŒËÙ5æ &¿ñ<¾ü'Έ^ØRYÃùÆ}Çî¥3&5#йiY‡<šUïÍ/¿ÊÈ»&Ñ­kOž>ëHv.™Ç6±sâ½Òò° Ä"a¤Aí( ª#ŽÅ„4¯,ò%‹b±¦z’»î7ˆŸ^IJ¹S9ê–{¹åÓ®|qÍéD%® ®<­Ï5«X]íá³¹É.’8¾`(jZ¹F+6µþ€¢’nL¾îB®yêi½×Á_^@»¶6¶xªX±£–½ 5ï§w{%uu~:¶)m³б÷”™Ò…¤ŠÝ™ª@ü«§ñÐ×s8颇1å;©‹UcTæ“G®ÓS©\ôaÜùÕ{lø=Ì>gh;3'jö`4ô×}.ˆ (ÄUmÞóK,ü¾zkÊ=ëVn¤m›~Úü«q¢q•|§µ)X¾z{ .JÌp6 „"‘FpaP „£é.Mhw1™¨ÙSËWÜÇm3´-ƒ‘h8ÔD/ià<¢­™oö|:ô@™‘œà¦Áœq9mX˨Þæ‚sîã•ó†ÁÎ9|rñxHR\tå-\tÆ9Ü÷ÜSÜ1éiŽ2˜}óíìܳ›³ŽË(K=§?r3Ïrõ°~)|;m«Ÿ‹Óø6»÷OÓvS§|Äã¿ndƧïsh ½ÿ N ’pæSÔc?Þ{úY5o»‹_ù‚åã/Ãjˆ²3æäõ+¯dúoo°÷å7²þå‰tÏ7fù¢W¡(i*ë·íÀèl“°î:rýQyà“טjò0rØqì߯üŸSFür5 ”ôèÀÕÿÄuClÿÖû™¾C§ãÉÏr}Ý ØÔµ­îõ•}ÀÁCºÿÛã ÙÈ2­™1KÖ¬oÚíšü$ïÏ­ ý˜v©ë˜àãÒ²ÔLçx8„ÑjÃPÔ‹[žÄ-Obî‡1è¬{yàÙé|xÿHö†6óø[Óp ¼0zØÆŽ(t5‘]œxÓ}€‰|[“@3ƵqUE2qç§ÏóÊ#rýOèÞFÀ¿'¹ÿ ü@AÂ#Û³owø>ÎeWÜ û‰PT°™º0„§ÞÂSï„yêÌ¡ÜüñC|¾üÎÝWã=£hóY™>Ÿ’]O¶z:ÿQM†Áãè^ó!ÐPö@Õ,È@¢ôÄmãï%ºá3Žºî)¶ÕzñîXÉßýÄîpË¿¾~þ<ü¦‹à ‰')ûusçòÆ'?ð.å˜*ac“àùföŠ:îKOwêüDÂA¼á&‰Fð‚ÄÔJ<†/$o¸Æ 7 e£ÉÈÏóæ°bs‘˜P³~1¯}ú%5!•½{t¢ÀlES†v ¬­ÜÁç¿-dæ_¿rôýO³' àLH/‰Gñ‚„Û™ˆ $¶\‰Gð‚Déï¡pˆ-•U}äqüðÐõÌüîUŽ~øm@—CR¦pͤ‰lª¨§®b'?÷±ò}s`×b®âµU—æýETT#¬]¿šÕK—³xÎlÞþìz^ÿ%}F0áôáœ}üqTÍý”3ßø–ݞݛøñ¯9ìILç˜#‡£î˜Ï¯³7pÉÑ£Ùðâ§j¶ö\EÒsÀf_0@À§)ø3Ž>ŠÍSßãÑæá …øñû7™øÇzÎ14!þMH¸ž;ßz—m•¶¯œÅíïýÀˆ#Ž$ˆøÂÑx¿ƒÂM.´H8„/¤UØgØp-1jÜ=ÌÙZA0âgÆŒ¿X±½0 ñFšè%ÕÖ,–P®V1³~ù\¦¯ÚB0mœçß×Tqlú<#„C|¡äl{Þâ?±˜£*7®åî×>c·'ÎoˆðÃwŸóåÂõPÔŽ~mó!&hIÿ*žº=ì‘BN;÷rÑ“kn¹–7f®Lå[C§ï›Ê·á ˆDãI<nì£'“‰h Š ÿâÑïæb @mX%æÙÊ‹Ÿ}ÉŽêºw%Ïi"¢$8E°mWEýŽàqѯr6û]7›^­Ï>zž!—ÝÉòÚÜ:lêë+¸eò7T{ü,Ÿý-}µŒ‹Ž:¢Ñ[túˆØ*W³x‹Ÿ‹Ž;2§œzë‰[8â¨ãY\Ò7Ãþ„“PÀ!?ào4H|!›c(|ÔD ïà‹ÒÉÂõGôæóËñú¼Lÿã;~^´>‹[ÑÇòù ¨ª¯§nçÆ´nWyiÒx Ôíâ»Éï°d{õuuüôÖ㌼ñuús3GvÓF_±~-ËVo ¾®Žß½Í~g?Dù>§pÃÑ=€÷z,ǽ—@–ùµñIb|þøƒ¡ŒñÅõЧ³=úYYüñ]¼úþ§¼3ñf¿ð0AÄ—úEƒQ{Áw|ÀÏ?ÿÆ–JmNãË8ºGonzòVlÞE$ Ê§íµï¬Üv·¶¬dú‚5üñÝ»ìÛ­;Û¢6Þ}íaM¾>¨çôÑãøæƒwè×µ# v…€¾`Óv]ÛÞZÐ{λŽu›ö°tÚ—ÜöüÛø€‚®Ã¸ÿ¤½YõåF^x?ó—ndéï?pëõ7ðíæ ¸Üx€pr „@ |ÁP‹õdÃ|«Y@üCO>O|íOô8ü:¬ÚImÅzž¹ób†_tç}ÈNF÷Ÿü6Ý;weׂ£ÐŽ/äu§-ðÎÄ»ùᇟ¹ôè¾Üô¥Vº-ê׿üö'`ˆ®¦¨ï±Ì\¸’Ú=»xâ zQªù¯Ó{ÐÑ<ÿî×l¯ñª­Å[¡ñc;w“c¥m/m>Çwë77ͧ‡ÖöÄÓi ¿ÈB4O¯š…âR 1wƒ§ÑŠÅbÁU¤MÅá<À‡u>ιñfú½÷(NÕG¼ßé¬>æèý›9sißmíÒ |g—ýùrÜœóìdޏe=…NasE=^w;ÇôÑ„ÊöUó¸ú«·xö‹2¶WÖrÝ•·1º—Ö×Gn€à£OpÕƒ÷2±k{¶®ÝÄ\Ç¡ýºá0;(vçc46yIâUÛÿÊ+<7ã<»ws̨ó¹ç¸ÃW}|µ`û“1w6WejCY-víÝíÝŠÙN‘;¿qûÔaw‘ŸoÑð» .>ù,~žø^{;oÜ÷'•ExþÝ—yjê4âžJJńӴùìvÈ\xè|î|úŠÝm¹âèS)œµ€ÆŠk+m ò)h(ób4’WjÓ&W¬6J òÉ·j×mV'Å;›"Ðó€ãùù¡JF>þ.÷ôéÂS§åû‡ã¤Gždø]wâR=ø™2þvº¸´±U/Ï–¨•ÑõK3ËÍôíÒ‰_–Íãœ5‹ð}øÂ ÇŒ¾„'Ï?‹Â© :ê~ñ8wòd†Íûs¨ gþ^üp¶í–×kN߯o×·á„{6úòy¿ÜàÜ·'3<ñœ#/þ8tf»ƒ¢‚,VÍF>‘ÎO>ÅQ= øfÎ\:t@»´mE“ÉFYA&{“×ÐfuQš§ùgŽ6šf¬dÔ 71°SW†6 ™÷;—¾ü ŸÑ‡Ï¾}Ÿ‰?LÅ¡Ö.Ä»‰u`¡¬ÐM}utíʬžd¿›îå‚—Þå».d—§’7ài»Ú²fáTN›ÿ+vîdÄÉ—ñÄéÃûÕfßC9¡G>öá„}ÊsÊ©E‹fóÇo±­>À~í2½‘¶ü6X,¬&]®B,y >)+E%bIŠ×éÈÃbiG8X\Lùkg7šS‡ìOiIU5^|o#÷×qZÕ­gÔ¡RiqbEGKyõ߸tL"äÀ¿‹«.<Ÿm67N3ø½F޽‡Oß~¨±Ò'/>ÄõÏ|€Óå$äõ2xÌM|øá“‰ÒV1fý:…_ê+ð}ð ïSÃø”¤ñEÝö´ñ•aÐsÕ˜œ¼ðí·¬9†ËÏ9°ðÊg_²ñçyn^ê–«³ Œ<‹…G¯=›G1òôŸ¸¡KyO:µ±òô-×ðü¸;q˜Uê=~Ž?ûvî¾P5í:Á´¹~Ø@y°—ìÍ»Ÿ¿Ã‰ûh·ƒ¯¸‹;ùG¿x˜1S ûÈ‹Yøí™ <Š ‹·ÂÚÖ¼³ë‘|òÄœ~ÛÓìÕ÷C{(}]£õñ¾Of`¼ðlÆM~€_&?‚Böºî1°:(°XÈË3§ìG¸-£FCÌ=‡ˆbĨ(š³OÑá˜Ð«çt(wâræc)pf!}O¸œO_ÙÉù—?Èý^'ß õž #ï~ ¹úanøiϼ{'§¾ N½‹y—ôàÈÓ.¡²Òż<áfNºõIFúcq>ÿáWÆŸq:^ÑÀqÑÞ'ñóÇ/qö9Wrè€)¸ÝÔy< >î~Ì ´ï¶®Ý·ríy?sg~êñEÝ\÷ì7ŒèÚ„/Ræ³Ï‡HÚ|¶¶¤öÏÌ^W¥¾¦Zê<úi†žºj©õxégQ©®®_$µ_áŠÍ²hÎ\Yº|­xC±9îß*ûŒ#wŸ= Uê*å¯Y3eêô™²qwZù5.»wÊ¢•«dÓÎÝÇ·lÜ W®‘Í5KdGCA©õø3Êyxë÷È’Õ+eÙÆí)¿o_ô¸?C¦nÉÌíô|âñ7åë7¾;‘‚F¤Ö땆 Q`@ª}Ô ÍÚݲxÕ©òiéØA_½¬X»Z­Û¤³\QYµfµlªÒh1ð‰'¤­G<•ZW¢±¦·{}^ñÉ둔ë }%MD]}•l¬¨N)‹³dõ ™¿r]Ý<úØ Òûš ’±âj\>Ÿì®¯—m55²¥¦F<þpöêÕ»eÑÊ•²lóvñ…ÒÒ‡Ã~ÙÐO)ŽíÙ-‹W¥=§Æ¤ÚëZåª][eî’¥²qGj–äâ¯Þ—.'Ÿ+‹·ûdǶõ2íÆ´¡¤¿O¯×#¾PÓxüA¿ÔùÒó5#²fõjY¼v£TÔù²ÒK,–ZGbñ$Šð×Êò•«¥¢Æ'"ª<–mžEÄðK¿‰.ãѰÔz½I*I¶}ÓzYºeWc–úŠÕëÄ“àÓ;¶É«dûž$þWUnB©u(¶lß);ë÷o“½O#wý Ã·jLj½Þ‘ÞG‰ødŪ5RáÑúP]±M–mÒú§†ü²bã:Y¸z}êZ7Ðvr©µˆ_ÖìØÙ8îêú&>ûìÃg¥ËØk¤Ò-ëÖÊ’ Û3ûêÛ)‡Ÿz¼\øþÍg’.ùZ, .Ý¡O‹‘TWWK$±Ž>OT×Ö5òyŠ‘¨Ï#ÕÕuK«°~ùR™?‘lÜR•_¢AY²hL™ò½L9?3SXÉŽ-eö¿Ê?ÿ&+7îÎxGeÅv™9í7ùþ—_eÙÚ]×tž`î)[²°múøê[8¾Tâ­–… ɆÝm¨z©ÜS›6”¨ì©®–Û·É–-Z¥‘äV»c›,˜5K~ÿýOY½nKʵ  }••×˦›³–oZ·b‰,^¹¦©*Du•ÔìñgÖyÙ¾IfÎøC–¬X§_fëz™õן2wñr©öˤÕTW‹7E®Å¥®¦Z|˜ìšÿ¾ØEƒÁ ý3Ä`PyàY­ô›¯¾Vªë›ÇÑÚm2ë?dÚ¬Rá‰êŽuÙ*­ÿ¾E _.m¢­›ÖÊü‹Ëßí©Ü-õÁ4"Öʼ¿¦ËŸÓfË–JoÆ7¶o\-³ÿœ&Ӧϑ-{²ËýšíeæôìóÙš½þ-™ôÿ¯Õ­›&§Üt‡¬¨Žý­û9÷‹7å¼ÛŸ”ˆ´¶¤‚,rÇ7ÊßÏù_=ŠÙï¿$ìs°LÙúûÎó7ü­æYãÛÛeùߘoßxé>aÀ(YÊnl¿üäMÂAcdIeóröÍ{/’Ç]!{¢±ÿ¼]#W?X.¼÷uùß<Ú1½zŸô÷2ÁzÙ°aƒ¬_¿>ã߯ ¤Æþ}{Õ·O Ÿ.ÝݪÎþ¦ Sñx<’——÷÷¨Æb1vìØAûöí1™þ±ùKH,Š‚LïpÙX4‚Éliuõ§G}‡U°ÿW¢bÍÞž·‚3GŸJg·¥užÿ!|»bñ,¾Y^ÉågŒ¡H7?Hå³ï>b£¡'·wP³ïÛVQEÇv¥ÿGx;@uU˜’ÒÂÿÕ£¸ì°Nü Ž`ÛÌ7ZóStÚÆ?Þ¦ß1·óù¢•ŒêÛZ²èï†Ë¼^/­ ³µµ¶ÖÖÚZ[kû_Ђž¬y¶ÖÉÐ3»â1ê꣸ó혌­óñw­è¯µµ¶ÖÖÚZ[kû_ÐìîV°™«Œ&ŠŠZaÍß¹ý#WGi¦¦Zkkm­­µµ¶ÖÖÚZÛ?¯)€Aù{`üã@§*ª=5ÄT£¢€¢Õ©VÒ—DAQÝSfšjºê¥¥4ý'p•†N{WÃ5IíCòýŠvcÃ7%í« ú×2)­a°Mcjþ¹´«–´ë:%M‹þû•Æ Êg—ÞÜKÚ§ô×§©÷+JêÃ"™ãmüxêºý«ßÔ¿§éZ&ÑØW%yÍZ8/)oj¤+ARûÖØÉœŸ¤~6üªèÒyæ8u×91†Ì¹Ó{6mÞÇŸþ;M…ÿÖ0y]i†>³þž´ì’<‡©|ÝD¯Ùæ%7í¦ò€ökêwDŸÖtè!…å’æºá]kŸ.ƒ”ÆÁd™% u%ñGÆý’¶–é<Ö4¶TÙšN£úãÍÉÿº26u.i†>•fUu®¹’ì÷¶ôIôž<¢KèÏ™NRç*•f³Ë$=}„γúzO_.5Ð{ªNKÖM©ºPŸ¿Rä¦B3:Z>”,k¢§_³®U²îÒÑãÙeZz_¥‘›4²‹%sv[4 qUÅd4з¸èo <ÿ‘ 3‹“g2b5P%‰ …0cèPŸ" Òp- “) Å¡Gˆ’$¥‘@3„¿’ô°¤³ ‚JÈJ¢ÞY:¨Ô“…ýüWÌ"È~äF240bϤÌW‹äy›& )]dšó£‡U”ä5JöJj§Óé ‹N}_ ðMÊ}:‚"Ac(Ò„µÈT¦J²ÀSô0A`L¥¯¤k’nŒd™GRŸI¡Ýd…¤ga™s›#3AÇz–»’ôMÑñˆ¾öJÙÎP²ôKI¥]IãÓL×Z}KæügxÖmÎ÷•.H$mtd‹®W8‹Ÿ/e С³4ÏLúØtdb6Ë%V’éTÉp}e÷²fxv”Ô¹jø=cl’ ô¥Òw¢¾K¦]EÿoI­F—.PÒámƒÞ¼©™÷ë9)EßJhœã4¾JOÉï6¤{i ()ó"IƒU›dgNÚ¥—C7dð\2_g³RDwg-S·éôKOçëñ´.JN{¥šF’MF)¹iC1è넬êÙøYm4±EÁ(ßà èl²ŠÕ0Èpy+:«§¢#%»/_É1É„ª÷N]<ªçHRI׳Z¶ ]€é͇dßãjø†èy™2¿ß¸é¥kf±æTÉ-S$Õ˜tÀFº÷KOŸ%¯ÚŒþSt˜7iëUMÉzÂ>0#ú}I®¢G'’iÕ«¢+˜$¬K¦·Iiðò4­“Ò¸j’Ù7ÑQ z†KÖuÓáÝxI]SUÇ«’¼îªÞ¸tˆ&Y)¨’}wA¯ÏŠè»C%íŠ4­u6>Læ·Rtæ!?¥'9vrË’”¡î¸ôlnÉa“K`Ê¢øE2ùJ×»#:òPô òÆïI¦G‰ÄyçJŽwHýÙû'ió˜ün]Œ#©ø-]nI¦l!«7_Rå’¤ÓšœVtd“’E¶7Ú :Ä”! ÓоŒÔõµ¨’ÛÓ¥YI¥A]=)ú4«·£–¼^É¿Kú½’ÛÓ(Ù±uʛè{7%‹þÔåá$]¡(ðSI!¶¿Wûž½.™!?Ù†è€ !»ŸB˜Jª2Pr¸Ä=­d±Jî-®4æÈº!¢èII’&@’6¢’-,ɱ5–Í9«§ß¥É›£ÛoÑ‚I?J2³I€¤ä¯zÞëlÁ>¢ó[Wzk«×/r€=šR²ÏyŠrгø%MXJ( pSÒ¶m’è?›Ñ¥ëyOÒJ3ëÒœa®§ØÒÁv6^Ñõ@ê̳ÒÌvY:`OV¼YAšÞ{ÒÁl²‡Jѧ-¡ùÐÉ"Dg̹×ÏHný•!ɬÛg%I$(©²ª9Ù#éN±êöWt~NZT”3@4¯¤¢Ös¸®Q—KÎ(icRÒ¼ÌzFƒèÉ ½õ}£M²x椘X%íN(Jóî¹øD_ÞŸMÞçÒ§YyQI3.³„´¤'s$mÑ µÓyGŠ3ŠTºþ›¶ èlZh% Ü( Mmë±ñ’¢Oý¢çS’„¦ŽrNVéà5Cág³bt¼ŠÒŒ%ÝŒÕ%:WÇO‰qKgrÉÒGr€Ã¬ YôWsÀ@ñOvÊ-JnAÑ åR¤±M’¦ÕEiÞÈR’…}:`Kû§-‘’Ý“±µ¦¤Ícᔟ™L¯JŽ5W”ìŠYZh”)䎑”\4 §x“·¾50£¤Óµ4g´¥ñoJŒa¿Ð ðKƒ¤+ ɤÝFCL²{ÛôøLgÞ=úÈÌ’åY †Æ(%wVUºñ¯dÊÊ&GU6£3‹lS${ÌîÔ&m_æš=9F&(zÛ¶J£Y’ ÌO§Ò#VI¥Ñ1ºdB²‡-ç\ähÉž7Qtí&]žÓ[‡\ôCš'‹®Ñ7ü•fŒ.¥ ¯×7%§å_4îréx¥ ‹ä ¢MwÄÐŒq# ±áJZzY+èü{:3ÁRƒGš [QÒˆCr{QôPEgW&u»RÑ‹GSv—$³ŸJ–£d^Nú–(YäF.¹—ÅØÌ –“y¡¹Jƒ]`ê脆d'Iñ*-)w ïÙÌfhIYO6¹•–°¨¤;%2Ç(ÙöÔDLe3ˆtæ;ÅhVôè"ÍHÐ{g ï(9À­d1òEG×dq´(éVg—OQš_ =çC¶´Ñ_/…¿·—øg‡rÅcHæ6prÆb iÊøÌ,q”I` ºe2¼yÒ¼‹^Èž¨Û—Ëj’ 1Ť$Åôd€JJ¼K<.ä’|¦U×C Çh04 I(&£‰b· kÒýUžZÂq´û!·—#ëvŒ€¨ÄD¡¼¨#PYWGDÐJg5#0µ¬h$½d—æ²Ñbȳ•R—=…ÉvÕÕ Š¡)«PÑñ,*9¼ ɺ[rdÕêÅä’º³(ªŠbuQæÔÎTÜ^S‹ÁhÈ^Nô=JJ3F‘®RÕ~Œ«q¬ÎBJ­™E SQÀd4èÖl 8…^3û«Æã˜ù´±© xñ‡ãÚzd«­’ž}ž d“dŒªª(V'åNíèÑm5µ ².zó«ãýJd1U¥]Qz§_îñÖˆ‚É˜š ¡Š‚Ó•GI{QU}-aÕ€Q5®’WPˆÝ ì©­CU M%jDQ1Z9Ìx½‚±D ºô-aiÆÈ%SÁÇE¡°0gŠ ¨'¢Š–¡dñ¼)zÞvɾc’#ÿIÇQÌvÊóìx#a|þ –¯èÐu‚–DoÇC(eèEßKª›_Íc‹4/ϳ˕U+šÂ2¾d•iÙ½„ :!Wf¶>ßJzØŒ’¥Z@ÆR\t2ïˆ4ïxINxËZ¹Doë[çùä¿•¤4täPz¼pŠ~‘ì¨ Qàþ/*LþL$Rô@ ¤ /È“Þ8•¼æ’ÜEˆ« mŠò±;kjÁ`@i¨Pô½›’Z›/C±ê ‚žƒ4¢èÖOL÷¨$£›ÈsX9s*ËÖíÄéÎÃjsÓç€ýië2‰©ATÅb¥Ìå`Áôïykò—T‡ã´ß÷0®¹ä m1‚áX3€B¯aÒu“•³ðùË3¿ÊÁÕ7]Bq±xvØ(»ô’›$»‡NÉâU$ÇduÑÆifêŸòî§ß‰™é|ØH®=o v‰ŽÆSÀfcBX¶x4i‰·d´:iç´à¨ñ…1 IåNÀâp@ÍFîà-J†áüáà…2WÑ£L)ù"-3²)(\Õ›gòô䨬ØN}0‚ÙhÄ (yê­œq\o<`š¥è{ÓÓ½«é4œà‹ÝA`çj¾X±•ž¢<ßB¤q=²ð|.1qÍêpÀžÜÿÀdJ†á¼á ‡´&ÑÙiH›CAÅHYA‚JEM½fˆÌÛͼÿÚSü2m1ªªÇq—Ѷm/λòÊÝ¡hâ=*ŠÙN{— ·‚ß—?7¹úΫ)³EQÌv j”mk—ã‰éÜ£V#^¯˜››Å€g÷æ­©§¸}WÚ»‰„„"1Å$W³•Ò™7QPŒfòŒQæOýžµÛ=¸ÝlymØ{ÿ½p[ŒDcñ,q’Òr£&×dâšÕåB­ÚÄ7>Eñ¡GsÖØ“h_h¥Úë!S1”Lc¾%±âJÃ4g,¢¢?¾\%Æ#ÙÂD’«Oƒ›3²Ñzé§lŠæ*C(-ر˹¶’Sê?'¹ Ag•»IIpºÙìIÉ] ÙH¥yžk·)W w#©*ÿŠ@þŸGhÿ´³×cª°jÏ LF,c"iUô™¼%µÌhÁýzÛ6SŒùü†ÇÖ‘!‡ì7&Ñ6ï2o&ã4säªeÖŠPÍV•Àh¡,ÏÎm—ŸÀ„W¿K\0ðþ¢Íœµ_GvÕ{š<åyN>{êN»ùqPLô,s³ng¿l\Å]ÍTÖH)n¦èg\gF{>m¬u 42O`fe”A¥°»Îß‚únÍX÷é} [Œ£‚b¶ÐÖiáù;FsÝã߃¹”²"»<‡°jËTº”©õD²Óš4“ÅŸ«Žiâ^£ÅJ¬f;ÓþZDç¥_‡‚‘hÊsEùùìšý.]Ÿ‡cØø‹o=a5‹â@É´Ôõ ZR÷1ñwi~> ¾~œƒO¼ka{zµs㇠Eã\yû»Ü}õv×ÕëW}j.ñ }.ÎÏgÁ§pÈéwóòÌ\>¸+»êê›áEË+é½ùùìŅsèøþЙOÈZQ1š°ÄÂÌøe*ÆŽ}8d¿ž„Ã!Ä`¡]ž•Gﺘß™ŠÕfÁæt°iéR‚ù}£ŸÃ»šØ]ïCT£ÕN[‡/ßžÀ5WÜÆÎïw&sf|HGgœª@œö 7±O/ëMd姃Aª Äc‰Rä.f÷¬×(?ä2¾Þct#»ü>”¸š– ®dzºsÔ-4Xœ´uD¸rô`^þviâžžÌØ¼„ÌT×û[¾–ÒŒœÃ€µ¹ó1U­çؽ{òWPÒ…g^~“ëO9‚Ÿ—`LMx¿E'ñ$G|Ý¿ #tÃ@ÒËJeää Á,ò$£|~®¤,ZúÞ–ÌÇ¿ Çô@i–º§#ViáÉ-‘#éúG!k²ªÞV¹^HP dwú3êï&Û´ù5( U¥.£oq1&ƒò·Àe ÍëõþCc:õ¬®Œ­Ø.À`²àp9QD0š­äçSXÍfN”ÉT¬§v-?›Ù˜¸ŠÙFÃÌ#—ÂÅ÷L$ÏfÃÒ0Ûª–aos:±Ù-Úƒh•$¬v›Y‹°Ù8ìTìN7…‰~mûÌátb1*ˆb$¯@»f·˜PUɬÁ§';V§“‚mùùùX-¦¦m#UPìÎ< òó)ÈÏÇl2 ñ»=.»çEvíÞÍä»NT¢ñÌ’Î<'{6Íå‚›§ý!³#eíŽ*6®ŸF÷¼ÕÞ€~ž*ˆ(Ø\.ì6 ¨jŠL2š,8óœ( Ô(BOþø%¯¾ý5íöø‚)K¯Š`´Ø×Óá°$b¯ì'6³Ád±átØQDPEÁnwbIZßìֽﴱqÁ'\÷ø÷ìν„"•l©¨cÕ¢·pˆ‡jÆn³4zU«ÝÝnAUU & N§E“Õ¡Íy“Qiòš‰VÌjw6ŽÅíÎKx3Áå°áÛ¾€³Î<çm§ÀîH‹# tßãxûí×xü ÔCDÕäáŠÁ„;Aßù.Œ†D)2D›{§6G&‹=AnŒFé¯a.óÞQ3Aó"TUÛŽ~ï¯,]¹’Õ6°~ëf®¹t*êëQÓú‘ïr ¨Z¸FãÜŲ̀*8ò4q¹ßh wPŒ8ó´g~¡f¨ˆæ]wä91%øÈ] õÓf2¢6zET޼†¹ÈÇj2¢&úb$´Ù÷8&O~÷Ç_O}(D$áì×xÛ™è§àp¹),ÈÇå²k³)`²9)2ÕpÛé§qÿSßRh³bP#ìª÷pͯ³nûV­_ÇŠ%Køæ™‹P0 Šk0îLÚ:l|4ñ:N¾à6B]Žá§y+Ù²ð=Ú8Ù³‘ãÚçqüåSµÇAy¡‡ç¹š¢¼öÌÚ£­ÛÍóצcÏykÎ6J ۳᫗èݹˆküŠvn7n· ‹É˜ _iä+³ÝŽÓaCQšê Øœ.Üùù¸óܸ\NL„©¨qóóß³{w÷ž1d+jàÅhÂåtb2$õ7˜p¸\˜†Æ0 °;\$ÖÌl4hyN‹»+ÂÙVÅl PWGÀÕ_*C,˜þ5-ÕÜpêpîze E®úD†»1CCü P\X€X²j)þ‰ž}úQ^heWM-þp«I‹·4ªq‚A“fýÅ…H4@4Ãh4¥ÎerÉ!Q1íØªÅðÔ¤ñ«6zôî@u½—°?ˆQ ‰mJ0*Ò¸îFƒÔ8õÁ¥؈°dù*ÌŽbútkCMbûƒ‰öù.A«–n ‡6åhߦÇ‹(m×Nó&¶) ]~>q„ªz/á«Úƒ8œœwÞ%°³¶%Û§ÆU¬…”aú5ìÚãÃ]Ú‰}º—¨ ÅPÕž`˜’¦~šEôéÖ–Z¯‡`T¥¨¨'*ËW-§ÆÄ•_Lyç2,f‘H*Mƒ^ O0ˆ‚B,&Äcqì…E`ýúµTÖù)íÚ›žÅTz=¨ªJØ£•ö…ª«v°¾ÊC»NÝi_XÀŽºZTœ……*°«b^ƒ™Šêj@Áh4‚@ØÀVè¦X¿~~ºöêNq4@µ7ŒÑj§Üi£²r+7íÆ’_H¿>Ýq£²»Þ‡‹á­ÞƒØ]œþ%ÂΚ: Ɔ5†°?ØØÏ=•;ØXí¥M§n”ä³³®ž<àj‹(t·kô«@u}pKÜ€¨Pì°‘8 M1£¢ …ùN¶,ùޱ·>G»}/`Ýâ·pµ¡Q ÄÅÎ 7^Ëí¦Æ ]lanž?“ o¼>Nð†" =ùnº‹»o|˜ü¼ÞÜtíµ|üYœ;do<õux# î¢|Ô¸‡¨ª)?³ÙŠÍ{j<ÌV,6+mV¼Þz¶TT#6àWŠË:Ph†.eÅ4ßoÔ›ª*xüA¬ŠÆ¿‚‚W û|ˆ"ªbÖä’gK—&Ë¥ÕþŠÉB·‹šú*–mÚA8ªPܶ”’6%Aâ±0þ°‹ÍœM[Ú§+^q&#ŽØÎ½œì¬õël|d9÷7[Ü$9v¯ÒcS•\€Lr{…Üåá”æ¶Ž³xݲ…eå¬2~¼¡@W²Ìe.§R®’XÍEW¤yO§ÞåJ®Ìº[¨³½ž~øBNc é£M'±å0D2g[ëtþOE 4–ŒÈrÑ\»"w«¾|’áæ³_¦pêaÝÙ¯ß^Ø¿?=÷mÜytÈÏcõwŸphÏö 0û÷äˆSn¥Îc§Äé`ÅÂùL™ò•!¸gßÿù'Ÿóý#{âJa®2óo{šXæ1ÓÖPÍ ƒFð—¿ñáÃçÓ«g/ìÕ›£Ž¿’a3N[£ÇJ׬˜))p³àÑ¿í¿/Ç?œ}ºubìM“ŸïÆj6Sævðí'/±O§:útçÄ+ÇoqRè° P_ œ!3Œf ƸŸ™3gðù”_XúÙ'Lñ+ïñ ó6Ua³X’BƒR+my”ØBÜ0j_Žq=8òȳ›Q1Q–ŸÇ§O\Á¾ûŠ×ï<]:Ò¥C9Ç]{?1w>.«QóĨPTX@x×rN;êöë·/‡°û0‚_wE(s —¼?·<ù5íòݼÿøu 4„튋vvWŒ:ŒIß/§­;/«à4[í¨J¦Ïøƒï~ÝÀןÌïSæÃ¯`}PìˆrÃÐAœwëÓ¸ÝyX¬6Úº›Ö½­ÛŪ/æÄ¡c™µh>'ìÛýöîÇ^Ý{rç¤o)Èsc5i€sñ·ŸpHv Øw?Ž2ˆ^íJ¸äÁ—)ÊÏcæôi|ñýÏ,˜1…ß§MåÃO?å»é‹p滉í˜Ïa½»Ó§k:öéÏ£ïͦ¼  qÀ‘`u¹)/O\}=zõáÈ#³o6œ3î5bVmÜy,ùä!Ƥõ³÷^Ü5é[ \n N¼›–rƨAìÝooŽ>œ}»ÓyŸóðã&?Ï’"ic‘‘H„p8L,ÞW@qØËCBÏž½9tÐôêÒ‡g?]J›<7mÝFÆ{8×>0‰)?|L÷ò¸W?ÂOëvRâtâÌ/$/TÃÃWœKyY:”·çè³ï£öíÛÙµ€Ãö>„×Þþ‰wn<“ž={°_ïwÎ=ÔÄä;”9m|òú#ôkÛ‰ÃÂ}{pÄI³Ý¡}¾›à–¹Ö«;½»¶§cßþ<úþ<Ê 4ò0Ûµ5>whc?»µïÀ{õeàÀ!ü¼¡’R§ƒEófòÝ石ÇUÛ¦3õÏé|üåç|öýoøãFLáP˜H$B´8Ô$fQ,Äxèú‹€|>øþM\ÀöÚZB¡(Žs”røi§Ñ¶ÈLœ0B€)ŸãŽ9…¶¥f"ªÊ€#å°=¡Þ»€>gØñûà«XÁà^½¸æÑw)u»1)‚Áæ¤ËÄí£‡pÄ©·atºië°òñÛO²ÇBöêÕƒþ½º1ð A<ÿÆTÚæç øð‡¢ 5Ô$”óóò¨\ð ‡ <ˆwçì¤ÛÓ‡oÓ,†x ïÏÚDwenß}ò2ût.áàMr)`ÑÖ¬ËÅ<ËÀže ÜÿŽ=b0ý:·çæ‡>¡]~>f ô±½¦ÕRÄÇ߀™z®÷*&,Æ >k­Ì´£wIa%ýÄ'E_ß·¤¶jKB$³•ôk®¦sÖº¹©ÞRI?0DÉâýMü(Y+òü šÇã‘ÿ‰FeóæÍFÿ³ß‰ÇeYe•l­©‘Šº:Ù™ü¯6íïäßjþ¿^Tùáé«Ù¦¨ã0y}òRrð©ãED¤fɯÒÄÒþùeö|ùàÅû#.œ$""w]=FL†Da&Å F»] FEŒ–Bùfƒ_Dj¤7HÁб‘ºpLD‚2¤q # ÊyC:6öcôÕãåÉ»Ç ï¬òŠx×IW#btiû\·<ý¬F„öûÈ÷³É¢¥óåñ»®•{žyI*Ca‰‹Èâ?Þ@Êz/3çÏ–çï¿H9}ü'""RQW/1ùîÉK·ço‘uuR+"µÛ–ÈÁ%Τ"U±ØÍ¢¬rï”÷¦®Câ¿õ~Qå¡ó /ÍØ.""{¢"Ý.mA覈ˆLùäMyä‰G¥[¾"ô8Nvˆˆ7è•uuRŽŠ­–á½ 2éãi2祭‚{‘!¥HÑ‘—ŠˆÈÉ]´¾>?½R$´B ÷}½@ëkm-%þö‰ÈÆå?KIž­Iä±XŒb°È×Më>,±î¡¨¶î푼'ŠˆÈÏOŸßø¼«Ý!òìk¯Ê¾Ý]…2{sXDDVO_ì J›ýåÕo~–Ùsÿ”ë.+×LxC¼þj\ìÆú£XmQ¤lôU²SDj*VËO?)÷Þ|±rÚ¸ï›èÅ‘§Ï*€ŒºõqY¸bžŒ»äPäøÛß‘ž:/K?‹dίÕIÀ,ÏNž&+7¯–×^{\.ºìQYµ#$uaŸ„Ddæ—O oÎÚÐ4¿uu²Ó§õã±3´ï^øè2{þlÖ½D P~Yï‘Sm'hQrìUwË+O=,€t=ùªÅäêÃúiüyþMòÕ÷?ÊŒP䇭Qñlž¤"óäö‡– ×î¿àñ¯DDäÏ@Út®Ì\°LÞ˜8Né4àLñDDöT®‘ÇŸ~Rî»ù’Ôù¬­“Š€¶ÆÉý<.©ŸÝÇÞ&""7œ´WS?Äb·‹Á ˆ­` ÌÚ’°„´¦ñÛ—Î6™º>(a‰H½ˆT¬ÿS\ ûŸt¿ˆˆT{jé³&*¨].e ùÝÃè+]÷?XŽÐG™¼BÓ÷Ÿy&oN%mK;Ê)§*€Œ¹ÿ=‰Éáå´‘‘H,$AÙ4ÿ3dèmÈW/Þ €”v;JÞÿäkyóÙ»‘L‘Ê:¯ˆˆ<Å NùsKP•uu‘¹Ÿ@®ûp~BЬœý‘öÛ»³ErÉ¥‡?‘ÚsÅÒ®ÿH™±`‰Ì[0]î¾æ™øÒâ ‡¥"!ïwÖÕËŸÆWçÞQ \æîŽH$Êäõlº$IèÞ«#/Rþ5Þ_ŸC7%ßSŸû»µ:×Ò¾·«á=)÷Õgï{mÒ÷k³\×»¿6˼e›—ä~Õ¥}3×ó9ר>Çxràæ¾§;ÖkQÛÌ{êš™ÃfúUQW/[kkeie•Dãñ¿ .khGþyžÎdg¶Ø =‹3a*hqs§VУ¼ïÖlý‹Ï¿ˆNŽ<7'ÜÁnì|7gG ÀØ+ï缡ÅüþÖó¬õÃM¾Éö8²§…CNbÝÆ,]¹‚yKfs`{;}Ú 8ß­Ù)ÚáUîBE‰-5»»€KŸ|•¯Ÿ¿‹sO:›Õ†£•R3Ä}V^˜¶… 7\Ǹñãɶ,Y¢ysxxF×yÙ×bÜúôíÏ~{à¶ñ“xèú+GT D˜pçu`nËŸ+¾eð€A\}ߌ,…OÞx…í€ËlÐßµÕ{ »:ðÅòõüôÖýÚX^ŸÂ–[Y¸|1؎ݯ~1tE‹gó pÓC“p?2‰Ph‚_^y‚ÝÀSÏàðÓ.äÎ[ïàø£÷ƒx$a4 Š(YLL{óq¦®©å¦Wfpíé‡3p虼ôÐÙÄ×~ÈBo„cBMUíõ;ØT­ áÇß~aGõnÌæîÚ³71¢Ykhú<lí°tí&Þ¹w¬¶½þý\6oÙÊ‚%sXnkZw·•¦íwÂ|¶œùZ˜BQï3Ù²í/®»äRž¹ñ, –Uu> ƽ×\NÐÒ‘™kré #Øÿ !<ûúLºå"<8/ßÀ´ï^àÚ—¾eó¦-,X¹ŠŸŸ»ñx‰¸ºpë 7ñÀÄ(b†Xã8\.»7ÎâÖ÷§ÑûÄ[øî‰ÛØ¿ß<øÚ N؆ï¿™m@‡Žuú9¨aEu-Û7m è¶_úvîÍ%—ÜÆ¯ÜA¾Ý‹×Í™´•ç´±sÃTîøø/œþ oÞqƒ â›ï^jyí‹yZbPI[PáêÇß䇿²ïæô~6-ÛBX÷õ$^˜±’£ïœÄÔÉO2æ¸c¸ñʱ€àDPLvœvÀZÆ«‹6ñØÝwóÖO_ÓÞ¿ü1xòÖ;¡t³ç¾ÃàúsÑÍòòMcغà#^˜ºŽ¢Ò^ÜvÃMÜ?ñþŒùlð8%õóû†~ö5°aá vw¿1íëþd#ròýlÞ¸‘E+V2köÇttF¨õ„rfoÛ€-«çㆌ>€p¼é ßOÜÕ‰÷~ƶu è]äaS¥›¯ç¯bêïS9¢ÜF}(ÄÉ·>ÇÌUÕ<{ó©ì®ÚÆßf¹Ürê0âyüéG€Jž{ãÌF+6à•»ï¥˜=‡˜^õ Žgí†_8ë´Ñ\xÝÕ´Á’Yj”'âCÅâÒø¢a§PŒ–}j×&Ü•E.½ù*õ@4^Ept¡÷>ûpà‡ñðsOsó'QíõOÞ®Œj±ßcŽ>ØÉÒM;1¬-‹ŸÌ8–ž ”C/ ÙO¼Ó­Ûœö€Ò|Ü¥¤—êAç„&ÝÉ[ ú'Í5&e)ʯWHÉû(ú¹ 9c_ERËàéŪf$@*™É„Jï³^†^Ń\±¬-MxÒ hX:õﺱþŽél8‰(g±!“’¶¢£~ãÞ ²v/-^‰¡° âÕ¬^³rç9C¹/f",ž³pS±'J¯NEW„ËnœWD×víPÛµ#„ã@HM݉P¬Ø¬Z¹›†V[¹:Éø›.% ê0œËWRÜ5ß®õl ÁW?ËU‡wÄ‹'H™ æäSSÒ³Ž5ùäuêÏ‹OÞÆ97?A÷|3ûÃÅ\ÁyçC»<ÑŠUìX\Q?qq1bŒD˜_Á€R‹)·’ƒ ã¨Š™ò¶Ex»´ sçî´k×w»v}^BÑxSÝÊ$&Rkèó…(ï8kOèÇøo'ñû®ÇQÝ÷¶ò#¹xX7¼a¾ˆ {žzoÌæÆÞˆA‹9 _3íù*½~¶k;4î)),†Â¾Øà.jKY»vµkGo½—¸*ú'ÿ4Ð9`ˆhôåì’ŸK( H”ZC<+pèé7â¨ËÃ1ìª<µxÐqÿA(°vYwn¼õV ¢>ºpm£!j¢ÂÁ§_Á¾WÝÅëÞÏ“×üc÷&ý´†Á=K?ÌúüUj['ާظjêqØŠŽRÍÌ n ‹k’cv‡æ0Ù µ ¹äã‚##&&Ì‘0óriWúôƤ[N庉¯Rj~•Ã?—³¯¸Œ‹FF‘ÝB}(†Ñ¨4&^Ä€veÚ7ªê|Ù·DQrËJîcw•æöq%GíY%ólùä|„TiŸ}Û]r„ˆÎv­’W(’e YÀjF2oò3’Yª/¥²”’Rþ-£Ï)õ´•,µ3É<×=¬*‰\\!gf|ŽòYŠ(Ù«NÜ9ÎŽÿWNXÊþ[Aç"‰¨)þ!£Œœ’Åbk$’¦“bqMx—Ú´4¨b¦]®šç0æ%Pë…âþ\rÁyøkëFàŒ‹¯¢°´ŒrgºˆK”PTE‘1 ÞSO‹ÓÝgh0h@Áb31ªvG0µ±4v+Ai  G±¸ŠéÚÖŠ Ä"b@Ç^{ižŒ8ä9l!SÒâC”´P£ajÂfξéqö~"ï¾ôÏ¿ú.×üñ5ï¾wß}ÿ4V3µApïu\x:55UDUç\s¥¥]É ñ¨qŠ]É|š@¯ˆJðøZ?= ¯¯¯o,ó‰ X›“»™p$ˆ'Ö®EC„±qɸÿí©¼ñÅtœàcúž87¿67°3C1ue_Ã),>0qæu—Ò»½ÜgžKQQ1‡¶±àï³ó—<úȼx<^І½>‹'ÇϦǡÃéà‚ˆ/ž5˜_1(ˆÄˆ'Å·úøc!œ&5Ë4Äd5A\ó¸¶ëØAû¨ˆ{Yêƒ~E…8,&B@žQRô½$æÂ ëÙÉWÁpŒ’žéý,Ñr·ƒ€Õ_K,Ö`„B]]=’H P²%=HØPÍø7×ÑHl çšcÀl„šú:bq•s/½™ó]55ìPŒF#ëëñ©qŠú á©'†à úÙVSƒÉhDÔ(r×ígrÆmñËV?öo&ÀÈÝw^ ÀîÍ[èÙ¥£æ™"þZjkCY5yz$ÃC'j‚ÞžQ'°Ë_¡íИ4ž© 4È¥3¨ÙSETLœ}ÍU””tÅáRovpí„O9øä)¼ùÒ‹¼üî»üùÝ»|tù³|úÂu¸ðŒ6й‚ ¨Øå m¾+»ÒOéošW+½@¸(Ù³ˆë1'€oƒ¬J?"×)eÝI+¯“^GUÉLr!W,¢4“0•eç:àOQš<ÍÔSÕýžd¹®Zˆ]ïlûôD]°­¤œ8–QÀ>kòT–wÞÙ,×Ó1Š^Ðl1¦zÕ©Dþ–Àó}"QsÄŸÉhJÒN{¨‚ Å"øêëÁ`Á‘WJ~=à;?Çž1Ó^UQ»‡@T(°¨¨Ñ»v1Åîü”†X bhÛS(ðâÓ×ñçŽÃŽ*nê–Áá0ÀjPP#aêêBØ ò1`m}¢ ñ8Q{3YŽ‚`0ñÖ{0ÛŠÙk¿Á<öÊ`{åž{(·ô ï.¸…w£so+K|λðB,icÝQ]CDŒ o­&ÀÛ–iÊ»ÀídO­WËÖuc¤Z›&“ K¤–Åk+h[Þ ·ÍH0®¢( µÁ(]:‘Ó÷îÈÇ_Î’WbPÖŸË.D$Ja.5›“r›‹@¨ž8pèþ=àã©t;ðt.äLIœuPSWG g<œû–Ó± ÄŠ\ØýHŠ€Ê˜Jæædl¥êŸ<“X÷8ÄÄ¢­»¡i݇Ž(I¬£¼L CŽ‘`,íÖׯ'_ȯ›ã¨.ùÄ’˜y{M]Š~©«ÖÀ~›‚|L@¥§–hÜР݉®bÍ“Z˜ï$  å=`æ´/áê´/Òh÷Ã)_…ì›Õ øü!@Ñ ýTŒfÔP€Z1Sܦ×Ýù8×Ýù8Ó>ϰ³ïá‰Ç¿ç‡çÏ!’ÃûºtX"Îd/.¼äT»Œ&¢¡$¨Æ¢Ch~dˆ¬d‡öÏsàÛ±³ožqYL(‰_§VVªørâ#lTáæ‘g`PÌô+ߦ̦ (Í×B^¦ô ý÷ë–"裀«¤6Ÿn'{üj"œÅ@4˜èg°ª±X íPˆª°­®¢qÍÀôQŠjT/ˆ‚:÷='ðçW?ƒ°˜¢ñÔ]P0ìn¨Mj4bHÛþ5DýõìjP O¸¢Ä©Sá¤Kî£óÃqûèÁÈîet?îŽë¡í.Ø ´ºÏëÖT@ÿB¬¹áœ3ð¡UPHñô«å¶ioÇäåYÝÐèÈé(ÒnŒTsýØÛ»!J1{YYâ êË¥úZª«ý˜Ú–pÐàc8hð1¼ôN€³†àÃW®gÆ ç0¦O!Áz†-,ZÒåWS~Êéߣœˆ„õ=z’}(:žÊ¬uK“‹£KSæ±¢¤”@MCÙkJê–=R2=†™ǧv2£æiÓßžE½p\ÝŒ2¿©%t³’#œ!ٱќ~ÏU׳9/t¶3ϳλèfù7ŽWóäI9¸"µN5©Gl7®M–ú§ChÍ^ÿšJ‚Ø¥™ã¸” ©a÷*ÖÀf$®¦a“~à¶û䓨 ü€IDATn¢WûA|7uËW-cÂí³ÿþg³Ãç¤À¡ö>h‚‹>â®g~`ó¦eÜ|Ç-<;u58Êéµ·›M_½ÃóŸ|ÀÃWŽáê›^ÒÔh$ÔØÍ€Ï^Æá?Ä©‚‰“H”/ÚäiË8!$©ç»Ù4ý5úöȽ“ÞbÆŠulÙ°’Íëj(s[ã¶üIûþcùuÎ*–,›Ãm—œÈ~§^‰ÏY€Û¦CÂ=óîË/ñé‡ñçìå乜¹©jTëS0Ï\ƒ…¶y.ÞxôZí·/Ç]ù’—‡Ã¬-ˆ ¹sÜm°m«—­ãì[綾µþPêÜͰj6/~ü }þ+a±FÜx'ûv€ËnÇ£ïýÄêMëùðÕ‰ 9`_>ZZC÷>ýè02è Î€“ã÷ê­mk÷Ó€EIAeù´ËOýWVËi!žð°ÇBЋªM<Ž‚£½ú»ÙôÕ»Úº_Õ´îÁÍÅCTBÄÇjs§yüBAÍ[{Ûc/ŒЋg?ŸÂÜ9Ó¸áꋹöñçqæ¹PmûP ¼vçÅü4w3þˆ[ÎyŒÝ{ì¸Ü¦„NÑǼïßç£O>aꬕ(¢PÖó0î?m ó?Ï…¾ÄÆí[xäÖÓøìÏ F^>Ž@Må­ŸBc?%±Æ&w1R¿†Q]:3öÎGøiÎb¶VlcÅF-«¿S¯r ž*/}þ÷É-cöæ‹ §qɽϲjóf~ûîcŽv7Nøpðñøƒ)ÆbÐï:ÍØ8ièÁ@%gžuo¾ú»`GÔ Äñ‡b(& °ôÏøsÖ˜Ó/fñ×Ï0}»P’—O\$÷i-¹²ŒIߊUðû˜ ûpǹ'°fÉ2ÖVÀÝ\ø½†ŸÀ“—ŸÂkïLæ¤};ñòŸÛ´ÝºÚT‡›jêyýÅwyÿƒX8'f ¤·¶{óÒ]ãøàÃ8¸Kg~ذüžú„\z6E.-^Ú —®Â‘_ÈšïeŸ‡ñÄäÏ™¿fV®g}õÀEû|ÑDÖ¸*´sX¨XõLßN¿Ó¯`@‰/˜ãÔ:rŸ¿­{‚™ç€g€*Éô¬AógÉgl­æR‘’é)×ãLì (9Œl=þÕ˾W²€a½ûsa™,ê*Ir6 $‡((Mu6•æÆ@ŽS„Ò üÿ‹ë’íý:Ç$*Éq²¹ÞÕêéüû73Œ6ý›ô7öb"X òÉs»É·ØI>M·×y}t<à~úìU.ºø2N8òòœN¼~?CN|ŽÅ6"aQ£;&|Äʵ£yôÆQ¼0ÎÇgdâÑW!˜¹g«,:ëL®=ãl0ºyý«˜ýúÝÌ … –µé@±”b!®’rƬª˜)s»)*¶j‚J…¸Á@Û6n\E¨")Ø:}["‹Óg¿Át1Lâ¡ë/âIWQ¿—¨¥-×>÷-£÷*¡&äÀ1WðÑkU\u齌8øsœN~ŒcǽL‰]!îSQEpµïHžÛÍ^ÍÀ…·MæèÁûµ­skyn7mó,M‚5m |uh©Ti[È KÕTGãìwÊ™ßó~¦UsÝÙ§G‰£$/tî·?y¿ÂÕgž½×I,œÅÑ XºðÛ¿qæèó¹ëÜcïÊÃïóÒõ #éQ–‡Áldï!ÅlØ:”ƒÊ-ˆÀˆÑ'ðú¼]ì7¨Ñú|ñót"£æ×k˜WUˆÄ…>}Ò·g[â"Ø És»É3 ÊK$@Оϸäu7¸yý«ï™ýú8æ…µu7’ïn‡Ù` &q&»‹<·—Å€ˆ0`ôE|ûQ˜ë¯º†N=‹É@$¦2òò‡p™MÔûüäwÚ‹'Oàü‹oå˜Aûc5D)î4– m A1»TVÀïS&1öÇg9ÿ–78‹÷½ý#!óYKÐdÈé—ðÚ+ÏQJ„Jœç?Æä*•KnÀ¿¼‚„‚ôr&_|õ–(5ì;F³‹eüñSb>o}“ãÙ…¢²r:´)h,jßÔÏR "ÄB|Ö<}ÿ+Ö?–[GãA»¯¡€//¹»ÙLÐjET-AÐl1¦ÄÀIÄOÐYÀ}O¿Æ[Nfìq³jé›t(, & Œ4ÅT¦ÛigQgq˜Ê¤†XNºâfxwæýOåÄÛá ùñÆÚ÷ÉÛoå‚[&pÙùâj0¿Ì›Îë— bÅîµD0µmËÒòžä¹L¼î<nzêw?¨+zç©ÛOâ¦Çßâì³Þ¢¼ÿÉ|ñÎX.?çjT‰!"xâå|ôZe†\:nÜËäý†mÂ[Ü~á©8ó\½>Ì…ÝxâÓ/Ø»ÌBM½³ÍI™ÕÑZÎu61Ü<7þ& ĈĕŒªGžJ½sµSân%;0Ìæm“(¸l§ÖI޾æ’j“TNŒGržF¤c´Åó*ÿ¦óMÒDzGX¦¸—ìà>›‡6¨%Ú;*Y¶ösZ½…ɯ›ÎoJZ˜Ž^øB¶séSNóûûy;ÿyÇ`аºzù&#fƒ!çng&Qk…x,H0ÇnwaPT- =Á"‚(fÚç;©­¯dùеcfÚwïA¿öÅÔú=Ã*¢@aaæH€¥«VPëSéܵ7íKxƒ!ŠóÝTïÞÈú-5¸;ôfßò<**wáq:´ížp0ˆ Ø-Vít”¤~*¢ 1X­X¦Ý&~S¬Øl¦„+ºLªª‚#¯·)ÎÚuk©Ü]MD,”õèCÿ²|j½õø£*F‹2—ÊŠM¬\»¨ÅA—½éYâbO½‡* ªD "Ä¢a¢qÁá(Äa7¢Šª|ŽÅ†BØv Š!µ­F“ÅWÍ¢•hß³Š]ø#±F^TU°Úí¨‘:¼A;Åf¡Š('/) D£a"‘(ápÌV ò\¢ªÐ¦°S4È¢åË©® ѶcGúöì‚ôã‰Ä‰DƒDÜ#ñ˜Ì žzÊÊXõÛK|ô5„êÛIÓ¶¥ªÂ-þÊ· cw½‡Xœx-gÝú§ïþûRh€uÛw`q8°ˆÑD™Ë‘KÛˆXœtíчž%N*=,n7Ž˜Ÿuk6°³¦Åè [ïþt+vPY_OÜd¥ÐifÅ_?pÕ™ç0w»‡;^þ‘G/?†Ýž:TÕПÙl ^ú¶5ÿúñ“ÙÀšèÄ“¶pWµ!i/Åèo΋ڢ÷ë$WI @tÎ0°,GaêMqK’k¤àªóÎ”ä©æ óKŽ~µ(v5ÙÜÀ“YΨÏrLµ…Hј ” /©Š‚Ó•‡#ìSUü~?ѸÚ(ØUlN'N‹YÛvUcø¼>b¢ö»ÜnlˆÇcÔz|Ø]nLÄðú Î<7&‰jI8‰’N Ú±~FyyNbÁþH b χDƒZ9EI#ÎÔÁb·ãH¹§q‰áñøˆ©‰Ó|Dň3/{" #‹áˆ©‰xLf;N‡µq’"áÁ`4qv,Lfòv‚~áX¼ñ¤ †Qãq,Î<Š­&B±(µ?Š!5IFmN̆8^¯U”Ôa جÆ$Åç  ¢½GíèH—ÕŒ-c5ðŽÄ@ÀárcVbx|~T“ÕŽËn%ä÷ ¨©õ’(à’"À‘_ŒËf"W1Yí8í>Ñx¼¤ ‚(òòë®Æ¨­O¬»Äðøƒ˜­vœv>¯‡†è£ÙŠËn#à÷‰Ç1ˆ¦ø.76“‚!Ô?ápbÎEÀ`ÄåÎÚÀÈáP(’pdb0ât»[JbÍ"‰¤(Áh²âvÙ1%ÞïõyˆDµÄ“ÕÓfÂçó6õÓdÁå´k|WGe:0)$è|¾Äú¥Mg¯[ŠÚÓ³MP˜P,Ε·¿Ç=× aW]F³·³©áp@ Œ 8ÝnŒªÆ3ŠAAÁáÈÃbPñxý¨€ÙêÀíÐ"ÿ|Þz¢˜p;X ~FŸ§1öáßùàîa€ßG8KY7#îü<¬ v „øƒ‘¤Ê >q»…}$ ÖB#œ®D?ýíÔ3ý4ªx<~$ϧ 8\.ìf-l%ª‚ÓçÑ;.çÅw~Æl3cÏs±eéRø}c@;{½Î§­™ÕN[‡•ÏÞžÈõWÞÊÎ ”î{sg}D©)€'ͪ´Z ~´CÓŒ¸òœ‹àõ·úD´xñ|·K‹9ñùÃ8ÝùÕ(^°ñ•›‡ÕÜøâpÐG0ÓBMM6Ü.›v2[8@0ÇçÒî‰Æ5Ìe0âråaK=Ñx ŸßO,®z«ÝÝjnÜÒ‹Ä£ø¼~,î|LÕkÝÿ¦U ¿3_y‰›Ï8–¿vš–¡q7CÉôVe‹,÷4ëöIÖAJv`›kÛ5ËZ6̧ƒ¢?Ž–€©lzDɵÿ/9€srŸô½5OòßDUù[Ÿ½þϪ°jÏ L¦„§³K)€6g5gÅé–lhƲMÎXÌÒW DT¾!ý=ͼC¯_-+-dˆ†~ê ìñ1)ÇtætÍ׬€ÎèŸ+;QѲi&+®<»~ñÂÁp¬ „dŒ‘ìó/-ð4²H¿ T³y-rEçÜ茵Sšb铲sív+{6/âÍ· r×6êCQLF#Eá¨Soåôczâ †2+šÝª¢™$í—ÛMõ’)v䙜ûè—¡¨Úd碡´t)YçJÏ"YžË6aº€DG¿èÎCŽLõ–‚+­|]0› @µP·4ê ‘ÜtÐB=”së[É¥‹[èÕ ?=ïsR‘’C'éÍm.—…> ŠBDU5ÐYR„I1´‚ÎÿOgut*JÊéW©Çle±Hš+Œ«C²&§àN&©XˆHîgÉ’¸ÖR¾4“Yg­%ÌšÕ:Tôc‡šNYKX)Mñ(ÉåDZ®ÔšÛšj#gÖ£SRË’dUÒ:{:—ôX!%%†*+`–ÐdK æÀXsJ#çºf)¢d÷zÄãq¬Î"J­™¯¡¢ÆÉhÐq³µx“Û+ÐÐ/%®‹†0;mHÜjÔe3PZ('RøÑ1n³{·šhDˆ©*튊0ë,ÃoÁ(‰š“©;1—›B“ö{e}-UÁhPrÓ™’c»4M(-0’ç4y”Í+”.·³C9dxÚ½WÁb§Ë7Œ†Lhù=ZÙ#0`4dᡜ[É9É®´Ó¢¢c¸¥¬¥¢7èìòAiNVe‘ïé´ €b4@<Ì®ºpJ1m%™_õÎÉ–a×!éÀ‘Œ„”¬º'ÓpUDÑJe-.Ù3½sAÏéäHö<&‡W$wâPKõ®¢ýËå%o(*Y Ü“&ï% ߣ#òɸ´>*Ik›¾–Ú¼¦;o’ë½*úF‚±»)-Ú^øÿÛþ%“Ò gQ’Ù”cNKNÉ@ ™ÛÙN¤Pt>%-8© ç¥ùÀ@Éæ¥ü,êä“£4?—Yú¬¤ô9‡ÇOi¡õß8fÑ?~,éô÷Œ9Râ\SŒ•t’@²(½¬ (i+F’: äXs¡™ É\àKô$;}(z5ôÒç)ýˆ¾l€Y¯rzJWÉ}¼ÒºjH‘¡TÅñ>‡H,¦=çQ…’ýýŠÎº5>#™ó¥wºPÚi"JãÑèÅ–fŒˆ–H&ÈNáÍtƒ‚f<}zô«Ch’<—¹ÂäŒ;Yr8 ¤F¶ò/Ï’ X²'ï$jp*i€8[ÉtPHÆI5JÊš)éõ8›ófð¬’ÁÏGdfädÑ+ Ù ’çò|‹Ž7V!ûIIJY“®§Ò“¾%íˆÍd§Œ¤÷?¹T”èÓ{s²&i‡±ÁM_I×éÆž^P}gIN”þ÷hÿȉ44­±¢–£'8Ò÷?uÞcH®Û¥£ˆ8JÒN¤@É<× “™¦·U’qœ•’Å[%IÊ0=†$MRHs&¹’nî¦ÍiV—!ñMI+¹’VÿT×Ѱí”4&C®m%­œGšôR”ÌoéGgné$+Öô÷¤?gHSÒJJ@M’!¡è—´PÒé/­†ôµI÷d(i§n(9¼·¢߆’ºæ:ÊUI¦i|4Þ䱪¢h I´NZâD2m)JóÅ¢EÇhR²¸PtXRE‡V”äëz`:Ýó ¤*¸å¬di^T%K6@Ú•”# Ó¾¯7g<˜E*JV0ªä2f””ÝLšÒ-žd ITI›{=#.ëñ‰äFÕR‹£+Íì™+Yä $ÓOêsJ6Ôœ^¢*Cnë¬Irœƒ¯\ÉrØDC%“]eH]iêôÒCô¨¤ÝŸ9_‚$D\ƒLý0ˆdž’¤~¦ë“ŒcMÇÉsªëèQ²Ð9¶ÿ’Jqe; ™ï¡!†¿9ìüGn¯GU!¬ª¨èpë% e0J’7ªª™ü3ã°™Õ¦c¿$Máe‹aÉà-Áža%*ú³Œx´Æâ’¹“Y²ö'yÔf¾•´Ä§fJ³èeU²œ} (j¦‰«ú dÔ›S2k²5|+›ÈÈŸËã“•6ÔÜqqzkÓ\%†l÷ç<ÏY¬gMQrxf—UÁ¤Å.+IëGf"dYVÑO^ÒK€!ÝCŸÖAÉåÂJ›ŸŒ°,%“h¡7:-Ñ@wÛOW¦d!¶æâŸ[Ö+všI2ÊFŠ>€TÉŒ³õ¥EEÄ•Œ]˜TÜ_¥ÉÙälålϧ¯OºG-#F<@2Þ£¦Ä*Šd­mLUТšŸYBÇÒcÐs½'5‰ÊÐ2J/©÷_ÙñPÈY¥FI$Ûe&ÈåðÀB3I±9â^ui;-O@IÓa -ŒNí¨Ó'¦ªü]ëtþã@§A‹Ñ€7Ǩ6T¶l²Ô”äò&ºK¢$bpR¹¸!.G"·v¶“€Ò“s”l‰Yøþ'fÏ^D_ý÷_(*Ëfüɼ•›ÿ¹¼²g;³¦Ï¡ª¾ŽÍ`ã¶–É´õKgòÓ/³‰©ÿú¨¯Åû? ÖÞáa&Mzœ/×Uý·¼oÊ„›uüEÔ­­µµ¶ÿs 3æç­;®bìIctÐ@8ˆòä{Ó¸ùˆaœýàsD+ærôñ£˜ðÞâù3ßÿö§½ð ë—ÌåÒ§ŸdAE( ÑÖpßóÏóõ²jfÿ5…k{ŽÝ¡$p òÒ“OñÆŸóÿ¿MWpã"îžø[üÿžäÿøó}ß«x£ÿý}«Y5‹cï{¯Wê+‡º•2ú¦Ûø|E-SÿšQ?ÃŽÀÝøáÛÉŒÿè'íè^yëU.ÿcÇÝÏ÷=ÈÙ'1éë_ÿóëSµ’#ï{€×g®ûG²ë÷ßNfüG?7þ½vÎö»æn~\¾‰àÆ5Lxï FNx–e»ÿþG¢~œ0ž[¾ý£–h ÷=÷<_/Ûý¿jw/ý“³'>ËÂM[¸iÒ$^i!½L¼ç<Žy1A ÄõIòñ?Ù*þĨÁq@ÿþ•”rþÝ/âmæ™= ?eøÐ#x}î®ÿÒ·zö+~ùá.?¸ÝËXÚºòqs+¨hm­íÿè4„xññ—øèûéì?àú÷ïG¿}ö¢¼´@»n1b¢A:$þ›÷¯K‹NÖ|ˆÀêpжÀ’r½jÉLùúg¬.V¯šÏóßO¥&’äRˆ‡ùøÇïùzÕ†ÿoÓõãïßðòìMäYÿÖóJLÅ Eûû×?á­¿Vý·ôÍh²c-ÈÇd1é^1`ˆ©ØD›S›Ý†#/å¿ìqð1ñ/™QŸ¯‘Z^üê ~Ý]OŸþ{ѯs9­k¸þ±qzû“ìñýçÜDŠÑ„¥°s–9ø»·™ó~äñoçdç'ßý’éuMÏ—M¡¦Ý!,zñ ^|ô¾¼â4ò•þk‹juça·Ûš½/o©Þ„wÞgσ7nÛê9Œ÷§ÿÈ<»Ú”s91¨Ä墸¤¸EÏ™ÌvÀŽ˜4•`°1aùÏR«þdà€cXïìÇS/¿Éø±ðÎ#Wóå‚Ü`RÅŒÿ¦¨" ã©÷`mÓ™£Ž=Šbsš Œ«D¼¾Æ?7®[Úµ[²¾¯rëVÖ­ßÀFOƒ)ônm\³šóãO2ȃžZvWVg¾,âcWuE+*imÿèfúGŽÊ`Àd„}Ͼ–—_~ MjŹëíwQíùšD4¤dIü7!yøñ£·Y±#ÌQgžÃ~íót?ÓÆYÌþ ÉïPNײRliç‹Ùsé½×PFtÔÛ•Oy›R,%¥¯åmÛàr: Wn㛿æS,ôìw ô*k´â•›xoêlŒîöœsÜáÏÔTîÄ:u(äû~¢°Û>Ò¿;„½ü2m;¼qºwîÆÁúcV’ÇãË›8îøs(2)€°kãÌ])q…ùâûŸ0¸;sâ‘5@ùý×ìŒÙuäŠ]Ú{N8òŽdÀe‚º]«xèõWØÚa8}-ÇÓ¾c:¶+"Ìøý6Tùèб+ƒÚ‡I!P]ÅŽÊ::õîÕ¨@<Ìú5›q·iO›(`·8èTì¢f˾™±–Nÿnm(辯\[޳}¡Ð£‘F?g°†i ײ×Þ(qk†E¼b#ŸÍX@@±3äÐaôhçÊTŠk³ÃPÀK£†'Ÿ «ÁÂÙGË}cšæý÷oÞbøý/qsÏ~¼yɱ‰uŠòÛOSØPeØQ#èÕ6¼•ÌßPÅ^½úaw4Íÿ¦U«ˆ;Šèѹ-eêOSX_eØ‘#èÕN£;AÁj4¦­,œþ‹¶WÓ¦c/N8lÿÆßýÕUTÔèÞ½#KgNcÉözr½:h4¯F¼¬Y_Aß¾=©Ù²Œ¯g®dÿ‡°_¯râ•›x÷÷Y”uÚ—£ï•ò½MËæñÛâµtîu#õm„»6®Õè¥HøæÛˆ˜K}ô, ÔWmãé·Þä³ÚÎÔÖL·òtéЦñµ‹Ù¡ðÒè#5À·~Ë7ÔѦ¼33/¥Oß~”ã251W,båªe¬Ù¼£³„C†—„%ë¶­fÊìˆÕÉ€ЫC)(`2%.7-gm­…Cè•2Î/gÍ¥wÿ¡ŒèèD¢¾ÿö#n{ëgpwbTï¶tëÕKãõ¸—¯¾û™=+'Ž<–âü$à©â›iQSèÕ­?ƒöíNÔ[à ïNæñÙöëVHçvèÛ½< ¨»6­ÅäêD©#Ì×S#f)eôÈÃ1+°qË:3ýûviz$`ÓÖDqp`ŸNt+.¤Oa;lö–Ò&‹0'xÆÂ=“ßEuh´"KWoeß}úã_¿€÷~œEÏAC>pï”wÌýã[þ˜½–ÃŽåȃû5û͇o¹‚m¦}‘_ß`m)·¿ü3ångÎçJúàÃwál§ñõöÍTÕšé»w{¦}ý+*…#O>ÞÅúªíÛ wqÖ=Oa(,¤¤ìH~ýëSz¸“–mà Œ¾Šû^x‹¯ÝÈ}ÿÀQ§ÞÃ×=„ÃØpß_\rñ•|:mf#Dã`ï{n£+#êÛÎ…§ŸÌû?ÎÃj‚°« ï}ùgëŒoÇ ºï3„>C¯fÖ¯Ïcê—Ê€CNçëßæ‡ÎkE&­­tþïj Š€ªçÈU=œÜ¹»O¼–Õ?Û%¡Œj*}ÄÁ|»5F¹[åÖÛâ“y³9mÿN¯Ú{Ô©¼~Œ‚Ýiå£÷ÆlKžÎ?-ØÄБ—§(Qt»Û¨ w-™Æ¿L¨ ƒ¯Óò:~¹ç\ìÀöE¿1òá—È/*Á³{'¯ÍYÌ×w_G¦ýñ 7~<ƒÎåùÌ_¾»nÇà>…œ}÷=̪Réhˆ²ñqïñtÍkòb¨»—³´&ʽƒ4ˆKÞ˜<‰W+lts »««ÙUâÌ £)Ù³O¯£:àåùKøþ¡[)µÁsMâÑùyÔL¾›ßgÿÆ"¥ŒN‘Œ}ên¼è*®kãâ†ÇñéºjzÛ ìÆÉëíÇ3¸“‹Sdì;SøöÃwè›g‚à6N¾ýnŽ;ï;í` å…Vž~û)j+wRS[ÃæO¾ã³{àÄ]¨^úýï“î}™«[eñ¸J©ÛÔrøeçSé:ˆß;@j~É ߢCYG\JˆËß~Ÿ—n¾—‹‡öMY’ßæÏÁ‘ß…Ã;4¡“ɈßïO¹ïˆÑrã_3yî÷©<{ɱä‡ë¸tÜü´9D¹[åºOdês9$?À©7]͘ËæÙ3ÓÀÓ–zõÜýЫôèèå²;nãÇ-!ÚçiÏ}ûÄCŒèÕ%A›Ò@£¾*®ÿï,­âÐ.¥,Ýö>~7œoï¹–b—‘ 3~çÌw>£M‡vøê}ÔújÙúöç|ôÐxN9 ‘Ý+8õÑIwƪckm ¾_grçá0}ÞtæÕ¨Üó!7^z 6€¯>}™ ßÿ•}J‹Yòñ'œqÚÕ¼rþH@åÉ“x£ÊE7{œ»«ØRWÍÐÅ›˜rÇy,Y2ƒÙ;ŒtmâÂçžáìcÆòÈÙÇ6ÎßÔysptah¹>ÿþc¾©Vi'8õ±%ŒšAW4X°sî7´¿á)®¸öÞ4ЙàÛ£5¾ U¯ã©y+ißµ“¿{›guâÓGîÅR¿n¿“µ~m¨ç–ï¦1ý±qô/sS·uçšõ⦠æEÉ[ÇŸÏÜBõ²…|·ºšÎ½Úpó«/°Ïþ£øà†ÓÓ„A”7ß~ž—¶¨tµ)Ô„¬Û±Så—Çod×Ê©6þ[~ü.GöÐ ­_¿~Sß^Âì·žçó;¢˜,¼ôÀ=„-Ö–‰KIV~NéÒ]'^ËšŸ%Z±ˆû£ß>ƒð®]ÂöHˆ˜ ¿÷;·= &^,·Nú™ž}ʹýÎ[¸ùéo˜xà 9?YÚ¿?üü¯½„=½ 9ñ6Îxà-ŽêéÎùÜŽ¿&ÓyøuÜ;e÷Ý…_&ÝÏY¾eÐ>…,Y½ƒP$N`î’™T”éÕÞw䩼XÒ“¿¾xŽ×§|E,-$(®¨¬_³‚ HÌщwÞýÚù¯pý³óæÔ‹¹fD¨ÞÂàÁ‡±²ÊÁø×?`dÿŽ8„ÏÇâ€COg#}øùÚ3[QIkûg7Ç#ÿ-ÊæÍ›%þ|­FNíag»ÎrܱÇȨQÇË©ç=.AñÈ@.Ç_!""áÍ¿ W=9KDD&œ1P ƒøoº`o·Ðå ü‹=mœ)ýO;[~Þàküí‹'IÙÉ—ÊzoÒDêeìÙgÊ%ïý(""o¾xŸpÄER›¸ì÷D í”CO'Õþ,ùñ]qŽ-¿l‹ˆÈÓnÓØ»Å+"‘hD®¾ñ<9áñC‹x6þ.>Lûu}‚‚ ÇDDdþçoK×Ï–Uõ‰9ñ­“½O÷T¹êó";7H‡Cɰ'?“xÚZ†ÓøVUã²vÚgÒãô±òÝÊJñ…#"“Ç]*…—ÜßøÜˆóGËá¾'"" Þ{Q8t¤,­KÈ‚@@bªˆ‹Ê;“—çÜ!ë=! ëʈŒðáØÓäO“H$.³ÿøD|¨Ü÷Ó2‘X­ô9DƼümâþ:9úœãe@âÛÿN»þŒý¯Æ€)ò1´}–$`©<óù\ ëäÐRDÙçtÙòÁCÈÇË5ûòîS¬²¼&–ó›áµ?5¾Ñ¾lQ_7ýú’òèEDä›{®@òºí+k*½²kú+Èe¯ÎÌýžnYR™ú»gë1‚ßSVÔ$~¬+€\:qºˆˆ¼xÙáÈ{‹êŸ›rç©{KDDB«>@ÎyðƒÆëU_=,€Üñó†Æßî=P9´g6òãš´¶Öö?Ùþgq™ˆÇã‘löº¢Ѩ`²»‰„ýD#±F¿§Õ6kjÌ’Ù®m±N_8 ¬Üzá5œvîõÌ­ðÀæuxþż”?æÍAÍëÌaÝ’¶ÍUU1`LÙ&5  ˆhcûõêƒ-¼Ž#ï™È”…q8í(ÀžÕKØ ÌÑ]\÷Ö»Œût&‘xˆµ;Öj(jÛ…·n½œN¥nÀÞ¶ M\ñÔƒ¼óë,Œv;fSê’>k!=÷DQÒo¾@=÷ÉËçƒÙl¢Ì•GQ›.Lw Å+]JKèX`kô¼™L&&#*`6™q™ &+.›“lîr†µÏgüyîÇ©`²a³·Ãm&3MS¢`3™CÔx˜ ¿‰§¯¹ãú•cr–rHIÁÄ·F6³÷[mv”˜—3îÇ»;Ü,~û9zik½aÁB¶Ç\<{þ¹h> œ6öX%_-lŠ©ïYÁ¢ªcØRÇ:F‹µÞÃÂÕ;hÛ©€ ï½Ç “ßgi\¡zÍZ¸zÌ(ê7Ìaʦ0ãùßæqÚIg`~_°Ž¶ x²á9ÕÀžÍÛÀ`1 ÆÍ~7s!p:§ïUÀÿkD(¶¶ÿ« ›ó²KXr” ‚(AD1aB1gÑkÎ9ç„9çœ^sÀ„* ‚’Ãî²9Nêóþ1³³3=Ý=ƒéå®õ|Ÿïegzº«O:õÔ©sN• Ù‘Ë&oÅ´oÃÉh—Avn)œq&JrÈ(êÁ9{O¤qÍÖ…›õµpå‘'³ÏÐî¸3 é-!vÚýΙ¸oÝz”‘ñ}Í¿WF1‹~ùœ Ÿ|Žgæ®_+_/ 'oÕ7×ÑuøDÞ<ûP23½ /êFwPÛÄår“•å¼ägd³MnT-àÇJ“Ç´Ë9Í“CǸ<äg¦“æv ]ð‡w†mEOO%o¼û&ÍZHVQ)- aô„[Šo™tËýÌZ²ž¬Ì¬6ç Jøîû÷{ù5 ™|:Ÿž{@Â^È'ßÍÂÈë·J¹è”•…[A~^Ùi^h)ç»ut/öpùÓÏpáÓ/Ñärñãò5@Ÿaƒ)ö¶0ùªkxqæÏ¤gfâVáØÜ‚ìt›o–‹Â}òú'_nÞ½ù Ž9ì žýl>àãçÕµ¶÷oXò [lû}!§<€3Þ`mKò˜]—ËùßðxohÚdñÕü¹ ,ɡˈ.U9W»XUY ¤á2í<…ü „€ î~!…ß³ÏE:à·»L{é ²GÆÃóÛ#D2~›þuX—½‚]wÞ‘m·ÏΧÜÛYÞ³zÕ›³Ø£‡›¯æ¯à¼{žg÷éÚ ¦ÑáÑaIçêe>î1…7_Ƈ}Â/^ Æï)µñwx24ê -'ÊæFZ«W²åîÇsÙUWQ°Qy"Â+_ýLïmÆ’óiŽ;1|4b,×MšòÐVágÄÎòÙÕg Ë¿c³Næ û_OÆÁ—£¡÷†õT¬*gÊ^“9zëð¶pÐ233ã^«pÀ(Þ½y*C2š8úŠ‹{þMü^cŒ›Wðãòz&Ý&ÞðŠÑÞòŸÌÌ,Ü‘F6úÊÝn°%ʽÂFÚÚ£*ñä•ëoâ€îéœyí•ô:ñæ­Øel.— ï~dçâu+ÄhÛR6ð¦eѹ<ø›h ¤·‘Ó<•æM£¹±š×ç-$«¸[¶Oìu ºääÓ¥ «½ßó³ÉÎN§®¡.úÙ¼o¿£Öۉ݆•ØïEÆ`íòUddãr§lU„Ò *ÖVà[½‚]†p÷á»ay›c|¯þð#5 gñCmgï10ð·z0¼ë*ð­^É€Ò!Ü}Ønx` ,KW„™·4ܵ(® …Khnll“(i^Íþöj ¹é9*H˜î42TD†þ ƒ‚Ìö…˜/ÂUFWzÁú üëVSÓäåœbBïðVh ¤8¿½=u¾V ¯;á\*qÍýö;jäÀgˆ•tÛ{{Ò2áÅgî¢ï±gñÆÏ+h †ð¸½dºÂ÷ß÷ ãyç¬CYüíŒ9ñXÎ{áÃè8ËÎÌbÑï¿òí²µôïÝÏrä¾úÍ|zo=&nܶ‚  ð…?6ÓèJ#M¨_ù;Û Þ–›÷‹…[Nࣛ/§[ó¿ðLö¸þaÚ*~ùü!Š¿“ùPd¸cÙ›ÒŒLB‘½à#vˆ«r1Ÿýø¯}úe[ŒcÿapH›ô°½,îֹݴ‘„¬õࡵqå]·â¬3/exW›ØÌ–rÆÝ™šÎ{ñêS7sß´Oyô¸=™÷ÁÓÜ?sùF·5ò^ Úb-kk" aoJvÚ<œÅ~ßý bÙäþˆÁk®‡#ÅýnÖâ_A…µÆ —6;æ’[9å”9õĹüÇøèÓ/¸pïöXØÆ_§óÛªðØ~祧iÔ÷4þðtØó@°µ>åëC¾ð„’– ?þ;í©?üìPõ¯|³¾•³¦ŒŒû¼ozÕkø½¼‘!‘Ø¥P?Ô4±SZQ´KFïz?îz>w7»=ø8oï1™½õ¥¢ªšÃ·Þ•›÷na>F(„ß„ôön¶ÙhfÜ3š_ç}ȧ\Ë­ï çÁ£w`Ùß³Vå²ÏV}L^bE(@" …a„ðùCq÷ŽŸ¨Ú'É@ÀÇ“÷}qßÍxò–»¸}Él}ÂY\üÚ0Þ;ç\Â4ÈÎ ßwÝìïø©²ž½¢žh…ˆAs¤@YRÀ¶O››)(ìÅ—žÆ‰7\Ï€³\Ì»ëb²”•e³²f?,+gXQx­\º’ µÍô-ëÚN<¾CŸ¡cèdò‚ƒÙÙñIGë¾›ÞžÍqgŽ;7‹&UG¨¥7÷_x®…”ò9{÷8õÍgØu†ŸÑÛMdT‘ áÊj!ØZÄ}$þ®¥% Œé/ê’É›sãËáÌ›³î]·‰LÄ!üAƒœ¬ö˜¶ek–SíͧÄ.„@4 M¡” Æ3ÂDn@V&5*9úг_f^§úq)€?ª/føC~|®Älh;9Û!';pøÓopñéwqã~ÃaÍ,üh*®ŒÉqg²×ÁDz×AGðÐ]7qÊ£pØÙº0“uÊ9xÒá™×ÊNS/ 8í6®<&fÜ.â›u-œuTü¸ øƒBAN¤ïÓŠÉi®¡6o"w^p”e[GŒšÈÌQùaƳlsùÜ·åx®Þ{¾`ËEaZœb…Ù["´Ä×òÍú ºnö´Û–CG¼Ìé߃¿ªŠÿœ|ñß¹eöäÅdt£#ÌFä„ÙÞ]¯½L×n·äÇ™·f,y"úÙ‡ìÄ O§w~züÀVv”8†Ž+7¢©bîU2Œ à¡‹y]!–Mõ1±Û¡PxÇ!òg†‚¹³Š~½ðÑ©ÜøÖ\`ðÞ;ÂEÏÌÞŒýl~rÄP­eËQ{±|óC˜vÖ9þÆ>ˆyÓ.׬DC{:ÿ÷ Ô¶BUUÕ7#ò]ĨD¶ìêªÃ«Ó+î¼Yóƒö9ƒ™ó~aÁWÓ¹ñágYçKýé‹~øž€§¶ˆ7Ã}GoEïƒcn»‘¯­fYåj®»ñfæ]8j/¿ó"7Lû˜U5ÕTHKÇ¥rsÝž[qË]WsÇ{3ù½²‚w¦¿Å;ßÍ{PZ[¨¬o ³tÿå³Ï¸ö¾§YV^…'à'èv“›_ýþÍo¿¥¤ïzeÅË®©±žªÆvhsÛ½#[ÚFÀGu}=þPÛ÷Ô54qN’™™É;_|Ìû?üBMsê_gqÍ3oUF€ ¸é”[!–T¬äÿ~Äßz™I7>Lc³ÂcD¶T­l¨«¥9Šö_]}u-ᑠʺZš#Û}MÍM,Zµš‘ÛŒçƒ[/gÉw¯0úìÛXӠǨ˜< Ón»Ž™ —³hálv¹ý¼ǰïÈH¢˜o-3—V3iÌ6&• R!fÍùŽO>û‚éÓßaêCw0à‚ÛØrü!Ü´ÿxŽ?ð@šzínz‚—­ã·ù³xìÍ÷©ŒèÏž;îHZÅÏÌž»–vÛ=êÉ:õƒiž÷ZÌï¾å±7?  ÈôBMm-õ a}>zïIT|óg<û>Kׯç±gnãÁYë9~¯pxPyqê9ýÞ{ø~ñ ¾›ùç>÷OÚ‹\ÀßÔDMm-M¾@´Ïëêë©mnWòæ–FªÃu17Ûq"{÷ö²ë¹§óêìùü¾~/¼ò³–Öššê©jj¯¡ðµRU[O ²-žïÉbùŸó—?²¾¾!NÎ{•0vÃ÷k÷Ò¶é›Ï !GyùmÍRf÷ 'ßõ• Aæ­ZŽ ñò‹s÷ûß°º¶ÖWzá¼¹ ÕÕå,¯w³ãäÃxòȸîг¹òÍ™1ãvO!û›ÆmVv&ë*Öpß믳dõŒôB.˜rs_„£{•…ë*øaÖ§¼ðn8ÓùÓ/§sù¯°bC-~——ÎaRœëMc岸óƒoXSÛâçÊóOeÄy·EjTz)Î+béÂϹâ¿ï³dÍJn»íff5dsüöÛµÑoN™0Ž… 籡 §m»…ƒ5jàÔq[0rç3i²¹¢±¾¨‰XƒšXûh„íccLÍŸúê¨ Û˃.¸šîøè^¼oÍøš ~âÛncö2ëÂôþH¹¸;®º‰… áÓ7Ÿ ÛÄóé³íÑœ0ªŸÜ{)› Þ’—æUX/èý ‘Ý–Èxo¨ JÛ0´·Ñ¹h°ÛpcÐÄM7ÞÏ£>ÃòZÜï›ü1nG R Ôl¨`ûÃGÑ0ç5޽|*×\uCN¼œœ¬4Œuë©2ÃÉÛoÆ-GáœëgÁ/ ˜öèmŒ>„»¿,BL;ßë3¦¿ÄÁÇͽnj㧗¯à?WNÓ¬DC{:ÿ÷àfà€n÷.³ø.“A›u£¨OxbQiùtëÖî½Â^Œá“Ïà“|uÔìõå‹d¹do}$§TÊOýÛïé9hŠL;<]‡ðöµ—qì=søµW‘“aPÛêâá‹®d|Ÿ°ç3Xµ–Ûßx‡×¿,¢ºÑÏÔó.cÏ>aÕ…ç^‰xïáî'âÅ÷J(_UÎiÇœÊ$ ¨°”Íûô&ÝÕ3—a4ñÆGoðÚ¯?ÒT]Ç1‡Ê¥{ŒŽÒZÞœWΨƒHX‡t.ëÅðPiÔ§PXP¾wd»Õ“]ÈàÞ}ÈO¯Y:—tg@ooxGݧö¾¿óAºî¾âFì•Á'_}ÈË?ÿB°©–»έí@Ÿív墽òÈ‹SXXÆÅGÈg3g¡"•ܹ ïײœˆÄå¡gŸ>v*÷_Nøûn9a7GQag¶èխЧÿxfßv)»Ýô,·¿½5wý,.Øk{ÊçFä¼Õ€ýëÚ½7ÃB¢÷óää1¤w/¼ tÌÓSöáœéÿeÊœNì;fç”vâ¥×Ÿç¸-ûàm^Ïõo~ɳoeQç‡'/¿„AÙaâЫo?ê2Ãm:æÌ«YÓlpÇóÏ1~ÈPvPÈëßΦç m(6ÛâÍÇsËs¸úÝ'X¶b-o_sÛïõf´§¾ö‡Íù_]Û ›Äá{#«µš—^{‹é?~A]]3—œ~'Ž÷õÄ]öáøyK¸ôŽ©8ñzøÒUˆ GÃÝqVò뢟˜³¾ŽC)Mø}z~)ݺu£oAØ—õ¡[·aDBÄqgä…ÛÛ3×ÑF—ôJ·n]¸ÿêsÀ“Ï»N¦wA®ôÜð|ëuue°Y·nôè~È÷ý—ŸWïË“S/r¸áý¯>ÿUN½s>mT÷Á?ÇsÄdîºâx¹)—@k½·Þ‹1úòëôG™¶8ÀUOÏ`ÇîáëOò>^:Š—n»ŠÓŽÛ‹­{åhv¢Ñ1Ñ1³×Ù±Îßµo×ú›¥bÍ:©®iب§úë–È€='ɵ-´¿(Ø,K]$ Y$õ͉YžÁÖ&Y¿n½44Y˪±zƒ¬[W!--Áä¯òKEùz©¬Žß¿þ¯dïy°|³Ö¿Q’KYÚf©¨¨”f0zEUe¹¬«¬±ü}Uùzilm¿6d¤Ö#y÷ŠH@jšâ.)/_'kÖUš…%—_u² 9û.Ûç´ƒÒœL}Ͳ~Ý:ÙP׸q‚´ùañ®¡¦:Yµjµ44úâ>ŸûÆóÒsß#åçò€øªdmEur9þÕŽšòr)¯¬‘`hcõ% ÖWHsK0"ç“ålÝÙíW·Toʺ¦h 6”WJK0ü} ±^Ö­['­þä/ÔÒÔ,µÍ>ñ×.‘{N’ë>ZdŸy¹¡B6Ô7Jœ¹hª—uk×KmC‹©[¥|Ý:©©³®{Qµ¾\›}ÍóË…ýGF\õ”ˆˆT¬['M6bÅ÷Ó%kâòÊ¢ª¤b{÷ÖÄÌA²Êg?ˆŒµݰ¾\*+«SêÓ «WÊœù d½É.ù×ý ]A.g¡ƒvjáÐ^ëûøý>iõµÆ½Žåï #á½*Ö¬“ú†ö …~VW±^ýºT6ÔµÛŸºÊJ „¬ÛðÕJ}ë?5Gjèìõ>{ÝÓqé´Jù;eäͤ¤[æF?µ¹²œGåðQƒ±™ôhÿ½;=‹Îî‹ìÂb²S}e——’ÒÎ WWù9aÛ]غ«w£$—²´=™””dÆ]QÔ©Ôö÷EqmTÑ$%õGz9±1äÄ«zi©Õñw2=¥œ½ÏÛ纕 w*§¡¤eÒ¹ËÆëÝï,»6+îY‰u }Í ¬\þ;kšý +-¢kN*rS)é@AiéÔÅÛè|dº;;ÊÙº³Û¯Î(,&#¦Å¥ÚŸ”K—ìÜ”™‘•IP·¶œ £Çrب¶Ï-¶H,ËÊ¥K–…W--Ò.öG,u.µm^uU9?‡Â5K¬î¨çä›nÇÛ{'T”DhÕLÿz=Ý|#eiöƒHm¬}´hqçÒ”»²¸¬ÅQ¿þCv?…svä ÊN-Úk}¯7-éýÛbþ´¤[|ß´eÖÇ"¯¤3y&µÉëÔÉ~”¤å“«}aª¾¾^rsÿ~5ƒ¬Y³†²²2<žÌuå²6 ¿U+ã¿3qüÎtÍñhüO[á›YŸ±\J8lÌ0ëKµ<ÿÎ'ô1ŽízwNr??ÁP÷ÿF747´•›©õQCãÀ?ÍË4éÔÐÐÐÐÐÐÐФóï'.-v ¿štjhhhhhhhhüíèûÜÈ֔´‚é ?…?r/•äzó÷mÇ~þgßÁêæ=ÔFü6•÷·jKlÈlä=“]g%ãdߥ"3•¤½æïÕF\ë¤+´}V}êtŸTõÆI¾Õ¸±jÓÆ¼@‡ÿèû8ýncíâR}dש$:ôWõ-ÐÞ°‘ý¯’Œ³ÿ¯÷HÕfü™ùÊêþ˜Æ°¿ÿ¨íÿ3óúóEªv8Õù%•÷UsŸ¤(#·Rx]›¦O±Ã‘NC„…UÕ„BîØƒu•‘?1,-ҩŤ* ÖKLÏ/—¨@‰j¦”Šžmwÿ¶Ï¢ïã@3¢_Å´=ö7–÷`#ÔãLªŠ‘AÜwmÙ¢òW-lÚ°1: p¾Öö^)PÄ”XƒI_"Çõu\ÚÐ,ù·>Ú/oØ8Óš²lqhƒõt¯7tj·y—„v«vuIY‡ÂR‹yŽÅ}£}²ljãþ, -Ú˜˜'eOcmÔŸZ%$¼{L'Ó—¨œÂ×ÇÛJ»qlõ'éF¤Í*"#ë1£G‘ÏÅQçmìd‚þamObÞ=ñ‰ñ¿3ë@ô=þ4³îgDÖJªÓ©Qé¶w°ú6a Æ5+Å¥ZB{Uü±°íÆÁÆîbqÕò†$. ›ñ©"ÿ+ Ûû!¸].†v*Æ­6½¬æI:E ŠÒ<¤»Ýå¶ŽR&%kSžèä,:¢b~Ûv“¡h3Ö¢Ú¨mˆHœ1iÿw1hÓ§¢ÄÙ XÙbÞil+Õ~ïØk-‡‰… mÆ]xŒ›&ÖèD¦â&1dXY N{.§ˆé‹˜ëcfÜ5bÉUä;‘$<›xÙ%<Ãîzû›´ÿPL“¹DK,銶_Ũɶoc¦!žä(‹6YMüv²Ži§X´ßB°QÞ+ÄNÖ¦kãÖA*F”ío·X‰‡ òÄ{[vƒ©í1S´"úÔþ·XŽ/;>ÌcOˆ')V°2Oo&…ŠíGs›ì‚R1Sq Ó¢SÅë_ìXhÿ%&›¢âþ­bI’"ÎîÅ,bÚ,1í°~G£Ù*ò>bnCŒþÄ’(e±öW¨ Šsmö8ú¿&ÝS_´ë|ãûDE-DçâÇ¥$ö¿ÄÎf#/&Bc¢Yñ:j²"nb17™Ÿ¡œæe¿x3ÙkEdeu?1é¦R ´8ªSq2%¦ŸÄFÖ6sŸí˜‹×]³}Œ·Ëæ~K¶ ´™‹æ#;Ò™â‚32¸Dð‹P DçM:ÿ¸”­*¦Þc8|Õ¢”IÓâLŒ×ÀeåÑT¦AêŠ7ªÑ5™i²‹Õ#Wì—&#ìŠC‰5¶Êbgc(bßE9 !|æ°•Ž»bWæ®8‚Gè”X¼§Jñ“V›ˆ$ÞÛ©Ì+; ¯¨¹Q9«D(Óú8V¾Ñ¨6v”$ÒÙ|¿˜w }ŠÕ‘y¶µÅežßaçEW¦kÛ xÌ»®x™Z9KTLEÉSÌ„› £1zw1Ì2^–qº¯bô7FÑã¨cEÚ>1Å~§L2±ðÅ}fÒWLDż;áŠiD›žª„ Ú'—²X±vCÅ{¡Úd ì¼>®ÄvºbmQ¬œT’™Ø>p™¦÷ÞÛ<Îc½“¦û+ea;ÅÔveI€¢2#‰W„X{d"ZqöÂe%K“n(e½B¶ƒø~‰.þ-üjf9ˆI%ÆÍ§L6&n.Á‚p+;OB¼ÇN)›Å·É>˜Ç -³ŒÕ­ðøU¢â}¤*É‚ ežŒãÇS‚þšeÅ7íà¨Xnö}šßWpvÛ«ÄÁX“²˜Ã•ÍïÅ¡_ãô¬]qÛßoOc¹‡Ä•8ïI¢mt)p‡$åZµštþexQ‘{›1µñ$¬@ã—ZI½‹ñŠyr7{SÄÁù%–+!‰m³yE&6žz ÏY¢[Þf2ŽñmJض'Á«ç´ðˆ´_'ñƒÕ< Í»gæ‘mâ} Ñ„÷³ñ˜Únƒ'y/+lÔ»k±8ˆÊÑäµ°ôØI¼nÙõ9&ÅU$nÝÙmaÚl Š%Ù뾉%#1Û;qºoXÙva2ÒVcÐr7JYx€Ì}oá-´"“Êb£Î6€Nì·ÌcÄFª‰6Dâ¹Q¬×]ÙØ;O®Ý»·y•D»+§ëqµX/R­tÈr+5vâ7éŽrØ.Lè[I$b³q ³Vò°±v^¬„þˆŒu'}³j—Ý¿íÆ’8siK[¢¬<ÓÉôÅôFxþ´•·R‰m¶sÒ!Ž–cBìÆ°$. ãæT±×Ë]3‰—_Ô¯¸ãb½Kÿü¸]ej³˜Ç†´¯«­ú(™N›í©€¡Td§ØT †w¼ìu¥¢vUE=@ª}P˜ •Ýbµmõ{»­fI2°ã,¾U’ï²3Jb{gn§yàš·ql3U”}X‹Õf¹…aá11s?óý툶•A·ìSeû»Ð0ËÍLˆlw=”½ñ°pæ(KBü½Tl?DI“Y§Ä¢=boe%gI>.l~› Óvx«~LèCe=ö”C¬©SK¬Ç.v\8r±è÷ïm*Û^1 ‚ƒç¶½ÄNÞÊAvqÄGllU¼Žˆ$!5 ^q›¾Uv¤ÍÆ^& ë€=±Ùɱj£y·*v°?¨„ÏT¬Nð,J í‹Ý™±[0ªxà$/³=W*áþJ)kûheÿ-õLÙs=SVúcò\ŠƒU9Í6ÌÏŽé7¥ì ¬8ô$Îï¢âíl”ĉŠ'œV2°´K6cÍÊÖXŽ?IôJK¢üÚ§Zá¯ÏФÓÞÎÇM¢bËiÚù¨$éUV[=ñÏqžÀ”µ7Ài@&l9R‹AÀ”Åõ* ™°sÇšWŽNñÔN«r‰P ±6”b1U’IMl<‰åPŽÍnRJe•.ñ\'qÚmÇH¼—Y"ÿY*Çœe­Ã$!Î Þ!±»ËìÈ©8“3kr¤‰Šå;+ëX¬×HÙxÀÿˆmvêߨÝk»Å‰ÄŒ‰Ù™H¥ô€²y“z²œÀ¸Ð•([«í!§‰³'Iµ/Jlvw­vImªX/¬vœÂ㔉áµ9_Eì=¢ÊÁNØíÅ.ÍËŒ¸{*k§¬lb|(Œm ¥Õ{(›Ý61µ#©-‹Ý2Œìû‰…“ÁJgbʦßÄba#‰±©IÛé4fT*ºÖ¶ 3í–H¼üŶ!â¼HÆf‘ ÎãÆ’·hOç?ëì4C1ÇÀáà½Ä¡ÃûÀ\eá!T$S‘‚—Ðl0•5 ´$ÊÊšhˆ8+ïe'ÁÞë!DÓaˆ—…'¡(·pOtëÒdT{²ŠíªSá”­,ô)Á“báÙ2{›R­‡£lúÂbQdIDÄ¡ÄA~Ž^ ±3²‘dɼhˆ›0-%Ì[Évì¼!ÉHºÝÌ&ö ‰è›)õ<%þ'Ħ@|ã.Ê9®Û4Á…¹“\i:‡<ÖccµÀ±\ÈFH…ˆu2›Óù/¥ kBó>qI›V¤Õ|S1yNqˆ9vZÈÛ$uÚ:’.~”£°´mv6:úSeïµMеøÐÁæ:›ç;&¼Xí¦™r*$.ØÁ›dnýŽö]ï¨ó {ÝVIlžr²ÿÊbþ‡]Ip4ojèx1b*¬"&bѶH²É µ$ ž±A3e£§d c‰bìvŽˆ}‰Ý6¦“Ófål„ ¼Yy”f„h®mj ÑŠä¸ÉÉÏ…€¦¦P C„̬\Ò<ÐØÐÐnd,’Œ73—ÒÌvUk1TÕ6âv¹ÃÀpy(ÉÏ%=¦éåõµCàr%!B&ù‹] u!à 3¿€bwøÃ µ´Àã¹ؠyá°1^<;}“$"™7×*#?ÎëjõyLÂTÜ"I’¬Ä-’Å$Å»å*=’‰ª’ÅeÚxòÛN³Â:V*Õ> y‡ç:•² G±ôÂ9,2’ïp,å“`ŸÌá1º.vÛ‡ ‹‡A`[àÖÁ‹nKÚL¥§l+$ñtÙ@qî{s‰¯¸çH¢‡XÄa1iJØqZTÄ‘x3Q³×-Ç„±_ˆŠ$:j”¹Œ'ÕÎû+8‡_ÙÙMK#&«Øô³J´Û±á%ÉŠxªT<±Y¥àmn¿³iUµÂn§I9ÙHµ‰n¬wTÒWº¡=c=¡fš2†ƒcÔ¬’iì<1×…BŠÎEùx5Õ5(—Ë"<#I}/«,Ûd¥Ù‚YzšÚ³U3²³©]ö#oÍœƒß•ÉÈ]&²YI-þ ‚A]ÅZpgãIwƒaàu»i­«¡Á›–—œ`^mffçPóûlžxc¡´,òsó8| £‡÷£¥¹WzÙ&^}ôIU¶P\OqÉ &ì¼-é~Ac㌂mâJøŸ99Ù¬úî+^þùg\iØn÷]é–—F«?Úö¯ Ùu(gïQ³ÝîS‰[‚v¹åBBâKõĉ/cEˆ,ßO’ËÃlèÅÎ+k*Aâ4–¬<ÞN…Õ‚DLq^¸$]À9~;v\‹²!ÿmRV“ŽÝûšp¢áeX,ì—Š-CD’ •Y¶Ø®åàM6Û`•Œð‹)aäÕímˆ±(qî[»É_LãÆvŒÚl›ô\¬Ê39•-#É"˜Tæ‡$ºmáÕ%ÉÆV‰šÊô~–N‹¤C±!‘vîÙ¶…±²Ñ)!±Œœ]»œ4Žv›…`‚ŒTâˆXð§Ð?‹E¸Ä•ѤóŸwz&x?%13RádSÕɈ´ýÄ•F~F·žyªœì7i<ÒÚBÈpò´Õ‹³Xí¥jXÅ<¡˜F·ÅÊ<7=Ukâ„Nà…9+Ý3#X¿Ý·Ý…üI'ñú}WÒÚÔBqv—]p/|äáÃß¡kf3 ÍKc•îeÕÚ_¸ýÆ›ñý4´ø8ð´û˜8j¾–V\Þ òŒ ¼÷ÈM¼µ¸‰úúzHß™…«gÐ)3DM½±q'ó^ˆI^yiiTÎ}SO½ÄÌeû1¨‹„Ig/Kœ òÉÉI 7Àì‚|J#ÿådxÃÅè(O:¹î¾ðTνé~rÓÒðÆ \EvA>99ˆnƒ!•—G^n"‚!“O~N&†@^~û³<.0p‘WO†×…¸<GÚ˜—i£ƒ‡#ö½[²~#(HÁf=ºáÊî ù+Ö²`Ù<ÑiG±lù"–ÿþÀëo¹§eæ„唟O~n&†5uu”n}¿ÕÖ±bÁ¾v‚h®§<”Ç-3–RW[Ë1“¯I£½æ[[€9ùѾ(ÌËCâJôÛ&E>òÝÇ µÙPºuóàóûqžfM$.¢+¯7\vÓ–|ª„­`Àìü|ò²3B^AX^…yáXÓ¶ È”'¢ü|Jòó)ÊÏ'¯ ž“KvvžôlJ òÉNs;-|Ÿ¼ £M?…ì¼°¾t.È'»M?±ð´©ä¶ÕÖð%Tnû˜HÇà °ŽM–lç¡ #›šñm¹=hQ۷ݶ¿¥L”}|V”´Š=™±šˆÅÔ9â°-WàÃÁsm9Üì’/tÅ*-6JÙ{ܬBJbcbIRqbc¼TV jVdêã’²œ½ˆ‰I0m %±&ÃN1„*•÷çÊæìödµÉÅʧ,ͱX%¿&¼[’Dsb®X¯ÊyÇÄ”çÛ6…E­îؤ)«¤GK¼µQ õ@­~YŠ iGE¶çÇè_{зöt¦æY1{:ìVZbíip¹Ýø«Y¹&Hßþe¨åãOg£ÒsèѵÅ%YÄEYnåëVòéìyx2 ÙjÇíè\èe}u#ùùn ‹N]Ò)--!ÈÈÍ ¦©_0ĆU«P*“̼tD„4·P¹z !•F^~. Ö¯Yd3¨S>Ë–üÄÂåUdåÐw@?2]!V/[NAYoºeÃ73¿¤ÖƒÇŒ§{¾b}]3n—Ë>‹µˆù¡¤{W†eÂçÝ{Хؿ¥ÉL—‹b '''ì´ ²ìœ| —@¡´°ñ70㓯hhñ2|ëíèß¹€u55àö’îR¸ó²É2Oà&àiY éYáë‘!$nºæÒX»Ž¿ý‘†€—¡#F2´{1• u‚ ”³hÔ­@Ò>™ýŠè–M A{‡Å6«ùÙYø[›©kñ'¥›KôDо{½BùÊ•¸2‹éWZÀÒ…sùu] =ch÷ÖÕÖ2 ¸¨€Là§9ßSQÛBAçnå‘™ã¥qõZÜÝ(Ȭgæ×¿Ò{Ähz冸ü›YõèAÏ¢<š‚P–—ÕOwf!#côÓív9K'Ûe5~œ¼ 8{cìË„ÄxÖ̧Ð$ÛŠUÊ>ãxcª‰XÅ\'‹³J¶5g®ìl?G¯«8“(I²õçäµ[ˆ…‡Ë®<ζÇÚ£©,Npq Üv}l%ïdc<ÉVføT#±µ 8% *2SXT‰ƒ3ű4‘²¯7ë*‚ÝÑ™f» ¿K^':ùB7.^Òjñ¢RäP1ÆÆA£°8”„øòIXxÞ¼ù¶:éœ$ ã”%EkÒùy8±/©ãä!‰`Åy¹|öÈõL¹ð9.¾ïz^¹æLf.iàèóŸä©[`æc÷pÈig±ÖH‡ žÛÉǯ«Îf ë{îÿï|»ì~67>çÒ_¦ó6GòýgÏÒ#/V?Ó è§×Ø#™ñÊÓô*ɤ²Ég}H‹¹hw²mó8¯$ßšÄfÚ’dIä8U, çÛ‘­ØãE9x9U ž9Eü÷NG$';â;Yl˜ÕD¤’x}H²•K4¬jöŠØre=¹'LôN®~‡äK¯ª¤V¯QY0)§$YØÉÌœ4Jl¨Xë“ÉÚ3›ê¶°<¦Ð®Ð|›GÎn ×îø^'ýÝ®wòÀÆÉOâ'‡Â!<ÆSŠÃób¼˜Î<‰½žŠ]»ìJÚ_±<@Rµ‘VcU9Ü£-q¬íe½þÏòÎÄj©¦‹˜ _ûßÀðûX_·–³:–ÍÆÍGÆ;í΂ßW°jæ+ìvÂYÞŸßÞxˆ sßcû=Žæ Ó0çµ+ùö½g¸ëñwh¬7l˜Å%ç.Dü>pç1tï#VìfåÊJÊ­ÁEøèNðP±z «X…;Òo@XðÞCœþ:œyËóŒöÎäˆs¤²)ܸ–p×I‡‚*å¾×ß${Õ—üçÌÛ¸áþ¼xå$êTkâîºYƒ-T4¸¹ú©¯ !Ô562ˆ´ŠÝŠ¥_Î÷<„'ÐLv†‡™?,Æ•?’ô\À·šÉ;îÊœú^¼ýÝr¶.YÏ£Ç0eçÃÿûLJs2ð¥âI¢ýD–Ø”››C㚯™°ÓQ¬OÌ«³ßftA#‡í»œ°#ù~焺±¶¶¡½Fœ¥ÁB¾*‚yÜýÅÄCEc†ÙP‰3!py=¸ZªyóÝ·É;ðÒw’¤`¬ç݃òÂ'OÝÄ'OÁÉ7?ʾýÓ9ð€)œ{ùM8ý råL·Ÿ¯¬áÌÛžbÊn[óòuÇsËËßÒhHžÁG>Éò=攃zòàÔÓx±÷DŽ?j7{ö]Ö¹ gþ{L<á,:ÙŸß^ÑÏSzó͛בáöã3²Ñ“ÕCbI²íJ•lÌZ‘DIâ•0'û©ø:½Nñ–¶ž=e]ÐYRÌ’–$U²$´T’?,¼Pɽo$¿çI8Û;É=•¤¼“DæfR‚šûYì „$Yè¤2ѧRº,…ëÄ©"„ÚÈvØ–¦³"÷I¼r ÌXx­“ÅÔÛUQ‘˜“¬ú1•31´]0ÚséÔA’\NuFSÑ{«NHÕiU‰Ââ8ÑøXë¶“7ͳ×;^L§ˆuM°„Ú…bÄoî´0:áDæ~ý»ì¸+ãzõ GnÜuϵ4“Ë£Ó^¡—Æì>…ƒ·+bîëO± .¼ýª«×³ëféôÜá ÖTTðÛŠe,øígF•eAS9™—™§ã™9™äfç´'>y<€‹ËŸ{›»/8œ¶O÷²d§)D¹ÉtæÕù¿qÚ¾ûpÌI§QT.YDˆcRã)‚AòJ:Ñ©¸` ÷mNº¢î×ï¹áÊK¹ášë¸ìò+ùqq-iÅzàûçïbNy€óïxIÛô¢KïÑ\xÊ¡Pý5Ÿ,\A–'Óa€©Ä3µãúÍEž^¹óַ½Ó?fÿ­úQÖwK>þàmŠ0õŽÛñá"ÃmAt ´A 袨sEéLïšà1 2ð¤çÑ5?Ÿ  O?Ê\éôèÓ€®ùùåg4ŒÄ˜F“ŽeedpÝ3ÓyðÂãÙmÿ£˜´¹‡uK7`ÜwŸ¯¬áâÇ¿åîóŽfä°¡¸Ç¶áõAPá?àቷ§qíÑÛpï‹ïòÀ'çöRêryä¥ý_ÄÜ·^à·zÈÎò:…Çáô¨WÐ’Ä‹hµµƒÉk£F ¬‹õÖ˜SM•Z¤©” ²%»’¼dZÔ ‚s‰£¸­=qÞâVÉ&d±÷Šcsô¤~D½s6å¤ìB¬ôÐJþv¶É¶}*±H*DÖv×MY´'¦ž¨éþ‰õE7R‡ì#Þve±}˜,éJœˆ˜Ä{á3“©&K3Ý\Y¢d„Ó¬/vÿÙów*·§Rر%•Ý’öq<ŽJ{:ÿ>Þ©L]­YÄ•$x:½hlàÜû®#X_S˳Be@¨‚åË–œwðH.ò¹qãcéÂj 55~2òÒÀ•ƒÛã•–F¶R¸rri I4?&V7 C€ ²Òõ-ÛP_¹ úìÆÙGL¢5ÐŒôÝYóö £8‡Æ•‹X€=.¸•ý‡äQ‚¬æººÁåqÛ/´&… hmjˆz?be³¦Ù`ðÁ'óã´iiôQ˜“ΉlÉ£¯­%øfþrž¿ñx¾|ÂCÐp±zÕbÖV7Äñ|ûí²XÏ@ŒQu…Uôë¥Ë€BvÚ•ª|ºtÆ„>ðÚ¯K¨ =n|¾Po¥Â…Asc}øöVÁ1 å+  /—•sÞäóîÆð¸ ©–µ†GO݇o»ä„8à”Û9ù -©ilJ «‰yLåªß)Úf.9jšA²¼.<%ePžN0í×!sKNc<üöá È(Æ¿¾‚¿/8ÿà‘\ìsãÁÇ’ˆ~Ö×úqçy»A™Ë+ÙÄNÚyJœ¶)ÍÅÙ-’Ö~q™Ù¤–UlµŸ¬Ÿ•Ån”T¼’‹Úˆ,g;’*‚µ:e,©'.Ñ(v^Ò™³ÌÕ¼1Ê:î-Ö©ìDДͳ’ÉÒª4bïYN¥Þh›WÉêüö„Å’Ý}búÁ6Lâ«M˜m¤cü®Š_h8él2]W6(IÍãkÞÉŠÙ‰°Ýa±óBÆVx±[¸([§¾ƒWØNç,<ç©îBXÞÓ!ŽÚÒk¬Œ6ý$ñ¯:¦óŸ§ž(加v%3,jw…$<ÉgGþ×P2r3q{ÃCS]#àún¡µ¦Š¦VƒÂ¢N””Ð=¯…*Ÿ›âô r)@ss ~Qx³ÓÃÉ1p{=( #Ó 4²jµŸ´®Ñ&Cæü Ê›Nf†`„|„€â.=ð‡  #ƒ\ƒ6+«dqbFÙ\n/@(+-LÖÜ®(E Ô7νóV†¸›XWßLNnù…EôêߪæF\Y9±òl' 뱈¹4I2\±%~¥lIüÌ& Ý.ЦÍP¸Ý2@(wA ”"³ÉOz¦—¦– ®@·Ç•”Äú…½ŠP@³?@–7oV~Ôû«¡x;òrÒaÑ/aŸæQáßwÍ ß«5€ÛSŽ‹• `àr{iªoˆê§¯v­BaaX?Ër[©ip¥æ‰IèT¶(áß1 Šd‰*f}Œ#¢eÀ’y l?·±±%±N^Eeñ‡‡ÐDÛz–Ùá  ØgS‹XÇ™Æb„¤õ@S=†Óòlh,j…š™¦˜œ†I çŠâà=µÓ㤅¿% éë< «R|É`vY$ÔS–äubãÆŽ$²y§…–¡³—Üb¼(‡ÅG, Vb½ÈqŠ—´h›å _Vñ§¦ŸØê(61¹’<&6ÖRp(‚ŸHx£%ÊlM’|·$•Å„&ÿ áLjL±r‹K4·mh”÷Q¾æfpyÉÏ+bÄ6[ðÁ kè>r¶.ˆúšªjü¡°æIÐǚ嵸€â¼pöº?ÌÚ´ú b^~áÑ̪±sçNí:ær/@°MI-M2 òq»Ü¸_CcÄkbák7F“[w ÒøZZÉÎÎ$ H°ÓÃáñð{ú³ç>}ã~^Y]M‹?HvVøf! 3¯€Òü|Ö×Õ2® ƒàÊ£{d’E0­…°ÛÈá<õú^úø'®Ýo º¤AÝOŸñþZØbçtj‚!#eñú䜰ᩫ­£xó½™ñåþᘔº_é^<œÃz›vïGh6S^[ßî=µ™¸\.EÐçÇé3#Œw~!Ì›Çl‘žÁ/Ÿ<Á9wO‡ìl²Ü.Tä÷ÊíFBaýp)! i 9j8/|¿”²­va›ÂDý „Üq'žŠˆýD“Œ„XMd¶‰’:1´,"žñÙÎíR•èÙ$ÏsJtFmöJ˜ktZé¤Ù ã4§dòĹÜPô3q®mº1ÛÌ©xE­*ÄM¦*±–¤SÂIl?Ù- 09ÌÈíÚn•inåaRÉj6Þó¨§K¬« ˆMl´“÷QÙt†8yÇÄÙónwX)è‹e”$Ÿ®•ýü“Ü‘`³Ð{O¹"1æ[‰2ÕJûã¥bRª4ÄØ%QIp âm%¡â²ùcN ’Tæ·ÿ'tÜ:vçª:·C²Íáma_Ј¿GÈOƒç_uET°Miwîr̘Ι‡îJÏ^“)oÉ¥ “±;ï€Ák{îCÌüâ=>âPnyo>d•1|›®¬|÷!.¼õFŽß{$So} €ŠšªhÇÔW7BU]Xÿ\*¾ã$HÐÐìY<‡¨ @M}‹ÍÖ uè”vºÂlˆ  º®W o¬ÛTSíƒG]Ä.[ærÙä~œ|ý#|<ósn¾ô öèÎÛ *è^P™ƒ0ý¹ù¶Û¸éÖçñ…²ÉÌðD/3¹òš©\;õf¾úmžq1cºÀuû䦧^ãמaËQ{¤”ë/>—ñ‡,VÿI·Ü,& ïK)ŒP iê++© µRµ¡€ªÆFššƒñg¿«˜FJ&¢®ª• 5õayFÔP»Ö—#À~»¡¥ìuàñ\zÞy Ýù8È.‚¦&64hi¨aCM]ØÞ\‡¿© Œ@3M5,¯‚3.½"*Õ¹÷?9gLçŒCw¥g¯}(oÉ¡0/-yýFÛí;,bÅ9ŽN%ñÔ˜eŸ,%Ž Äf±Úª’$.ÏØ-qq*oçu’Dï‡Y§œ¼gm50- —$œP¥Äâìg;ò›§”³—Ä\«P%1¯u?S"–W•\w+(¯Å½T ïš'ªLóФðLIí¹8²)ÉËhEuÖbѧ”å{)«ØH»E†R¶u[bÙŽÛPâ°°!P*þeÏ’Ä“ ,PX„vYìøÙž}îp¼ml}ᤉH¦]sS›wJ(fo^l‹®ÓùÏ::E’=›­=ŸaÛ½}ûõ§4;› Œ[‘Ö74ÐyàÎÌøâ N;ùDÎ<á(r³³i ¸9ð„èU쥵¹VwçÞð,ËWÁówŸÁ›Odãótb·3ò ..½í)~=þpn½ðR<]óάÙ|ñà%Ì’‚†BÑo‹¬’Ax #\(ÒnÃ0À•ÅýûÓ¯W>AÃ@Bpy6¬?Ùýº‡·æ­VN*ɶXd† ·;ƒ‘ýú“;¨AÃÀ0 ‚!ƒ^†Ów½—ß ˜^È›ÍäÔ#Nâá+Nåù¼\Œ–f¶ÚçF éNS«#Í‹rg³óæù¼r!×]ûùý&³ÿ‰GÐ)-ˆ/`4 6±=}Tñøí7aHמĮC Byyï«o8lÊq\~â¸]!è3†§y†½‡°¾®6lH“ÕÙŒ«ßhãN²ŠíR€!„ŒAà äÊ`›ACÐ)œ<ŠxY•«Ý 'l%A‚ ±%®Î} !C½7N¿Ü2ª ƒ}Ϻ³f¯à¡WçÆwษÿåâÝòÙíà“Y²n‡Žd낆AvÙ6›ŽÇ0ðä”2lÐx‚µ-xlÛ®ŸÇInNNŒ~¦Ñêk´ŸÈ”ƒÇ»-h“‡Ç©hxÊkF«chqή·z)sµ +¯d*u)ȤØÈHRŽq³3wòÞ(’ŸïlG˜mŽb·EU{lu\!í$§Å8-ðìν·Ìhg:‰U`°rxß» ÉO–S,£SL£$/ÝfK4ívãÄY¶ñ´6g¿‹Hr"''‡“üÌ'cÙV$ëP'§ó)kæ‹ %V§Ž©„Iþ<+%«lµx²‹UµûÛòdDç­»#`c›¨«SÕ××Knnîßþ `0Èš5k(++Ããùû¸nÐ0XXUMÛMšÛáóe³%„q¼.7¡` „â3E᢬ _ …+×à—4:uéJ×/êð§Ëäæç–¯YE³Jºt'Ûã§®ÙGAA>Á¦ÖTÔ“[Òƒî9.ª "àõ¤ãv…ðûC±™‘É!-ÝK($4"ß)<é^T(„?jŸ'T¼ö~¡i)ii^ à F/õx¼¸=à÷û1 E~A>9À²Õ+¨o PTZJ·¢ÖÖøð¦§ã€Pìw[±kë­!Õ>#'¨ÙŽ#öÙ¢fcÐ^´)úä¸Ð—Šn™+ ¾¶–&oE]zPâ‚`0@U¤`»;¢ø(\î0¡ áF»r)<‘¬õ€ßÎãQP[[‹7-ƒ®=z£|-¬«õ‡e p8éF‹'¼2zD) iÓkr bä}¿ÈDÑ>„IT¤»ý-u”Ò(ìÚ$DE].ÒÓ¼x<®°ýtÅøzBy¾wÄ+ÖÏtÊúöCaðù)¯«#d.—²1„Êä­0ËÃ)A’’7GB” k³ÇʦãÆÖ¸Œ¨“eКã&eÿ]2Bb·“Ê"9Y ';”Õåmǯ*œ=pˆ½¼%OkŠäÏìØfÛ;µ1Yù)§ ßîô¨d$ÓJG’xà£'îˆÍû%Äe*{²hyB 6Ùÿ6Ùð©,‚,uKÅ&gmÄú„0å@î-^–uPí¶Ã“H‘@Åy7Ìéð§çAj5Tãæ±æÏÔ¦QÔI§Ø{#½±^pâŒ/²­®,W¢a’ôùhô·&èwûÊÃE(à§9èO°7(ðûšÃز ¢Š£‚A±÷&ù}¾¨Áj{@ î3±˜-4>6±ÁDÀ¢ÏˆùUÐï$,©(‰2‚š8¹*Ó$ÛÖ6«›R ˆÍB"¡‚ øiôûãon™!lsf*å/l˜Ä½‹ßç‹L(ö$À¼ W*B¤£N†¶~Ë.\¡Í˜vø|¾H\q€P¤2” „cY] F(\2*ÆôûhôûLm0ÍV§Åº“ÄKg1Y[®Ì­Æ^Bv{Ì LÈûr4cÄ­X†S,«²ˆû$³Ë1B%=òÎ6ñÊÚ-¼”}}̸µ™R‰‰eɈa*¥ $‰W'ÉîSJ‰#výèä•Sv^teM4“°”Êñ§b±uju\¤JBXœŽxv 0Öc.‰çÜv¡,²²ÅÙ±IO²<Ü#¶ày*ãË(Z…[C›Enì\iVd™„fïb±ë ± ùßch/‘(¶¨®°¹>þ4kC% ˆ“Ñ$+óXn»Þ.™a£3ÒÅ~`ËFL¼ÊâßfR…²_mÚð[&6éêY‰¶špã’RRØN±Ò ±¸œ/æII9`ÆÁS`%SK•rÞÆÄ,›„e#7Ûú~q“läÞ6“«à¬7¶…Ñú놖ÉJ*^íôXœ¤@TìJõD_^RóöY4bWÅ..ÛéÌcLa“ø£¢D?¡„‹]&°í½œô_ìmM*GÛzX¤àÆ‹uQÖëØØÏ¸mݨ«*>NÙ¢‚Ä'$ÅVEH™XÅd”ˆé€ËìÓxQItÖœtÕ¶P‰yï„c$mõÂ"!(I=ø¤'9í`J»žˆWÒF¿Êa®tØ}1ËÞi¾p<8A9ì,˜K°m4aФóÏ8:­m—S¹‹ØŒ1«Ò#–DJ¥0¡ÄÞF%’¤ÄDÅ·/aKX¬ e¹ _l›&,°9ÝÉÂ+àokxL£NlÈz2#›ìôKÙ(çä ³¬‰_É'¼›¤àe°”·$®ìëŠV¥Œ ÉVŽ]!ió©;6:c)Oi÷ [Feµ‚ÝJPÎ Xï¼yAã8Î$ÉÙìj#È “—Æ¡@6¦‰J,¼}VdÙòa~e3†cõ_™Úç˜1¯u0n-§â½Î çŠVãÍVLvÐd‹3ñKØéÀþÄ«1.6^.EêG Úe@cãL°ª­êD~@VαYì;•0K‡ªç-YÝ_eÁ R:Vu#Ü{ÊBžv $…}}*ï,ÊÁÆXŒaç:{—Ä1ô¿Pª³C–L’T݃*Þ(”³g.áß*‘xØ"s9…²žìã œ8“fÇzivDÃT:­°lËfXx{•ä”àXVö¯í¶+âþ–ÄvZYåà µÄöÙbk\Åy›,Ïʃ®H= ©WLÙ{’Ùsåà=RI¼‰VžÔ¸!b’u±Ò.«~ÂeWhÞÉ“«Tr/¹$Ÿhâ°”3YçZ³mÖã„0•èUÆ Z&%ñv+ o–•]•Ș,ôÅœk'Íef’y›­tÕÖÛ$V‰ƒ—×Êq¡ì½Õ •±½Îê˜SltΩ¤SÜ"A%© ‘x1/`Á&TÁÂCƒIvæ¼ç3V§ÅaÑ(&Ï4öÏ»pž¸ö‹õ"F9¬þ̉zq!n^n»SàâÜ"ºdÒ?J9]„Ï7¬ @Ì 7Þ TâȆa´y/¢'+´× “¶‡Z®ÄU‚1nKÒP‘ßIBádŸt¨Úâ+yÆ­ø”ý8µ²Ùf2ëlJ1æg$Œmí¥¹X½e‚Iû¬¥þ„ÞÅõUU!s›ÌSœy¾ˆ³½6ÜT;4ú¢bþåbý>B¬µJˆ­ŽÚg,ç_SâWœ}1Í)a»¦L×ÛÏ÷¶s¬rZu“P:+¤Y^ï&;ÙI§bPQ!›GÓ"ÐÐÐÐÐÐÐÐÐø»Ñ!Ï^o ñÆÿDù ¿¤¹\dx6Mz×áHgH„EÕ5¸”ÂãjK­‹¨|Ä]lÙÖäŠ=†Ïª¬J[¤»8œûjqtψý·UVí}1eÃØd¿$”9²9Ÿç£SíÆ€ù¾Ê¦Ö$™¬TŠp C`XI'Ü›`2QÇ#†[)6+.ÂëÒÑÿ ƒEUÕa.ävoríë¬L¥˜{©¡¡¡¡¡¡¡Ñ¡ÚtY«C œêœihhhhhhht lêܧcƒ©Ýœÿ6lâüÇÕñä- Eà54444444::D„MÙÙÙ13mÚŽÓÐÐÐÐÐÐÐø— íÛ?~¸·&Çò‰TFÐÞN M®d’Jr†:ÀkÏÝÇ˯~Äʵ‘Òk Oæ@^|û)ÊrþÞÒÇ+÷ßÅœõ-t**¢s·-8øÀ xܤ}Õ<õÄ4ŽÜ›mGuw¾¶i-'N:ˆù-mgô*Jºö`Çýæœ)计¡¡¡ñ¯Bû‰õštþ¬3ìítù{/>À´é Ù÷ð£È úðü¨ŒÂÿ!çh+_¼úÿ]ÔÄúuëÀ½3{0sâ|Ó*þsê©xAŸä¤S¹È/ëFi³Aº7 Zyæå—™çËѤSCCCCã_†M»dR‡#"’ôä·†ÌN+IDATl~ ¯?ÿÌÿè[æsÏ'¿qpÎa[r×K HGêÉ¢n”®Â^*« ·>÷߸B¿eñ¥¡ã+44444þ]ˆz97Ñ´Ï¿±S”ÇÁ@Òë¾þìÖ¬«¤¨kw¶=šüÌvqµ–¯äÓ¯¾§Õ•˶ãw¦sqûÞvùêU¸òºS’§X·ðG~^UEÿa#éÛ­(îþ+ñÝ ilzöí˰ÃÉóÆ\`4ðÉ_PUçgð˜mÖ»sB]Ê Žká‹QYÛˆmÇ'´!Ê×­"”[F·Kæ2÷·JrŠŠ¾Õ–dyÛßù÷ï>gÎò ” Éö#z[ÜÉǬ¯fÓØâ§WŸ^w*¦0?/UK™»d4¯£(ÉÍ&P»žUå>úê•pÇŠå+¤çRÖ5þZ ðºõiT›êëëåŸ@ åË—K ø[Ÿã åçŠ â†l¯9eŸaÂà}İù¾ü·Y2~Xw¤K·ðÿ’=\V´„¿ŸùÐMRBv¡äy<=äö·æEÔ¸2ÙþÈóäÞÛ®nKi²»Ê¿[½æ¥ëÏ–LÒr¥{×NÈáç?ý~íÏË6½r\Ò©8G9ÿ†;ÚzÖ!#ÆHµÅ{T­øQ&lÞK)Èõ äÈÍ/}¿Qò2p¯ÿÈ7\Ùþ. 3–´=±R.?r»ð3ò c/½5îóßyP6/Ë@23”¸@zmw¢ÔF¯ÉÔó®´è3ø¨A|‹¦ W?ßîæŸÞ—brÎc3Ú¼ÿ™ÒoÏãECCCCCãß0$?W:s 𗵡¾¾^:0é þ1ÒÙ°BF”d t•—>úFü†È/ß"7^{»TEªç¾# ¥c÷“%• R³z®LÜ,M P¾\~æI{m%N×?ýŽ|÷Å )éº× aŠUþ½²åÄSÄnµ|4íùèãÂmhY%#ŠÜ}ä“9Ë¥¹iƒ\ràáû=ûµ%é¬Jì^Ùup@ùtÞR ¶VËá;ô(–7¤ÞçLÚ<ú.7=ó‘Ì|öä¥_DDäÁS'„IáË_J ”i €Ü>ý·0ñýåC)¡l´¼ñåײvõo²çðRQ²!òŒOÛGÙë´kä—%+dÑçOK&ÈYÿ"">Ô Qy£¤5¦]7:\ G~© ØÎã´õÑÐÐÐÐøw‘ÎPHækÒùÏ“NŸé1ONc—{#‹WP›Â{,þd?t-vØŽ1£Ç0éÂÇ•5)Ë£®b ܃“vÚ&•h­Çãé€âÊ¡X§--v§-¬ôAAzâ÷.wõ¦O}Ôy„‹|¶¡°$7ì\0Œèß»{ «ö=–W_zž;®¿†[Î:šW¦/`Éû7Ó½4-̽ñ-m¬i‚´®¤¥ð2’ÖÀÙ÷¼ÄŽÝ4·IÏÍ#/7ŸžÝz¤,OÃò2¬Ÿ9oô #®â’#·¡¡¡·7‚‚|:÷MËYl[ÿ¼Ÿú2¶Ç7„e¹Y¯vVÚ°ðj€¼Œ¶çæóÐ g1î”»yáíoùüÁ‡)z8{ôÍr\~hhhhhhhl:ø§øZ¯Þ2Ðȹç>dùýNc'‚ÿ+^ŸSýì×e¹ÀÎ#÷“EQ|þxò F欫ª H/êÅá§^Êk}œµÿX~ÿàVì|Ô<öÐûí7¨ý™iŸVÒeühòÍ÷u¥'ÔèÜrïI,ü­mÆlÇ;N`ÌÖ[1dP?r²R'dÊå‚@ÐRZ…¶fðÃ/ 1r¶Ÿ°ãÆaèÐÁtÊò€á¢˜5ëûèož?ïDÞœ]‹·wWòr²˜þå/á 6,eÒvJò²£¿Ûöä+Ù7£÷Û…G~låª;®Ó£WCCCCC#qæÞdëtv¼íõ±‡Zé+ô?ðlŽÞs >~àÆí}O=;KN;”#öb•Ž»òF¶Ì„·êÉ¿ÈÓ÷ßÀf»]€·dÎ?k[*Ê[X¿!~ »fC9¬ß€–¾|5……%uÖ•¼õît¦=þ /: ‡‘«À3l §NÈSWìɉ×ÝËô—ž`l÷-Ø@OßtQÜ}!/Ÿsêùçqá—óÕ’ðs‹·8„«Ùž§.Ý•ý޽œw§¿ËM—žBï².Lýð·”åY]ÞåU–ß¹3ºòÌc±ìÃè¶ÓA<õÎtž¹ûz†öêű= ¹=9þˆaTõ4ÿ9x¡P\.óIžx}ç=òy á–³Ûf[ñm]'L·õÕ~†O×ÖGCCCCã_} æ?¸Cµ€S/½—XJÅúJBÊK)ÜžbJ2µÂnjøvÆkÜüÈtB7Üû.—œ°½‰µw⌫oæà `ˆ!n:u.¥Ïfƒµð44444þUPlÚµ[:àöºÁ¯55 rØ^×øŸZ¶áoõ“–™¦e¡¡¡¡¡¡áÈ’‡þÓ¼¬ ³d’®Ðر:SN äMœu<Ò©"…‹híÓÐÐÐÐÐÐøWÑÎM™u8ÒÙ&hííÔÐÐÐÐÐÐø7A)íéüg­Ã¯içÆbÎŒ·8bâöœvÝ=›T»¦žu£ö>–÷¿øEw’†††††FR.¤I§FZxrêTžï‡ÿ÷–Hù"NØa[í:™w~XAAQ·MJR%E…,ÿl{ì0”ûŸÇŠF­= óyd·W6QÖÙñ¶×D¢•ª6aÔqãå—s÷“Ÿþ? ¬–ý·Åc_,äúG§SW½‚©§¸IIêÄ«£¢¡†—o¿ˆ¹¯ßÁ¨Ý% m‹†††††F¢g®o¢¨cf¯+«Ãîÿ‚0þÊ^ìB7ôèÖ+á›Å¿,`þoë-bˆù¿Î¥®©•еËù½º€U¿þÀ»o½ÍÏKÖX<§…÷Þ|gžzK×&|ûð%GñÆï ÜòÆl.;~¸ïZë+X°dEøúÕ¼ýêë|üñlË·©\ò Óßz›éŸ~ÁüÅKih bøX´º€>ú˜Ï·ï·ï>æãsî1óÝ7yæÉgøêçeOHã so⋇N§â«'9鎴uÑÐÐÐÐÐH žXp Míìu0dsöº!×^xœtÖMRÞjýÛ¥?,»LÚW>^´Âú‚†µrúvÛÊu¼ò§ÚøÁKÈž#]Ò«寛o–Ë.»D®šú°øDä¾ó÷\äó¥ñg¤póé^Y^S'W6Xºî2E®½æR‰0jÁ•/ôSôúõ¿}-£t@J 3Òåê§f¶ß°q‘ä‚ôsºe;¿zèTé’?D^üï“2 ƒès>ý~‘PTârõùG‡¿SB‚7Snz|®´¬}S† ÞN®:î´ð÷}GÈ£÷^¹KžýzmøkË‘#û EÅ…ÈYרËxò…‚g Tûô9»q¨¢Ò‚mg¯wXÒiuØýK7&²Kå´”uñœNê|(C ²[xe^eøšûÏ$€tüòûÊrY6çI9aêG"õ_‹„Œ¡r˃wK…€’©·Ý)ý2‘#oÿFDDNÛK S^ÿa¥ˆˆÜtÌÈëm1áç§ÂÄõΗj £¡¡¡¡¡‘ÀBštþS¤s¾ é©_3_®8iI!¿»\yï³²>–|>y÷žeX^˜|î{ìY2û·•q÷X0û+Ùo—±H—äÉW>ý-mqndÒ)[~{Ý”-<òåò0…\ñɃÈmŸ„½ƒ§MÚL Mîÿ`vô7ž&k¿‰ˆÞóÈW=*bˆ4´øå—'.@Î~ëWyò¤Ié2§Áº…sŸ¾H™pøÑÏ~}ú\dÆiY*“/ŠùU¥ô9vê"u_…Ÿw÷""Òdócï‘JÓå¸û~1~@ÆO¹1L•Z¥röãÈ!WY“úÖåŸ S®|K[ |Á`˜t†6MÒéêØQ ‰Èí6”kz_k-÷¾מq]:•pÚUÏà *=ϸ˜Ÿëšøé­g˜=ínFèÉŽûÉÜuõ Ùj[^ûèkj+—s`¯rþsàŽõÁ#o|µíó# \FÈòÛ n¸Aμä¼òFÈÌ ;v ¶j5ôÚƒS'nýM¯Ò^ÑèÕå_Ìà³G§²ÃÛ1aüvpÅsxÓ3É«i_çR@…në6ÖW£Hã¾GÏŽ~Ê??7j?}‘ pÌåçÅü*ˆDâI| Ud¹Š9ñø\†×åå€cŽÔøBääæ!¿†e¶`潌Û~vØ¿¯'“Rš¬Ck³ÃǶªFÆ®¡¡¡¡¡‘€M4‘ÈÓñäœZÞúúå ùõוt0”^ݺÄuR¨º‚¹?-¤1ÙÅeô(ë‹2âï±ô‡X¾¦p1xhºflé Á°9ï4½l;.›<Œk^¼Š·ÎØœ‡g®äðËÞ ¯í=}@ivÜo¾˜ó9ЃÀ|÷Î{ðuví¢©%Hzn.y9ù”•v 33¨aQU^ÝÛ2¸p““*ž‘_&|.h¨' u-,Œ~ß¼p6•@vF:!£‘ •Mf àÆÛB)…AHÏ`Êip⮽ihnÁ“žK~^¥ÝJ,eX¹wÿ^Ú°hhhhhhü°ÎYÁ–z.þî5Ú~]7Ë ‹„Çߟͪ9ŸqáII÷Bóªß¹þ„#)(îÌ”kîçÈ‹ïañ²?u÷~BVw¹øšÛ弓 'å–_6´¿÷ GOŒ$#/¿OÏÌßZ^|ñ1Ò|Íì{ÄùÜ2õ·;¯/yêr{Â-½ÈiŸN#33qûÈ~£ûF½­O=õ0ƒv:‰‡Çìi·Ç¹¾K‡ŽeÇü¦µ–Ûm ;ì°ÅÒž.¼ÿáË}êe¼óÚ;l½ßE|óÈT?}gå6#iCÙwâaä¹Wï¼+£Ë²{°ÝG†ßõÀ«Ÿá™Âž\qËcœsÚ§ddf0t§ý8vÿ­L2kâÄÉYi¸xû¹[õjVCCCCC#žZD67M_§ª¯¯—ÜÜÜ¿ýAÁ`5kÖPVV†Çó÷qÝ@È`qM ‹ ñºþâèá>дœ‘E]X^v4U??÷ÍQ[çò\hodÎ )ÝÉç7HO³–Ç´óåÐÛŸ¤`ðŽÜ{ûTv7š’¼ÿŸÈ‹ /ˆ'=^?Zª×óõoqñégðCµŸÓï{{OÛ] øC!W×2¨¨¯ÛµIð²6444tÐc0ÿÎÄßP#¯?z÷Ýq=cz—ñcMoM»7á²ÊòFXW‘òmí'À!·=Á·Ó ëBŽÚs[9ëšÿ·þ3N€ËŽÏ.‡ŸDyáhŸ>GN &´©z:=Zæÿ‹0B|úÄ}¼²¼žÌî»òî›2nH¢'zç½"Ïù—=vôÁ§ðýÁ§ðÃoÓ˜SºI‰äÐ ïaÒU½Øië!Ú–hhhhhhØ@Eö×7U Ôá¶×ý¡‹k"®e—Kk ††††††Æ¿Ã`qu  õöú?âUx\D´öihhhhhhük "þ£ëtþc×ÐÐÐÐÐÐÐø×’ÏM´]¶8¼†††††††Æ¿ J)Pj“m_ÇL$Rÿûá»|ðÃo” Ø’)»nKÛAœ µå¬®ó3¸Wö‹ƒ-ü¶r %Ý(ÉÍúÛÛÖÒT˪Šj<^O¸~ªRQO´/à§ ¨”.ù¹ÿ˜¬ÄWÍ‹oÀÒÊVv¿ã†éc454444:¤­Tç¦I„:éT^àþêß¹õ•Ù~·ýß«x“~ÝŸçÎáò©/±ýI§rô®ÛF?ÿâÃ×9ì¶÷¸ï±û™²Eø¸Ê–ÊŸÙÿä©sêÕœ·ïˆ¿½m >þ#nx„ô²dx½çfÑÔÔH‹?ÀŠ•ë9á̳™zäÿ ´¼÷ê<÷s3½¶Ò¤SCCCC£cbu¾u<Òùœ9êkXÅ/¾AÆ ]6yÒ9²Ï@zd3¾_/ÒcÅ ÒÈlh䉗Þâ€-N%(ƒ– ›PàŸ‰?8j{¾µ7Þœ*Ïâ¢ÞbσŽáàqýihh¦wß>ÿìL/f«¡Ýx«¶‰-†öÖFICCCC£Ã‘MM6̰ÑN‰Oùc?V[\@çâ":çÇoø˜÷ó"òÊúЧS^ÜW-•ü²¬†-†ôcͲ¥xKûR–çaÑÏsøe]=›Î€Òøû…š*ùpÖ|jš…­GmÍÀÒ¼nr¯žÙ4ezÉÊî÷¹Û롸K'~ýú ^úz7ŽÛ¶in<îØ%Ÿ¯¿þŽå)îÒƒÝF ~ÓX[ÁoZÑ¿þšµÌøa1Þ‚&l3¯©ófÇ/ëjèܽ/; @^—.LèÒ€`Áν«•Í6Ûœ±#úF·ð×ùHN†”Å·?ØRÏÏKVÓ·?j×®ÀS–ç¯?Ïá—õ ÛrËDy6Wòá·ó©i­·i!O…ù¨¦Fú—䯬S$Zõ@CCCCCã—mÚI-0¦SÅüßT`ðÃÇÓyì‹…œqÊa,üæK~ú}ùÞ>~÷ejæeQ_SKÞÀm9c—Íyú¡Ûx½¶/?<{ Å1Ò»ÿ¦kxlmWæ=|Þx#ˇlÏö%î{æ=êš}duëËS/aâ 2Vü2“ÿ\~T=-\rÿs\Ã5bã¼™]z±ý®Û2 rß(i …ÈÍÍ£(­•ÇŸy…)Û^DVv:"Жà¨^Á…—ß´%5lÕ¯„ßþN÷m÷äñ+N¤G¦›Ÿ?ý€ó¦}Ë9ÇïÁs¾ÄܾVy"w? · ðWrý57óЬ5 êQȲÅëÙý˜£¹ë„ÉÄÙΆ†<-Mq²û…G¹î›Vf½v?CrÚ:íé»8ïýæ=}-Þ| ‹mË%î{ú}êZüayÞp1¶Éó«ˆ<óèìõqÉ}Ï2õ†+8Ò$Ï-{fÒ¨J²ÚsèVÌ~+^þ†)'œÂ®ºi«¥¡¡¡¡ñ¿É€DmÒi-°zzª¤üǧ^¾ƒGOØ €Þ[ïÂ1Ûuçþç^£:ÂgÞŸþ+ zsìnC ­­MHAOî¿é*öџ͆ãˆ=‡³ê—eÀâY³øj±Á±ûïHV žªúV¶» õk~ãÛµuàU=o/TÔ4°ÇÎ{rÜN}¸ýYVÑHçü 22³ÁXÁ3_ýÊI'ŸÀ[öÅE[Œ›ÄåS&ðñR d§±¶¶™ÉGÏ'ObPYv2Œ,¨6YÇ3ÍaÌ.™Ð+‹ªšjz B¿Ì³çþdÙ'fJÝiжœ>y+¦=÷ ‹Ãß~ÿñ ~¬*àŒý·„¦ÖF¤ ÷ßt“Fôg³ÍÇqäž#X'ÏP»<ë|Œ» õk'ÊSy0;ø‹zçÅ'žâ•+ÿËçrøñgðŸ[ãÛ•š|jhhhhüï \¨‰þ)ª©"´‰Ãßúšëï}–és—1xÌö¼öÒÕŒì’GÙìóãq{P Å28õ¨ýxòäû™6k=§Œé³oÌdÇ]þC_/ ­4ÖÕ1bÂìЫ0ú«œÒ|T¨€…5Õd—–ðþËÏòÅ[Chij¥°¬”æ†f à/‘D°¥;‹ÓO<Œ'¹‡›Ÿráóf“‘ÕK©yRÚ5îWƒû &+´˜ßž>y%Ý9mÊîí “ã"//tP¹²shXùÇ^ú#„?h 9¹¤ez,‡ƒ²hç”#öç®7/ãñærë#xö 71Åfk­åiÄʳÄBžÝJhnhJQžn¶ÝyÛî<‰Åó¾äÜïgï·ßcÜ®;qå)Sت$W[3 M:ã8Œ-¼¹%ô4˜•Í´TWòÍœŸèµÓ:¥Ç‹Áø Á°I.Ùl»‰1êU^xõ †4tfAC÷ì»cäÇBH /=þ7Ë~ü¬ž[â\M~Bx8óüséã à7„4¯›ÜÜ\Šó þ1ˆå‚õ~Ðw{NÜùmî|ûkÒ³ H÷º #%>#÷»–@3~ÃE>!+ÍKšaÐæOÏòâö( HKc]E#MÞã& ¦¹ÕËí!33ƒÂ‚¢”ÛZ2`[NÞm8Ͻó»w¯`æòŽŸ:1üe(HP ?]%È3§çQy.7gœ.}£òô››³Ñòl¬ªà»9K©yèÝ·ŒÁeedë# ?Ž·½›)cB>¸ìü3™þØM9¶?Üq'c9…ë^|ŸŠ»P\n‚•U”¯]OF¶ûæpÚ”ý¨þi§ßù›m·3;õ+ˆ²^1 pµçwÏÿì¿Ü7cûî¾ ÃõÃU»œ¹+éß§7Cúõ¡Ïžt.ÌÅïüuÜ[†/ÀчíMŸü­!ƒÖÖÈéÇàb¯|þ¡è¯šxåÃÉêÚŸþ@k HÈ0ðŒè¡ !Ðê(ÀV3xî¯ô*ëÎà~}Ô»'=;—â ú6¢µnŽ?j_r+æqÎõOS´ÙÝ:’¤Âò·'^ž/c¿ÝGEå©jV0oe¬<{„åHMžµë×pûíw2擹îÍoÙû £yï᛹ñ¸}ÔI{9544446}DÓˆ6Ñ#Á; §3ùP9ºrúé§qÌÁûñäK¯ñàC÷ñÊôOyã¡ëé“í%³pû•p׃÷S`Œ·a9_ÿä¶«O£8'|Í·ßÉÓgrûÇ+9k÷ãJwbÖ³¸õéZj×®æéW?fànrÞÞáâí=FïÌy|Î%œÇªÿÉÄá]øqæ×¼1o%×LÊÞýÿ|mÐÆ†V—o $à¦tàx?3îü†uUÕ@O®<ù`ö¹ä)LŽÞ~ Ÿ¿ÿ&Ï~ÛÄw¨¬ªfmùbËz†|-¬­¨¤¶¡Jr¹òÌ)ì~ÆlsNgNG`ýï<ùÂûŒ?ð0nøÏäößù›Yµ®œÚf¿e{;Ï‘;~ÎO}Ǿ'lO‘«}óÝ÷ßrkFXžO½ú1äùŸw«Ž=‚‰Ã»òãW_óÆÜÔäY±ðsö>ý:ʳús 'qâä](Épk륡¡¡¡ñ?iûÿ›$Ü—\rÉÕéééûƒ à¡¡¼¼<\®¿ÏÁ2 ªZZ(ÎÌÄä9iÙ¹Œ=Š#v´úé;h02=(o&ã¶HÍšU|3 WTÒèæì>f0Ñ—*• >竺NÜqÖÁä´qñóÙGŸ±¸!€Û0XWÕÌ.{ÀçA‘WE¹þ˜±ÛÒ5­™™?ÌeÞÂß(o6Øi·]9`ìdºÿü~nS]-FZ»í8Š\OøŒ¦A=KXÓècìv£RVH[0n@>ß}ÿßÎÿ#Ÿ+/:‡)ã7 צzš]Ùì4~4ù‘¶|Mlð¹Ùa̺e{éÔs ; ëÊ¢Ÿðü,Y[CïÁ[pÜÁ{Ñ97³½ÿ­¬k¶Ûn[†”X©"Í«æóê‚fn¹øº´‘>ñóÉGŸñ[C0,Ï Íì¼Ï<˜ Ïqtñ6óÕìyÌ[¸„òfaÇÝ&rÀØÍ“ÊsCùJü¥ý¹ÿª ØsÄ@²=.m·44444þ硪¥…N)p Š—µÁï÷£êëë%7÷ïß> ƒ¬Y³†²²2<ž¿ÏÁê…X\SË ÂB¼î¿@ˆâ•–HT~ÿšqG^Ëþ\ËûŠéñFN=âD6l/Ÿº_  †ðx2þ_WFþ`+ižÌ?×Ç&<Þì?6P6üÊøCÏ¡÷þ§ñ©{ÅÉó”#N zûC˜vê¾)ÊÓÀãI×ÖGCCCCã_(Äâê’ævo¼¬ 1{ý/®C8›«–ðßOgSרÄãO½BÞ–»qɾ£܆ª ¬¯Nµô‘Çûÿ.µ?K8&œÁæ ^þ`&Õͼ>í*:mÉóÇì‘ Ïªª*Êkj7BžÚðhhhhhhljèÇ`þ]ñ³*ÔÊ'_ÎdU½‹í÷>”«N=’<3ÃuyÙaÒî4÷¤µ+™ý†tç¿w]Ãä}â±WÞÓäSCCCCCCcÓá?›0Ü—\rÉÕéééûƒ à¡¡¼¼<\®¿ë†D¨ji¥Sf&nÓsºΔ#¢H5ðþËÏóÌËoSpÓ½g ²ÓHÏïÂîûƸÁe,øz=ösWVR\Öî A¹6fGÙwZ+WðâSOðæG_£²òéÓ·éúP ÿ'„9P Åèÿ‹—µÁï÷w¼c0ý1Ç`¦%9óýgnã²;'˜]ÆáGÁ™ÇO&3¦ikæÅe—\Ä׿×3j÷ƒ¸è‚³Ù¼KŒ¬BµÜsÅE<òß(´')‡LØJk½††††††Æ?Ž@Ȉ9 \ƒù·C¥F»ôÇOùö§Åä”ôaÈðQ Ø WÌÏjV/ç“3ÙJgððÑl=d(yî˜ õÌœñ>‹–¯¥Ç€ÍØj»±ôîV¬5^CCCCCCCÞŽúbÖçyøeætzäIf.XËÐm÷àºÛ.cû-{E¯Ø°â7ž}ôa^}ï3¼ûqø1—pØþ»‘T°µ–ßx‰'Ÿ~‘åµ!vÞïPî>æH–hmÒÐÐÐÐÐÐøƒ á >›h »§# <"ö„ï^yø*λñUÆïs(wí^CCCCCCCãïÀ&~øºKË[CCCCCCC£#1¡Mÿ¨~y:­;6IEND®B`‚flask-3.1.1/docs/_static/flask-horizontal.png000066400000000000000000000603341501065614000211730ustar00rootroot00000000000000‰PNG  IHDRjÈ/žý% pHYs.#.#x¥?v IDATxÚí]xÇÖ½*î½Ð«m0`zïÐKHôúç%¤’ÞI^H'’—’PBï½—@è`À¸Ð{ÇÝ–ôß»’Ù–ä•´+íJ÷䛬W³³3wΜiw4`Áχ3 Ôi'ƒ„¸VPÜrLÓ†ûÀ¡¡ÿý™žeræG¦rTöã»$h|-ãô_H{¬Àh¬ðö¦Ê²Ã~ ÍWûÓŒZFÎ<¹[XóÍvMÏø”ÅŒ`4Ù®I&‰r°ŸásUµ Ø­k$6_ Œ¾W¯‹3˜Äg\òa€VëœÅ9ȸ”„¤ñ’dëo©Y=­î3Y¾“µ¡IIl‡UíbeÏ2N'"ã†Ô¯ªÆ Ÿk?ý"<|Ïý"«· Š [ö^ÞS¯Å™0ãŒö3îÃN-l~ÿÃÔ_àÞ»îŇE¶-î=•WUèläÛg][Vúãßþø F¾§R«£êíîT¿’*,&^¼—L_ëʳñ·ÿµúÍk˜qÌ8çÒ]PTAÂçéÓ§ÂÈÛîtÌq&yZUG™&ö^ú^dæ½jÑZ"î"á±µ;*ürÜœù¿õjÒ¨Ì÷sþú ñ7öB‘Ô½å¥>sŸufaæµ™éy¥Uµ‡ÊÔHü˜±ŒvlÂGðÖÈᥠ„5Ížý‡ßnÛ⊠ÎZÃ:|±^•ü¦³ê&¾úh45,Ÿ²Å¤ Ÿj•q&»wïò¿aÍ¿àâä ¢Ò±|þLè=ô6rÄi#âžñnWo“©fÆy±]¦q(B&w¤ÊNNü¤Ì¿ÉÒ Š‹áÂ7_T°:et·Á·ˆÖqn41ÎVe–}¿³û|Gª-6βn[¼¥4ê?ñœ‘”iƒÖrãâÙ‚••ƒ<=‡C®ò %£^°Ê %",5³lÆáKUn^¸EȰ³_^&²3_}†Ön;S¶.]ú{ƒI†Œ3½¦üE{ÖSޝ>³úó@{‚€èÛò9¡LU%© ñ^ýf¯ƒ•·ô¬ðý©IŸ–~Î)(€°  R ýgùhÕo„,—z4Ã@ÕÌYYâL·37À:~ké"Êâ(è±:xÁf‡O·Î´ì^9uœç}e|Ybyå¬O+6Î’ßéÉÄ2³…þó6BêªyfQSX¤ƒ-bØŽn_'[&á‹<†iÈ¿=þûŸJîmŽ÷Z}½¿ïZ^¤TÖ¸ÐUÓû¯µF§´ÝŒÖwxÍ|Ñ÷‡GÕÌøðMŸÈÔRƒçl0 hnØs¸äãv*Ë8¸ F$öê·9§E©ÖÌ«þ¯~·!•>$s¼oUS!ã6îjD“p'Ôí>ü ¥ÂïéYF)Ç¤Ñ ÏÞÝ ¡¦OfÚÓ'„èž‘‹ä}q"ZósZ†ÉVn8œˆv2çlÜÀ§2N˜ôu¹ }¦9˜6qг4·{Áþi4êÌËGÀuì‹JìÛ5ÅË7vú€&ÅtÖ%HSɳà³–;]=­«é¸ŽÍJÿýôò #²J›*Kœ -ÍÁœfZÕ+mšTø~öÂù0tð0̸h¿ä>a²¹<>èÔ ‚u:‡?\¸x <"""+±6YæJ¬5/bãµeÝ.L6ÒÒzÂraì†]¢½xñ|¸|íj…ß­ƒ¬“¥.p•Ó4a¹‡&š_ÅLþU_hsrÄvæÛš©Z±t!ôê?ÂÂ#™i0»5YÜm°_ñY™áŒ_Ò2¾ ÐûC¥ <î9-ôƒN-ó€¡cûíH¦jƬíù}ɉ.N…Û7޵E”+–Ÿq%§Úø.-Ïs‘*ÂÆ*“Iy¤`r²Ñku\œ 76wœ (‰ ¼rGñÆF«Œ"v˜Àþ~“'¬©’?:šdó$˜T¬SXÁîZ!ÇÌfåõCÊâ ÐÃó-ÍÀs áµõÛì®=’BÈ93&t/ñ®&EíäUÍftS³;’c{nBá¯ùæk#† «`tR £‰tœ¤æÅr>ÑŒêœ062Œ[’jAçê®üE „ëàAC!:*Z2ƒ+vs©—ÒÙªiB’ÖdÞÝÓ:à oŒ—ï1´Á@™D[=ŸÇ8¶xÇØDŽ|LèÖÚ­Þ¥Æê9‹—˜g ÑÙZh)¦ù´þ“;ë ½ahhSðò¨‹*b†ìMîQ6—ì‚—òý1NZ>Æ^þêÉQ…Ø}fO¬Û íŠáÁ.D'`ÿÉSÐ÷ÃÏ ÀÎ ße£ëÛ(„E¸¾Ö²HEk©åjÊ-,vÐÆŸvc¸ËÂlä¯Kùô¸kpvÞ© Æ»ë_f+6€3cº›‹5pe×ax®u#›o÷æ{pöÚuá3yM™3öq8gq bkQo V-7]÷¾ƒ Ü…¾Ån¬”ÃŒúJŒ4ÅŒ{N.tàÒ†šÚÍ;¨ÆÒ0ói¬ëÇJŒ+ï˳Úeá9ïrÁhOáïjKðN9vXÓô™µÎi“΂˜îÐêy² /:±ô5èžyG¦kØs(ö´¶v¾,àÙu5›G ©ûaŽ4}ýB¶6¿36ìâÕ³.3‘Mhó$]–DF_í§²!hµ.Ó×xŽm\äÖï}qŸ¥O[ :O_íu£;¾q1™?[â‹uï÷lBóL]”œ`ƒÉôÃ= áâS¡¡ÍÌ:*šÑ”²l {ÑÆ;’xûžZ ÍٳϕÖAôE—ö>gh?Î8£Ójª«!±ö˜ôtv~WÚ¤œåâT.ôÈiÕ˸bs¸Æß n’‘Öª†ž™g=nZ±ÂÖáð8¬ZeŽ%¹àÒ0šý`˜øTÿc4gD9ƒ¡ƺŽ ƒ!¯¡•Ü¡d7XÞFJBMÔ²þÎ<IåÚKØš'gQ%F†Aæõ¥fâSx!7ÜNîa}6"C¬F³b4© Žz²O6kÕBƒ…Ç?ö <õÿQL{Ë.°¼ÑtJ8¼AÃã:6¯ð=í%˜òÓðŸû’¡‡kb#ó—Î@JL$Ü•l÷-×êá»_~„‡ï{ÐIC’”Éjr‘«ÌШy|¡U£ÒæÑ‚Í®t:üðëOpÿ=TØEï!ƒ›ÆE®’¦“vR}Úµ•S¿iT£:¤9Wjl¿Lýî¹ûþÒ^¬YÍ^›£À߯ðÞíù¤óº»bZ?qâwoƒÙa`,rÿô'þþ/0šHCkø²[+—2¬uKøìÌ¿N¡Éئþþ Üu×ýh5§¬ÔV·¼Ø[F†Fð7^\ÙýI%ñ6Å‹­w"éð<þ½Äm×|÷)W†môÒ_CCç×Â=¡#Û¶†Ï—õ>NÆöÇ¿Àè;ïuYçKD„½ddryŒ<±»Ò'ãý¯ !Ô—èñ—m|'œ¿ª%¡NÁPIp§TëW±íœ™ŒmúŸ¿Ù®Éq°>Ñ Üð!#ëç„‘• ^|½X)ž]lãës%Žh´ÔtDžÍøØÚ°`×§Ðâµwº"%c›5}*”¤ÃÕÀ°ÂÎ÷`¨f¸PUcrÓÈh…¶ÎF“YÝå^çÜ«EüOÚBü˜±¥N\èp :³À´hlsfL…á·ßíó–€…’jçO›±`ºJÍ~妋È]éŸ'9'ŽðµÎTò\44ƒs¾Ç¹×Ë8:¿ó¡pÊ6WÚ§sÄj%̶`æï0ä–;=]öƒ1ããEØ“=¯‰ï2m™hç]"¥j¾íÄ3²B¯Óè‚øÊ¸– IQá¥ÿþ|Ér fÖ&#ܪ9|÷Ð}e~³îõ¡çŽ{åÄl‹fÿ GÞáICko •áI¹€Ÿ$SÔ?–ˆñŠýoM Œ,ÁÎûÌuyxÃïl;¿ÝÔºŽÇ.^*í•Nó0ôIibó7IÕªŠcL4¶¥s§Aÿ›Å¯Æuˆšh 4„1¹‚1d¦K±k,³²&ó_Fsa®“b½`­`dy…EpzÒ'dé^yƒptâ^cÓê`ùœiÐwÄh6÷ñåD”`xÖ–ñ¹Ûd:tªìjM''Š Ž{l,„8µŽ˜mÕ¼éÐ{8›3Â_„ö ²6>WÖ–áoSl|íð0—4Z ö]ºÍã¢*íy’‘ukÔ¦=ñ(Ý3øï@ÑÆ¶fÁtè1ôv9Ëç]WOQ(¨7yÃfrÁØl2ÑÇaY– Ø–á½m©vì¡—IFÖ²nmùÈÈ—¦|é\/›Ñõ fV:ÆÇ(eµl‹á u£Xc“¢ŠñÔ¼é,ÁÍ 7ÃÜ¡fçCqÈ`!ƒBb•xXÿæË6óÇÀ]_ÿÏ©ft㢙ÐeðílIâ n”DEC •Är.w>mO†kå¦rœ tPëÏ¿ŒŒ¬jd„À`öŒŒÐ£Q2ŒÐ×ifÛ¼h¦Ýt0D_®…ídË0{ì¨5Šœ~rê%7ƒÆMš ¶óý·D%è…!áÀøqÎ2ÛÖ%ù\Ó‰…ÓÐÝ& ïͦû­ƒƒÛWø}b’ÎI£Õ88Éï´äŒVBk;?æjî¹>;°ŸÍÓQjDG!3¾$ÜSl4”2Û¶¥•y¶A%'«XpÕÆwiX8£­ ÌÝäŒKü|ÛƒV¢ñ’Õg[+_kXεêuJØì ¿&vj‰U«8õ»çB ò …q9ëóGÏ~õyé4Û?ËfA›þ·¨‘ÀêØéNÃÂqgð»Pñ,ªÇì¬7;V¡2Ó N°Ù'VŸ ð9kðcïr·´¶^IšNëðä–ÃÐá­÷Ý* êL”?äö¶‰“+h¶Ëf«®é¤^¡LñNµñõäòÍ©K”ðÙ}*kú%k:­Cl›žPë©çaÇÑcn½ÀG —@Aq‘pZÞÖôL›šm÷òÙjì ŒyŸ³£÷g]4ÆfO¯ ÇXëÝ-"{¿oÚ÷fþÙ$á0‹ukÃâž©4®b:_ê£/àЙ³¥Œ6iùj‡G2’±í[9Wm½¿ùX›Á¾SêƒxOÉèûãbYSCì™RR™âÀ¼/Á˜¦n²Û“Òg8¤®ž{Ÿ,ÕW$܋РJ«ÓjÊ0+“j´¢ø¶µ#=@¢× rÒØJÎD'½V²ƈßÛêý­†JFÞ­âµÄ»ª’ߤà½%®@—-ëíÊOQm ÃÈdow­™'œ %'øÌeC+¬ª94ê5Lê‚`Hnh%wHîÍÆæçŒfÞ3é‰Ð™›NO…=‡±±qÓé™ÄÆÆ†æ©(›K€5šü!¡ÇpIŒM£ÑæsQ*ŸÑ^ó«Q¨/±œøq¥Â móèÞšÀ»ÿÕëáºfÓêær1ªÀÐè[F÷¡Qõ}ÞLH½Cf¶€°á'¾øp$£òasÚfü΃±õ£Bö굚ZœEÒƒX<§ÈðÖÉIïqn02’Ú¯G2×é´½M>¸åøõÜø—Z§\bsd0$"µÿî8S/2ô²«'ï¢ØhJ QbC6IÃMR{jýŽ€öÕc ÕLJJ…;(4·>Ò¸ag6KÃuè›ÅG®)Vñª _êjAÓ ÿŸ¾y„ý=0®*µo¤´Z¡Ê®S¼IÎæÌ¿”[XóµvMÏpŽ0.*5:·N£B½ã«l`?ƒ †{¤&ø­Ä-%W3Ïu› ,— 7•šÉÿXˤàæƒÕ 7IÍ t?}—$T×ýä¼d0ÜTj’ŒápE”*çX© žß^ÅK«Jn#Ÿá£8·¼\V‰ "Àdú^Ä­`yíõR3¢Róo'wJâ#OºcÄÙ  ÐÉob—0xˆÔLØýTY=†ñƒêÕ€öU·®p-7ÞØ°ªÆÅ«ÚBäœ(@D>üÿúÕõ$vÜÌ`Ø'5£‡º<î<¢Õ$z[RÐÛqì3æÏ\AŒºùˆ‰ŽQeÈ!Ôè8)¼„±¹3ü‚Ô„£½ì…³õK#‰‘R‰ „ûÕ‡ª¡Á¢¤N;€Y æÁ`€[‡ß¬:r3š¤ÝÝ!Çá= †²IÍ “—z–³$¶ µ Sµ8·ã³&UNs-Èm$’[tT´ßu?‘Ð^ggø!©™Ý¸;K®(²BTRm«ÄÀÉõ@§‘~ˆ¥Jd\Ï+ëþ–Èm¾…܆¡xr“x¢`Œ‹¿Ûa?†+\E*ì~šìv?]Q]ÖŠ#»‘ÿ×4ª;Ñt]“À’=ûmþÈmÑ’…fr6"""•IjÒFåĽ ©YGý± ¢¥Ã ÅÐ9FÕIý )'J0½äÉe:˜ÀvE^Åô|æÓJMê.eËøhx q}¿ÌíÚÙ%µ2ä¶x¾@nC†Þ¬8róÒÌ+ü‰ÐžÇ˧*Kó|¼ “ *:ÕöSŒ¯äý­.VÚ;‚ÈSx­ ‹R“61»Î_zá¡Ð»vUfB¯& Ø`½®òÓk‰Ü–.1wK á !·bïÚf?!³!xY¨²4ÿƒ—¶2>¢‰e2i<ë ygZ—uAäíã0Ýo—Qjr)ƒY'átNÜݨžG2âz^Ü3ù{Q„VžÜ–/[F$·þHnaáþ¨ÔüÐ&ãå1•¥Ù“Æð >/ âq/¿3íNxØ.½y¢@¦nÉÈ •Û–5ËrërÓP•vŸ¸Œ¨mRXzª‹T0ÙJJ4¦gVþOÀùñºº·pÐÝ\„a¤§IÓ³/DÜšŒi;âÊ3ôÞêî„…„À¥)_ºÏS¿þ!t5­I¬dìmhëÂ&÷5©‡e!·­«Ícn°[ZBnîæ¦—Î(8†á¨ñh8&¢ÖÖóò%$qÔÉ‘!zZ³Wh!½“ø¬:2¿NŽØî¦»]d¯‘Zv?oY¼ fîêÔï^Ÿ9¦¬Z!Vãe%]×. “`öØÇA«)»VmÂÒðéâå²)·mkÌcníû†à÷”›—Šãg4¤w€¡Db#©±¨¹fZk[g >o™L¯²Ñ 5GiyÁÕ­\zÃ1yièPZ¸eÑfø¡_;ÁÝ-|²h)|´pi%F„F3£Íkׂ/<!Ž'Ƽ ëÛj?ý’|ÝR­vP·É­]ïÁä"¹ñâ[ÉqÙ‡î%¼¼dƒhý-×Öä5sñK1®&øœC x]a+—+ªMPjÞœ¡gÿßÊ qL|Ð¥9|¿v¼<í¯ JŒfIëÅÇÁò—Ÿ…¨ÐP§ŸŒÄwqò˜¸l%ŒG’Ôjäyk"·ëÌcnmzršÜ˜Ó$Çß`Þ¨^™:h…h·_Ó]Œ—m–ðº÷£®ÊM"¢;2lèwÕö¾ã»¢ë M¶)!¤^¹#Qµ}·'M 4 ‹‡ƽ''~g¿þþ~÷u—Í´4äô¤Oaòƒw ËEäuKw¯_[–üy¹9NåCÚ®µäç š¨„øúãEÌZD­’3Í¢Ànyÿ;ÎlÓ˰ŸÝ-ÔHjUâa]e}ΰ֭àÂ7æ‹>[¼ >˜¿Ø¥um¤t) W‹Œ„ ½Î]¿QF¹íݰ\è–¶ì9CBª2î~J^™ŠXpJ¤ŠSYwï¹ß ¢tUœÇK•JÞËéj‹?˜ˆÖõ®ˆçë-êODbÜ7,ï°’’%Ö‡…Ø>±tÑšQq¯PS1lþFál‚ùú@ N/ëóž<@¥ƒ0Ù9ðͪ50óïpúêUa+­©NÝÒ€ðï!­šÃüYˆÍùFÂäTúeÂ0?$³{ñò«ÊÒ</·¹M{ '0®Sx­‡äfPðû¾…—`1 ¸ÎdzußÑL“E©Iëzˆº›Þ 4áe´âÄIvö ˜Ä6tØHE)6oQö6ÿ!³Æd¥Ùø¿0‘Ym""‰£­E*Œ» –ÿ.¾3õÞyû³%„&0Í~Jè?½VÛÏ_öJFD†„ˆ¾—ˆm›‰Ðäê&׃Ñf†¼E„fR¡u”Ь±Ÿ‘¬ÀW_*ò¾ $å ed› ûŸR‡dy%jÇF;uNN6,Z04 Àt>˜\&›YMf䪌„iLÌ.›+ì½E§ ­A…¾¨µòJ±éu˜ŸuÊã™Ñ´v-§CĶxÑ\ì”»¡Ø$û!cEy/*KöæÏl…”ïŠUŽ6ïÓV®\ ‹ŽžñhfÌø{;,Ø¹Ç¥ßæ`Wté¢yØÕ¹¥¾Ü ,ÔdÅë*#aš±Ôyð‘#òêóEÞ÷5ª´#6IM8oR†@ƒöomÝ ÿ·P^K9âÆŒ…ç~ŸáÖì!±-›äÊ1! AŒWa²sâ^ZÄ0 ÌžŒëym\ ƒy½šØ|âårÊyëU$´'íýQV×Cò‹àŠ)®æä@t˜tc³9pÏäïaý¡#!Òô*¨+JÄvÓà`”yVÔV®³R“ /»ø»B0ï°p´ ¥¶Liyß°‚ÿÏÆ÷4¹°…ÞÉ‚–qˆ™á~Ã"/- Ns¯N£qèIY/w=ŠŽŽM¯Ã H-íì9=i œ¾zMXdK„Vr1×óóÝpÏEb[¹dô4ÂãË=˜Ód©,Á.ül8Å‘ñ›dHs¢È[ÇØ!´2À{¶cœ­ðãîJníèÅ¢š!ò¾žû2ÓMŽIÍäN%4‰ª•ËŽž…!õ\ßY0çnxâ—ßÁ`4V£)³k  ¸ZÕ­ >ñ(ÄG„Ã_ §»§Ø²‘Øσ>‡Ë®Ø˜Ô5VdBÐ* Í-ÅÜ„i"6B¼w)Ï@G|ॆç¸È[á{¬¯ì&½Ñh”=Ñ× … ëìy Î_Ÿ-YAؽ¤ßj­v«çÁÛäûî.ïsƒúÃz$5›i&Ŷzé<èÝx"¼IjhT_Ieû›hx—A9pæ¼Éx…¤¹ªLñNó–0%)é{ðRGÄ­EhWCÅÄ©÷ÄBZ»êÄYègãÀ” ƒ¨¼üîGXº÷v/!Øj¼ŒÆÿ¨›ùÞm7ã}zÚü}ûÄ(@ –`œ-7'Ö,_½ óHWÔ‹Jí é¢Ò¼ ÊB‘÷WçËïR%‘Z“ú ÔŠíuF琢4-í¸CR;~錚ô-dž¿z@h% ê4~öË£C÷F +ûÐPÈCb“Dlk—-„žý‡ÊNl¾ÐýLÍJÏVX’ÄŽ{Ü« 4‹ZäI‹s‘ˆ¯ˆoo4s+É'_R£ÑŠ]Ìú¾gŽhRóÔÖ»»œ_±ÁAe¾_“zúßÏBwR§Õ „ö¯j3@rª0ýÉG¡VŒx÷`ôé—­’,íÔ]»lôº¢ò©y“Œ+«ñ ö‹¼Ü ‰ž}MÍL§–þ¬BºˆlpöbÙüäLÜZ­¿Â6àÝm©¥ž´|T3VXšQ„ Hgµ=¯°ú5m§¿ú6¾ùŠS„Fxò¦>ØM•v,/7ÖcWÔ¼WTº|)¿ÿ“ÁÀJ,–xj!9œÄPUMï×41‰”ËO"ó¢¥³ñë=¹ßðr~!<3c.L]½Fè^Y{Q:h&ó•aƒà…Áî¹ö Ÿõ:é'±ˆØ6¬XÝú ‘Çm×goá¨ÓDªJÌÀ0í çDzuw¢÷†Ã?—“µÃCÜŠg߉“0ì³IÂD‘W ¡•ŒÎxêQñÅ×’¬Y³Gl¯Y ú *ÝyàNnz‘ÔÖIÏr•’ñ %& +÷¯H£3>KV¿møŒGAÜZ³·Ü}–Þ‰è…M{`Ú€N.ý>íÌYüéDÈ-,ˆ¬D™,„6uÌÃÐ'¥‰ðÝ;ÀœäóVLĶmõh×{ Û[ª¼èλø7•š0,›ÁH4¦ò¨ŒÙ‚ψ—Q±Ñ(—Ø-]ã0-ßbZλ<æàŽKjwM,vÒçZÆù ÐèÅסçŸëÚ¬»š´Óà§G€Ó“>-%4¤ûîî•DlÛW/ü±±ë!U"Zɉà Nƒë4Ë)ç˜åyãvv)Â9·æŒãG©"~»?ST"O\¾ M_y º9…¥kÚˆh[Õ×Üg¿þú7ojó÷ ŸJö5`y97`ÇZ ±¹á\“!¹BůóBb;fÔhÌæ R{aÕ¢BRŠ£Hꮦ¹ü"îûÕw="9=¾f‡ý¦ãúuhùÚ;Ðá­àZn^™º¤¾>¿ëv8ÿÍ0¢Mk‡/Ù©AtKn {AbˆM£cRSòÄŽÔSË;!±åcxCM  `^™OÃi”;ûWWЫ6Db{Õ5¥àUO¯çò aíɲÊ÷Rv6´ë}hñÚ»ø9§ ™Ñfõn 'O€Ñ:ˆ~QZ:"Ç‚Ü Š ‰m纥˜³Zî~z™Yb©Bc5¿'í@Àð;†‘â¬ÈŽVãow"ª6ÑIJzµÿ"±ÕqöGz%œ`ôÙÎ4èR#ò `àÇ„±3ò™¦/³uª>} ü§wO—Ÿsô‹ êãÏ §¹ËÝݽn´ê1À©åLj^C²/¾Û^¼t@b %'jÙJ~ t€« ¤s`iá°Së²¼®Ô( ‚µd+4xîU8~ér'Df/ —¦|é¡•vi±»Zh(–_±±­_'Ï<`xz_~9$7!òö¡HMxüéÄøÚ Õ‘šà@¹¸Ûð;ÀdÙÚD›ÚŸêßG ³§ô“4GÏýD†Ë^rDl{P±9ÓeHŽB‘§­³ãoª«ˆØÄžÒÔVöÄh4U1=wømuµ1¯?p¢û©œŒ7ŒÐ~ÐmÐ:ÿ<¼}ëy ù£÷àÕ³à‡µ›Ê¸;’ƒØö¢bkÞ} ¶»ŸžÇ>‘u%ˆtDˆŒ–€üƒAò(Œ›¶4U¶3ãu$…ÿºbŽ`ž=u¹‰zhê¿g D€yã¾¼†yó¾w¥¾ú£ÔJ²lbÓV±öGÝ iŸ¾Q¡!PìâæwZRBËE¨›Ü¹a¢°»¡Â8EN6ìßX¢Ø,G›l†ä˜%ò¾h¬4ï9º¡IBRÞC‡—\‘ƒÐÀ±¾0ÎG$GjŒv,Іyº^Ïˇ ß|!œ•@3©­N4ѦTаùŒ|ìŠظB@¬ðÎ ¹*Ò 'B‹¯ÓÑw.cȲt‘ºz(½bÜVÇaº‚\ˆ>RÄ=;dzµÞù&'º¡ÕñÝ':SS°A.=qöŸ>S†ôòèsû4m"kP·’üÒø[Þñùƽ ÕžxÎæÒ‘bKévS餩É"¦~Nøáa\Ç šØþy«^Aˆ¸*Y4¨AÂ5剌î)|Ÿï-KVl)5éw HNë¡ëä©;¦Îh™IDpp¥„&*1Gãc¹Ùpp*îr;˜ÕdÃý*K¯§•5¹Cë”ZulY²ÿv«å¼Ï¶"Ég“wu:Í„Ž’"”7Q`#DÕ¬-¿øìÚ­xkü}óV¨ûÌK•º;"ÅvxËÊ Ë=²¨„ *K²3ÇØ ưM’+ tþí/ç\,™ÊYN3ñ²Ù õyYµ¤F¡vƒxwÃnhûÆ8aŒK‰ˆ|äIxyÚ,ѳ˜yÙבØV•!6†lxFE$¼Äï[•ðî4v)¶[CËýï'¯e˜·®åæ 3ÿÞ!,í S±ÜYÜCÄvdë*hЩ³¼ AÿkÄÓ `åsVª¯@¼ëp±$œ‰ñÒi÷y _~Àç}¦ (ßýaJ—ÈŸüÝ$!Iw0+Ch´¼>ûéÊó‰ØGöÃæ´ TIo ±èá™}aLß^eNwwY.”UkaÚÖíÂâÚ’ÙÌ’4! •«pض¬bÚ‘·²ÐR šÍr1Ї1Žm|OŽüÉÞ1½ðã2fË]øœ?XV?â»? æ%6•EOi&5öx(Équ’„mGç3 .…h ÙçKVÀG — Ÿ›Ô¬ÃÛ´„~ÍR ¹z5 ²?ûSP™ç/À¦#é°àެã‹ß‰•¸=’Û³¡0/‡™GþÊr”êVòP*v™ÇxüÝ«â<ŒñÑj÷†2¤÷§”„Ä©˜dgk&aÔ«0î~ /«6æ QˆÀ{gâon÷ÊÁ+’uEë% ãP2™wăyéRY_.[–®¶@ÑÙ"pr ®Õj7GÖ'OI¥ö¼ ø6ݬ0‰– é`èmç¶¿¨’ˆŒ2ì/ r/­™osá!æ.é‹dþ.DEã€ïá;—°ä÷!o¿DÞ{æÏ`MßYëT?:}éø$¶ƒª¯Ç­öêÉ/?Ž†Ç•Œ§%Äb˜nÙ›¨ä4S÷!Ù¢àH%Ö„Ùi!+í¦ uP[Ý9ÈDmÐûÂ|[lÝd¡Oz!KýÄÆðZW‡ªÂ¹iZ!i&ÒMµF ©)yÎSüxŸ bë5Þä")6 ƒá·¤æ+K£hl-¶naòàÒ±#\² †ŸBëk/W¿1ÄÕkÈ%Ë`ø­Ró¹WBÅ–ÐXèŠ^fÅÆ`°R󑾨 Øbë6ÞQÉ`ø›RóÙ:Š­~#áÓåã¬Ø ¿!5û”àl“ÐHx“+Ll †¿(5̊Íd0ÀÕS™\⠆ϓš?¼%[bŠð‘‰ÁðmhýçU‘Ø’R ªv"—:ƒÁJÍǾô5Vl +5_"¶¨Z  ¼åšËl’ †»JÍ/—q™ &¡©pDÝõ3Ç“*­N·žM’Áp_©åûç«£bkÐ"kÔSLŠtA!Ÿ±I2n’ µÔr¢”äej [DúŠ(Œ¬ßÜÏ&É`¸Ij›G÷þÚ¯s€ÆØ4Cbó®bÓ¿ÅæÈ`HÓý$ ebó±i4šâcŸ¾ÿ›#ƒ!©¡Z[dÓøñBWÔ;ÄQ«~›"ƒ!­Rƒ-£ûÜ‚—Yþ®ØˆØÂªÕö”Fƒˆjub¾üD>›"ƒ!U­*‡ÎÓWSWtçŠ.¥íœs'åkMtú'&ŒoÆ&È`ÈLjVäö¼|Ž!Ì_‰í2[¶ÄF‹…s4 Õë×EDJ÷ò96?äf÷®¤¤ÓÞ®ÓjÚh4š`«‘1rŒ"X]M&S~±Ñ´³Ðhœ™]X<ãÉfNs1 ç„Ú÷‡Òo Ðý7@«m¤þFœ½{û»Š+2šç¿öH“†³9C ƒ¡J¡öñ®ÔØêaÁ?éµÃÔ ƒám¡f2ºÞÖrÆðuaÍ›  ƒáU¡fd_Qžiô¹½Wx3ƒÁ`0¼/ÔxÃçÅ¥k/bvÏÁ`0 †…­ÃÛ s£Åð+¡Ê6ϰ %!i%^úJÝשYOr®2ü¾^%6ˆGr?ÆIå ¬[K}N¨™O&p¦õâ¦ËcB³Ú«¦>y¸™aF¡„qqv2¥ýá)iÛ3Io4ò5%XªÁhö‚¯Óh@«Ñøô»–@ã¥ç:SãY¦1 ëBMéÓ;&V2X¦›£ƒ¡il$´Œ†ª!ÁÂߎ]¹ßí=aQ`4|^ *:}&õÛ_JBR2^úYBK µ0ù¨I KÍÊXÈ´Î`0|J¨ñÎ6yÅ5ô$ÌBô:HŠŒ€ÖU¢!12\9³…˜à@X¶b\-,‚¡7 €ªññPl0p>{F:ç@aÖ/ï‘hÏZ*£È ÃׄšÛGH±È+“4…©Åæ±VXвH‰‰„°½è8t: ‚ì+WaúÜÙ ƒúÞUbãüB°)­@ÕbÞ(ÐÀËwdB\p ƒáKBÍÄ‹ÖÅ5Ú¦Š¢Ì2…I£`Íb#¡u| T vë1z=Da4ÒIŸsrr`úœYý{õFØŠŠ‹…{].á¢5l&@—Mô‘KŒÁ`0|P¨Qc¤qnÛ§ß6Ú%S˜ ¢" ]ÕXa S¯•vvI§ÕB\xx]¨Óé ;;fΛ Q‘‘pS¯>W †b¶`©t¸2Wòä'Š´*xÙ‰¡6— ƒÁ`ø¨P3ñ!96íb£ E¶€a¡Ð¦J 4‚p'¦0ÝAíØA$Tq$Ørrà¯ùsÁÖ[\\< 6ž•Ë”:Úܤ~õ~a‘Æ`0>.ÔhfGãç™`6T˜ .8ZÄE ÂÌÝ)LwP/>yè(l³P°EFD@ß^}!Þ›+ÚÉ¥ib“rW§£} ÂË ¦0ƒÁðq¡æO#jÂÆ Ëf¨^ÉÑáСjœ,S˜î ~|<¦G[é}$Ørrsa΂¹Â¦ƒ>=û‚­˜§D%wF—éÜÉ¥Ê`0~ Ô|yDÍ<…iDOÝðPhW5šÅF9µ Ó¨¡A_T,Êù­ Ørr`. ¶ˆHèÕ³·_°Ù,{ æ,…sp¸™ %!‰ü µcúªX8  †Ï 5“<]2ZFíj|H ´Š†¶Ub½:…é*ªGG  N\¾âÔ)Ô Ô IDAT涘¿pDDD@ïž} 66Ž×°¹aS ­´Ó³ªÌϸŠa†9v£ž?•ÁG1²všãÿsCñaBÀ윚¦WhtŽ13Z®¹ò(¤feð2s_jjQ3X¦0ÃôzhªÇAR”}G²jBThÔ‹…£/‰šµ%ØrssÁFn=zõèÑÑ1~+ØÄ°˜-«QêFLk&-P¦èc¸ É?éÑ_Å‚°Q%CŒ%Da·ˆ†‹pa–ÎG¤åZò9Âèï¡–@i´—òX†v;PáyK‡÷À@éìŠ! ƒÕhB®+q–ÿê,†ýÖb Ê÷`¾ðz5 5¥!e"T•uÃC Kxhç¹]˜žoˆ5ШF5X{0Í­xJÜzÌ[0[4ôìÑ[lì8Wœ˜£“ üìÔŽO‘Ð_dkð 1FŽ‘{‘(ÇÐC"¨Ló™Žn‹¡?xÎuuK g¿o%æHÀýŠa"ÖóS\ ”‰6õ`籬²BÍù5Ï·\”ÆØàx®U#ŸgåÑ´v-jpÐ,ØnÀ|A°EA÷î½ýz„M´¥›üÊy 1Ã8.uŸgð2COÎ YðÓ^ë°ä‘x{‰‚E¸Ñ²†§Q´ä’sºœ'âå)©¥‰ëlfMù?¨b×§SH‚uþ³V¸I­š ”TÝÜ’¶… ç ‚­ ¶¨èhljÉŽíX‹„}ƒKÝ'…à snȚϭðòQ·Š’=’¦ý4^oÁúÿ7—d¥åLS×d(ç¿@£š™nÓ#T¥› ¼ÑLiÊ=—Dd]Ïw„ç[7‚ ?l ñqç¯ßÀò“øô‹`[$¶hèÚ­'^y„­‚í›üêŽÓ\â>ר¼…—w9'dÍãÆ`^VOůQÃV|—Íxí‚-‡KÖfY“ßÊ íîrÚ2ó|™£›´‚ Áä…`ë¹Z”o§ròáƒíáZ¡ïo>‹ ƒÄªäM>—«%S¢‹ÍÃ0®ß¸¶t·£\AXû¥¢à'àÅÆ>ÔëÇÆ"Mö|ž‚—ƒ*iÖè‚á<¾»þ©XÖß‚y£•”"m%†èÊDAUoÉ'í¥‚x{*¼ØºTU¡ë ±(2 ft´GJ§D°-]<"#£ s·^ÂH›ß°™”»ó“Á°Ó $ãe˜wj2d@“„¤`lŠhQ'|=ÚY»í¨+ ˆí\Ÿ’È Ò0o¶‘ Ô°Þ…ù;CìôJ>&Ç64Sl€÷þ9ϵJ†„È0Ÿ0ˆ“—/ìí;aúÖípäì9ጠ@PAzÏmž0;Î͆eÁÖ©kOˆòs·¬Ó*iÿ€Ù¥C‘F QæyP¤‘¿ŽB ÑZs(‡‹kÐ3æ =µD1qÉëÓ-x™)q^S'ªŸ³ëõjmŒÈ‡ÚÇ»Ãù‹`ÝÚ•0´YcøùчU‘vrÌ»)íü¶q ¬ýŒ—û%Žv –íW~¨È“ œIO•¸xuû]põÊ%Øsú ´¬YCq…~îÚ5˜óÏ.˜¶u™¢§&‚|¯½çÌHiWÓ m14”(®-ØJu?•!)‰ZÄ@G‰ü|‘ßµŽ`^Dïõè0»X‘R@¼Še2ÞÝHô&#vj|àTvÚŠ‘zñ¬;uz×®ê±ç^D5oç.øsË6Øwâ”0 ¨×•ºÔ°7ZF‚¬°¸Xø{×äpo·Nзi „ؘö|¸Ww7g!Þ«õj—¶ÕKÍ‚­]§î>=%*ìPõŸ¹O>ÇS½¸ÌG+Io±‘yŒ³·IÆ5Nj‘V4eIŽ[¸O_j–#ÓH Ý!a´äD¸;ÖŸ£RD¦ŠCÙŦv…þ•~šÅEA• YÒ²#ëLÝ´ïÙ—²³!ÅKÉH}¶·y—FËèè&:Ãsd»6pGç\½š¨g>Ô³›0mšuþèuZ¯—G‰`[»b!„GDAÛNÝP°ùÞ.Q?K{ ë}¦‹|:5@Â;Ë:ÁV#ÓÇ$Çù–O`žÃ9\Rºˆ–3¡XvEXŸÁŸ¹U”oן¤:`žÆ¯%a´’Åg™úô‘& …Ú¢"øß x¹m·h¿’“ ví?6ÿ »ŸD!b,3ZlgÑ?‰ß‚âb ÐCǤ¸¯[g¸©YS rM@[*ðZ(ñbítÓ„¤Ú&óš§GTø ½(X„Û?ÂwÚçEcLIh¨ÃËü|“Dqnó(Ú5o½”eDÍ}¤èµ˜yä$œË-€gZ6ô^%?uZ1›¿s°û”a×:ðÍFù[²Ó´oÓÆðhŸžÐ-Ùœþm™™pÛÄ)¦­F]nV·Ù7`+íІ¦í-#l YÃægBím$¢qÜû5ò9 Äã@VÑÃPìsÓßAº5Pž­·Û‹ïq¯ö«ô=¨¼Ý±©°á@n¤Ÿ;߯]³·ï‚K99Â(™ÖÁS%›hWj—†I0¦_/è“ÒĦë˜k_{}2n èÓjµª+a—( ¶¿Wš[J».áý)Q?ÛL †¿#œ³À…Ž·y¤e(}¶¸Û O]¾ MãøNj%¤òSâ¥õ€1_fà#îLÍJ÷øÔÀÿ¦whdíÐÕ0fÍøo—PUâ㦲.\„Ÿ7l‚™ÿç®ÝÖ•• 3;;NI˜Ñt'dðXߞпYÓR÷!•!±jUØñÞpû¤)°=ó¨ÝÔ"ض­^,L‰6iÛBP°™¼$ØxÚ“ágˆç,p[´‘§û_,¢<®ß…6К6J^ãÿ0í4‚D^öyéÁ¿…­ÂPÌ›¡˜/k<ªYüu­´Å÷µÂbA¬=ß:ºÔp£N]¹S7m…ß6n…3W¯ ‹ÿKü²ÙówF»Miä¬MýºðŸÞ=`H«Â¨Ë]áà`XòâXøtÑR¿p©jÅšP6–)ÑíkCX ¶v(ØÂ#Àh0:tô+µ-óÉ %-Ó€Õ!WÂ(kq®J*ÚhsÆ–P l話©fxÁ|íN¬ æ¤Jt0Ìk’4è yŠ—aùêZeHþïÀ2.d¡æ!Á6þŸÃзÎx¦•¸ukodÃo›6 ÂìèÅK¨Ó•Ž~…Ø9Ñ€F̨Ño^§ü_¯îpK»6vïu/ ·ul·LøN^¾"ˆF5 ¶¼œðÏjlQÑИFØ<¸K”ý¨1” l@Ø`H¹ çªG IáŸÊîMIh@;ÆÈµD‹ KÁÐÄ"ì1ʘÔØx¯Ÿs©Uùº»Žõo˜'vÎê¹BK×iaÍÉ °ëÂUx¿s3¨^öè¼+9¹0ãïmðÓúM~î赺ÒÃÑí5E#fä$¹z5fÝà¶í 2$Ä#ïS?>v¾ÿü¾y+¼ðÇLaJUm Ê6ÁyÙ7`‡E°5jS2Â&¯`ãºÁPNIWlxª`ÃÃ.Q#êÒ‹@pS#„¿m‹¹$Z»CþÔÞiOªúþÿÏh—¹4*€ò}9æÏB¼Þ*çèšžýD™¡Cs£°H˜ ‘XB®žƒ)«×ÁÁÓgͲ[FÌìM)’(#q–P%ìÑFwjqáÞ]›{w—NB¿`1|¶x¹Ù›¶kiJ4 ’Ûvà°0ÉäÖƒÝs0T)˜ÒÈM? r¶ªIÌ ëȨÌþDÑ@»Wch*Qôäƒ6Jüê#ÙuÌk1¥Üzf]×@ad²šÖ,/ÌòPØUŒ€göƒÝ¼ 'O€íãÞ€ÇûõöºH³Æ+ÃÃLÛ[#…IP¬0eN­0%š »Ö.½ë—CA^.hð;¹l‚ÁP0vÉ0‚âÉ®Å\„’жó&0µÓªN ·dÏØ?tä­ÅÌ8n][„u‡FØ‚¥N¸b§>½>Ðg0BÍ)P«aS8™vŽ9 8™­wtjõìµbbTe¥öé%„=ÇOÀ‹ÎvˆÒ:9µN‹–¬aÛµf1„FFCr›.,á”(ŸLÀPèx :&¨ºDñµÁð†§åJp“„¤Xdœ‡ðã“ê)=ƒ±á¥5+=Á¼ÌUB œ(d?»ö`Vf1¦ùuü(ÕY” Rš·Ð¤îÛ«V2¤M]1ïÏXÄìYá’ÞÀë{?‹œìÞÀ¸oÇçÌ•N¨)4ë•,¡‘ÆP%±1ÜÖ£;¼Ô&Ù'˜½eÝ:°üåç„ÏË÷€ç/†½'N–Ù­êmЈ%üÑ´sei¢)Ñ|l»×™w‰&µé !aî;Îe‰ÆPÁÊ%lÖâÇ;$Œö)Œ“ü€Ñº·{=Ów7Å æÅðjM•ýÁÝžù1¼î‘k4{°í¢µ†RœqÙ94­¤ÆÑÏÏцŸ·SwÞGÛ¤)ÝõwhßK½¤ ÜÞ™­WêDJ´~mé °öÄy¸«Q¸§q=ðôoÞT„c/ÁOë7Â_ÛvÀÙk×ËlšCˆÑš¾ðà hP­*œ¿~.gç@ëúuàæ6­a`ËfˆåÑ aÔ¤)pìÒeád‡‚Í2¶w݈‚­»@PX¸ËkØxê“¡ü"±P#Œ †›Ix}›ëN3y"ßaÔl«öÌ%(ÈyµMhxÀaªÆd*Ä4û³´ú¢Ýn¯¤£s/õÑfi#Æ[§¡˜×®‘¿Üjêm<=-E‚mZÚ øíÐ1ݰC+l ( \T u5,4úA£jU\:ï’F,Ž«!oM7"‘žéãfD;\;£-ýÌ5J¨S©èćodˆžêêelÿq¨àyW›|0 W' âë5„‹ÇŽÀö´ý0ú«ï„Ú _óx¨G7èÜ QÕG;U†#gÏÁ²}`Ñ®½°ïä)È.(€Z1ÑðÙ]£ vl ¬I=ówÎyC›´<ŸTØt} nX!‘1P¯e'°L‰rý`¨°a¡µ/)Hþ_ãõq%¦ïfaøÓXº:Ý✕F,:*^¬ee!ŒÂ4“û:¶IïcæƒbDódjV:_Åzõ–ûw`Þ´#õtó·7­ìϹXQ¨qþË/ذÁ%ÁV?.d¥ÁÙ#û¡EÜÒ=ûañî}‚8!¡Ö ZÒª ÅмNmÅ8Ÿƒ‚¢"8Œ‚ŒF פN?¸œK3–‚ÿ3ëw!‘z-7þßψU‰ £«½³T=e„íàú% µ›w€°¸jlÀ 57,äd•Ü´ôR2è ñ°jÏ=™a²“Î|¼t´’ð¹C%yK~±þÂ4¿Š×@Þݶ²7QTF`ö—gàšã°Üé$xËtèÃGß ÃÚl€Ï™TV¨±Ró ų`«‡‚íèaa y"+''.])«ÖÁä•k¡È`üŽ…BbÕxhŸP:6H„–ukCØXáè'OâJNŽà7õÔØ}ü8ì=~RpÍq—H8¸^«ëA°Ê|ž‘8U"» ‡¿ß¸G¶¬„Àà06\†šZ”Þ É?Ì;ï“ù‘'1|‰á{|öU'Óz'¦“ü³½£¢üý/bºi ëSPϹ˜$ÈH¿ïÉ5Åérÿ?,sr²C¤ÄÑOĸi$¼'Ýjj¼ Çó5ÄX ±u“ ަDQ°ËHµL}jJL žv6ê„©·Œs„0mëváß$à F“ tZ°†+"8bÂB 6<bBC &oļ^¡°÷£]¢0t³.:ë3Ö"2Ê"|ˆï°ZÆôPC5ÔINÇ4ÍQ° ‘K šÞ¢Ñ·V‚ù´r·ìfôdk—0Å@Ós1lµä‰b[Æ”Ä!Øp“  —ˆÇfâûõBÙR‘c70­:¦é;k‘•"d Ò.d‚ ™A˜Ôp±°Pc0 †?ƒ7(&0MW/âë7‚‹GÓP°•e0 ƒáoBw(O²a™ÄÖmqõ“áR©`ÓrÆ0 ƒágàÖ_¹rÍìÖ[ãÞ7Cl½†(àŒœ- ƒÁ`°Pc(J°¡@‹¯ßØ,ØP¸‰9Lœ!Uî3 ƒá=ð5U 6Ä%4‚øÄ&p1ë \Ê:L Þ9k ƒÁðY¡ÆJM}’Íd„Øú!®¾Y°]>v˜×°1 ƒáƒàÖ]½jMlq !¹÷ÍS2%ÊÊ›Á`0 ŸŸLP© R`ÃKlýdA´]Ì8WŽέdøAù3 ÷…7D>ÒN›ÌS¢q‰)‹árF*\9Á‚Á`0 U 5Öi>&,-SŸ±I)‡áR& ¶ãé¼é€Á`0 j )6a40Æ2Âv)ã\=‘Î#l ƒÁ`¨<Ìâ‚N:Hl ‰½F@T­DöÃ&ÖÁ}ƒÁ`xO¨a¾Ñd2oäàËÁ|–(°%ô΂M”Rƒ´^¯Ã?“ê$R&Q"D&óÍò×J¤Üh¶›3±•h•†ã’Ø~¼•oíØ¿1N«ÌNIø«+1$NŽ­ùÿ÷S§Àƒ÷?h¯ˆŽ©0Õ~é“[;ØýÈÔéSa̘qø1Æi"ÚZ‰`IÊ­øÉõ»VɬYÓØŒU˜ÌuÙlæO¤ÜhÄ„j³…ŠÏ‡6ϼÌþÝ):ªæþ¢y3ØÌU8x·V"•˜+[~ð×mùôKײœøü#ö¹%/>S«D+ÍÆg+ÕMæO‹ÈÇ'¿ø¸æKil%±v騲yOpIˆG.Û\óqKiNrWkÚ¸|>Ta‰ª¸÷x[W•ÉlWÈ ¯sÏ’`Ú«o@þoÕ”´Ó€µpT«ƒ¼}Á§}áÄ6ApWçNuîG?÷ v%73™Þox᪻zÖí»Œf'(FþòØ5xzêËNû®üu‹ùeB݆#6sœÐ{¨¨ÎRkÛs åv=‡¸˜#Ž£{ØO(¦×¼‚'ê6k­hExlãòZÿKƒ}Ï×€J®[o©±q ñ’b¹¹¿¤8 ïMÄ?¿Å¿E×-¾ËX¿§Ç9xÍ–³$zKŽrL9³m¡øüQÛû¶ß°ü®µ¾Áó±Z-“/#t»ÏÛ»ÒoÖ}×1GEƇ/;)¥Ñž\ôV‰[×Ö%ã©…‘–„ôbZŒ{VX€¿³ÏÔ©ãE%Ö·5ÝZ'rXõãb5¼êWJ«Þ'²Xõv‘Òª·«´¨$¾aÅe¦j›ÐvoÞÀýß씄¿ºÞ»%£æï ½{ÀÃßM†‡Èªw¥º„Xõ·¥§‚F£Ÿ8«^tu•ÛXõ|ÔñµU·}L¨Úª7ó²ÝDl­òG×äÔüÖò‰ç!â©!÷ÝÞ&´pî ‡}êª4Õm1O¾ïß;Î}ý)¿ÖÄV,œ ý†Ý-¬ºø,ú^øòC=»³¿7›ô,„ciNØÛ”ÐÚ%sx-ú:ÝÊ]K7ñ6þógNÂþ›àâ7ÿ©¹W‚NQÏwþÏO t½c$û÷‚¡·jxKBV=‡´‡•ÿø{­ÆrNÑËCn½N[Sºœ•ó±Ú”Ä™U_´}=½ûzûg®\aŸ|§/_©é"N~ù éȪ§_¢³zAÜßÞ†ŠÒ«Nl #[عEß¶s/ð –ߪo“‰ ÉoÕ‡gö½_ øDÐÏ«b@ø¿ˆÎ”P@½Ð¶Çà@P©^„¶ÑA+~Æ™M%Ö>ÓóÜl-‹!á̱g½[ßçû†ÅLÕZ[ôvLѶ6Ö»åþ8«ñÿsx2¶Ô'ǬؖÆÛ±u§Y}Lÿ¿Û*‰Ü3Ãj!‹ÞÂ<ß[åȪç«:r¹{?×é…í|h«Å:·©Ž;𲂂EÜ­ûíÈ컟RXÜ5P¯ÛêªÁ-i£±ëaóÑ…ÅÂòÏ}HcÝ»à)XçLá#) IÔZ¦w•¹î ߥ‘«ÙN¨ÛAAäñ†w„Ï¢WVìYlö%ºŽâ³NæÙ»VNÑóÁÿ~ÿyàa‡Ã-B¤Éˆ-H…ÉäT dâ=×1¢‚fà…A·Áäõaò´ŸX›ÜØ\–®è‚Øsæ-ôhZ;hß¼© ÄCjyJÓgNc 4úžqlÎ 0f_"fÇù*¯ˆµ…ÿÛ7Ëî3a?~”UV‚^:­¶VæÍ™ÎhبûwÉŒ£H-=¼z› /§6k/g&ÃÇËWÁVþVíâ·‚¥/=SçYË “a`àˆ1N1„Õ[8õ¶®I‚ rßÊ­‚%žotI³û{³‰ÏB€¯;>d-{êƒÎ¼Ý¡Q,ÈÌ;º /Èèe›Eá‘‚(‹‡ÝZ«IU¼á G’±.P÷A£ë49*Èü!Ý…äÎ%›\êX¾éß"ü}=Û|Òsàïcàý͈ÒÓ“ô°@Y·´þ©+LxA-ü£^úïê]°ñ•g\~ÿZy9$¿ü:Û)XúÝŒ#Ø‚¬ÑS¸­ål(Ó$v‚ þ€ÃÛ‡òëWÙžëÙÛÀ¤þ} e“`0¢žÚsì¼µp lå±3J¬¨‹øÌûx}M`Z¼am½«bµ¡&bÞ¸ˆ =b¦ôœôþFLñ_Žc^V8‘º0Í.*c%Åb"X%Fí>¹­Z¥QfUåM°Éèr;À^†ÏÅç(Ä£åž7à=£V²~…©•0µù {ÆûTUÖ¸°*ÄQï å®:«gªxçØXÓôC‡ïE³ù[OuÍþ1q1·º¤g>Â8 Á¸›®U“MŽ?(#“¯21"[–üä§Ó°Wù¼a4ÌûUŒY¶Ôg|DT¯eDi˜49Uˆ³ª·xкZÆ3ÍŽ· 4Á^«ñ®ùÂv›–V¦r0n-6J­ÄmXîFfG"u›–«Y fjÐj=„Z þÚ)™ý{ôÔÙÐ>5Í“¯o‹j ½"ZÔº¿uÇv0cO˜ž’*¶`„Õ|Þíš>:ûM(g×ÖMMNqŸDhÓbN:*NAod;o2°ÖcSÛ…¡q¤¤7ÄI4‚Æ.>ëÙI¸Ä°ó ‚íܽ“U¸RÆn¯åHŠíÁ‚ý}ÙoZhÏž]ì †ÄÄÉ:d-¿D'ö˜~_yôŒ ž>•<³(òöî†÷;]íeË¢ÁîL±/,9 }Û´_;gÑRøvÍzöï[âÛANqIgòórÙœ˜ø$¹À.lúÆ3ëwÂwýj/êúÏwá䟗٦óéýc`Ì-Õ¿ÛÍGɘ°ÙÅÄ'Êc¢1µ¬EmÉ-¯ª‚¢OÞ«5Û¨¬²ÊîÀçü=`Š‹‰K’¾ûj3Ž]±þX±€=Á³ŠÔBC:¶‡µûØýýPþ^Vi¶u±0v»_¡Ñju ðþ’åðÕêu¬þÐ8q Š òØ&åBaìÄ,*:=aÍv˜Ü¿6V¾þm¼·x9‹´©'@ÿ´˜¹5^š1Çá·Jìc#K‘í¤hZâÂì×*Œ5³§oÚ ¯ÌšÇÞÿâÁ±pWçÌšçî½¥ <ñãtðq<¡àȶ™Y†‘#ÖÔéö‘°tØ­NŸ£Âxä‡ê‘ƒðøhS]˜eÃ{ kÕgö†N£…!K6òaÝ=ߪiXöÒ³uîÏܺ½æïÓE¬5Õ":^œ­e¶Zÿâ ›Ðl´x´zòQ•ðÐw“aKQ1ûÞê}ûkývº(Î9$®iÝ6Y„­ žDÀÏ_³5û£›7ƒßt‰k4é/^¿ÎÖúûØESá…†ŸN~ñ±ð)åýç­¯WA…Y°v¡d~‡½‚ÈÒ´+6™ŒÔw¸ìQ­½•’Íf$ö½Óý©¯Dìuñ}ît³DyØŒ…‰ë}§;%"“db{Ýé6Œ”02`Å31.†1™Ädó˜¾ÝĬ|tåŸÙl„è^ÃD$¦ïp£¨‚pÔ\î.“AÉ)LõsCK7ßÝÇ*©¤’J ‚\™œÁýYn™ù&'Y¥çxr&÷ð(¼¼eÏõ¥M¥¬]îJÛYrCeŸRñzW×Ç+´ #ÖM‰ÖêkëÙžËñâk§yÚNÚ¤ˆm4a >7A`Zݬ¾½Å:ã»êYÆ^!xžÏS¢ñø[¾€÷©R6s·zoO…ðUÚ¾´œIä7‘Â)ùü3˜±ä,.c4{Tç «0U =°W"ÛÕΙ±?>çÃ¥GDVvXM^Öbaì©Ë;Çð™ö ’‡?v®'\"…<„éМ_š‰Mþî—ÈQ6íß™ÅÑ6%6 JŠx "õó0ˆöZ]Ì5›1MSS=KL0F\éÁÞBnÍuMíuÕ<{ÓPp½-¥*ˆu“üòi¡º¹ù¬HSé!«o,”¬ XK;ynG9Xý`}Ÿä[Ú³ì¤7Õê¿Ã±0~.MÁü©°¸K€A¿M‰óP…L€¿VaL_ »_õKQ‰×[ýî,À}ñ±ÙòãÁâÝZ¦£.dìž7ЯÊ)Íæp¹&Ì3 TPf†µ©²(T™å›øßP»3³È]D"D0 …dHc@ˆÔÖšÌQî¢+¥‚Nq]£|(ÈZ±“÷ê-Oé†K»_›VÐ#¼¼µv蚆*N?É*OêZ½• ÷Æ·å]ÁuúX ,Þ¹néÜ2RÓêL‰ËÈ‹÷èšþlÐÃI1N·â´PVL4,Ùµ¶íÜ[wä@WLjrªÇ:M‹ d @É»R|Ãb"ê,¾C=’j¦ÚÒ’ŽÌ6LvV6$'¥x¬Ë’U f«µ•®èKz•¦Ëg4ÁÖípñ¨XJo UF#ønÎâ"ÁкÍí(œÌÌΜ˜êöࣼqa%I-àËbÖº”ïm ìÞ½v¡p:tÌ„$ìÊÜ¥üÅ&£×e™ëÅ´H÷ñßwÊVø„V-ÈK{÷ì†Ù3§ÁûÙ5WbÎäcZ#ïÍ2ú!®.Q²­”Ç×ï‚Õ¿­„¢·ÿ^¯o-رžž:CÔ2 KöíÍ…<Nz‡LˆKH– 1b¿*ÚÊ’ÊÆê×w ,)9Ãb#=OëÅÆ?~Í˯µñ,mRûÄ€>ðöè»Øÿ·xü¹ZzÄ™`hið¾=» µ}'ˆe×:3’vK² DìúPg4·è8 ˆjþúºs4Vì̓I“§±›O[vÍ®2™ "4¦?ñdDµáýæìg&ÁßüO4j öåÂþ¼ÝœÞ]„.`dˆã.ËØ?²f;|ß»<5u:,F?Âzyè lýt¨ÜÇcïü½¾)ÉÐÄÏ®–—»ÔÌß öíÄ´ЖSOÉÈÛe™Ú/¤¾Ò†\`bÒ eÑaøyÒ#Ý.Ö¥oýkÁbø|ÕZ»‹ýÅæÐþ½P˜¿âS3 *.ÙeS^„Тr™¬E Ëì|÷M4ÏÒ¿Wü ï,ZVƒ*B( cæS‰ · *Ú …—ÚAôrzW"ª½^¾ÙåõÓB3ÿ|ÇèÆû;VòÊ̹µ”6mÜó¿Ç†!3xßIõ ¸|£L"Ÿ‚Ø” ˆˆMS¦Â¹ƒ»'É"‘Ë6ŸÁ\É& Ñ.J? ìsrv°{(øêo¢†”üçŽe÷WJt@cÚ+ÿ¬wWf-˜è¤öÜ6NOi/\0ôVy2|é&YbK—ÏŸ=[ÖÃûcFÂÄ~½ëý½ÒŠ ˆ{áo5;HI!˜(Lëû‚aìœÂ!‰@†.ÞèV°ˆ@wúÛ~™ÓDúÃ'É©üeK»€½iÂCšÂ7ãÇÁŸ¥¥p[zõÞg¶›…`Ú$¦Õl?ac˜.ÞC °‡\!"š»< ˜ûõ%P‹'ž¯Õ•Z™n»•Fáržã]$±²ÌŒûím¼öü±þ9¸?<Ö§—[ÓŸ¿c'<>e:‹([aX¬²SEùpêÐ>h]W0Ò#dài¶‘‚4Zm[ å×®°á“Q]²àíQ#ØMa\¥}'NÂGËVÁŠ=ylféÆp1†‡ˆ ‹N(Üóò$¹¢ ñj“b²z–¶ÊYËsóX¶í>ÍŒ¹zL.ïäØRE[²øÈǦõ»ì ã÷//–Ï1äï²<+$í/@‚Án¤Sqýªu}°Û.é¼h=¥^•;©‡6U!ÁÐÎJ%ÛÖAåkàäUJ] d mçÞ¬Ž9¶}$Ç®yB/Ô1‘™=YÁß¾V–ãã</ìh›È¬jÄÏY#øÔ8éÌAœ¯%Úæ)2««cN bÜ%˜Ià„ˆçEÙ(GåÜSËl6›!C{wüeFÓ¨xðoÞúÙC¢n³Ö†ãå4¢®ì$veÆò’}Sgð6Ýn§î2d˘~WdˆphÁÿ$ähoŒV§‡£,cuŽ8¡ß&¡“ ~¡-‹ÍÆÊoPÇ@%•TRI%•TRI%%™ár'Gfñ76·Wï/)ØP*ÑÕCŸí†Nðƒ}𲮞ù²>ÌÜÆÁ¯C& vü!Æ C%‰ˆòžßè¼²ÑØê/(»»ˆ×!ƒ¡úDqïßc» =J‰£Xl?ü³'÷.Åèi«ßðÇ<%¾íD÷c†úx¨?ä*F Õéc:øþðò¬€|Xþ4b]$,ßàßÎ2Ó9`/ü^æ!a¼„—Ü©|9*ç„HSVÂm-÷ž·íe}Ò§4ÚÚܾŒß ­¥Ô*bY) [$Ã/Œs˜áVv xììPÉóìXžÛÿÂo¿)Pxß!OpQ4w•ï´Š¾˜þïu¬,ê,pWJÚ”5C `b¼f°=a¸@|»7ÓŽoÚIW#*'âå[›ÛUø}»fo=h•„ Éä¹·\ªcìຌh+Ó^Ã#cæe|ïÇzã<^l×V<ˆßüÙ¡ÂC;ð¥l7÷Z|Æ…¤SF°L1h•a™™ÃP{Ûc>¢Šœ‚•:¯•ø®¯HaØtžSÇÐ<á¹×MêDö—y«uEÒº‡ßmnûàý(”d ²°¿u¿Âë*È¥˜Çs/š‹"ØVÝ?'T¡sïTÙð1;(ZoG¸rpoWî[¶»°7åv‘î©X`ÆÉ÷à›»Î²§·ÕÞÞ„&±;Õàz Gá·>±#¼¯yn_YÚO<‡û»—ƳÓº.¥R—Z(ñ˜AÒÁ+˜>xJà·Ûã·+ ú, ½À…ŽœÑu|?¬že£rÑÖýt6«õÞì\#„ϬԊP°õ‰Ø’ƒŒ7á`në“P@òËNöÈäÙ>÷nªÿH1sߦ…ïgœä›-ÈâÒ –°ÑeóX^D+H0.+ïï ŠÂ‚}ôj5ÐßdfšBã&^«¹hb˜U—ʪþödzâ—?äÊK³ÉÑi4ÙJ«¥lÄo43ëÆÆÅös‹@f—\Ôj4Í<kQ8a¯qô¾øØYòÓÁâ7|tÚy{e¹«ñT˜ÌŸ÷_Ù2ý¼GU44”ÑÍ$ÄŠr-›½oæäù˜µcÙ˜4pç‹_•!ÿ·» ¼e Ï)e‚qZ·(|ž$N—Vý=+­Tr„Ðê)³YÛàyS§§™ìÐæe&µÇrÒhëÞ“m3~ö\÷®Cä(ØùN‚bf<¸ —ñLeJBrmËêÉ6¨oD'ì€L!“W5{eˆ„xÉÆ’õ’Œ²Âê/“ã…‚.²²ª:•ܓʋÆ›*–QJdeyEÇÈ[³\f/m&ôãŒ[»†E®^¡,ʶ³5»-…‚| :—£u€ŒIh Áz¼»«t -ºú›rtYLµ§.Õôv÷G¯–B´“ãð>X²>Y¾| z$Œ²Ê*è‘S'=Mükž½+«Sƒ(… E KæÍ€!£Æ‚ÙäÙ3 <:ëä»übx¯[ݽkèìôÇ~ø –íÉcÏ ôåÎÇ%ALì× Þ»g$a5z­kKï )ËæýƒGŽ•´û’o²µ ÁÅ£×n°ÖrC¯GÎ_€{Q?Ÿ;϶òN?2?¾ïn¸¿û-N¿ygfìÞò\Êòù¿Àw!RÌõЬV–³»2¶Nq­£³˜éc†G„ ê’H¤š´ÇƒÌ˜¶‚¿ùÚð¡°d×^t34. eåü0P*¡ÈjeÉß{þ2ÌÙ·› ƒ„’ 3žx $ú{tx± ©Vëúy·ä§üºð0ü^ÑJÙmf/ÛeɤÈÛgÒŠUèÞ >w¯K‹ç\¸C>þ.]/­5Xå²PPÑÿ¶hôg…â¾(±(È5`X‰èÈ}ù —Þ=þÈ?ƒ³èè¹Ó9+ªŒàƒBÑÔsZ½¿fÑLè{ç=.#Eѱ,{¬Ãþþ¥{Deüì•«ùúÛÐùwà"¢BÏuwýR“áü7ŸÂÓûIÒXH(ëÏf»1WY’O©Ë8¦~ôZ)œ*-ƒˆ@‡Ï]¼~ôvQYçPÏ!¢ojÌzjb *^6þ½â7)RåwJ¯aw‹FŠ|áw ‹Ž˜f<³~·Ýô¯”•A¯w>„ÔWÞ`GI$ˆÎ±1pò‹`öÓ“êtQÿñWÉ&ÎÑ·7,™ƒW]M÷-Œe1—ñLBù<·öæ¡¥Ð÷½ áÅ¿ÃásXA%•Õ†ÄÂ矬s˜p±ÞÆ÷¾U2“PþX:›Ì0QM&¥NAìsyמ8ÃÛE@K?ŒøÏW°ûÈ1¶Â‰I££PO±N£zÿžQ° ¿‘â”dBÙ´lt<³ôv§`3dÈâá>Z[ŽËc°Ð[WÌÆâ[˜ 9¢,{ñYöw-J|Û‡ÿ†‚“§¥Ë#6Ì[ßíðT7jºU&&hÙð¥’wYî;ü‘ú¯îCFÓYvÕ¼üèmØðú+. ƒhõ+/ÀC=º±¨THÙº|þaË"!ƒm 7¸ ! ðƒÿõË”ô›Ç.^„o}ÈzóBýz–ü Ò_Ö±7 R²ÙᆰLæ #z–J/…„ëÝ,¢Ö~ð}¿,É¿»ùP<ö¿©¬?ãk0ðV„+ù‘Þ=àÑ>=¡©¿?4 „¿Íš?oÚZ»çÀç:[ ÅU ´Jܱ€â™#Wƒ :˜1¨›lß_WpfoÝÊÿ(\@?ÇDƒ`:-|4önÙ¹.BiüÞ] c†¬ÛGÕñSªÌLЯwõ’^ · ˆœXyù X:¢7øûú‚§iM~<ôÝd^EŸ9pT­Ø—‘'Q³N¤š…á ûúÀmsÖÀÜm9 %ó âèwý:bäjèD‘V–=nß¹¯¾Á:Œî¦&='ÿìÔúÊýuþM¡È:áf¿{š ¾~Ð2³'D?ó2 ÿôKÙC&â^ü,ܹþ1w!´xâyhâï/Ø$ÞCBÁ«Y.³wÀü áZrÎQ¯ª(‡V`MKÑ# ó:¹®£ªª`ÆæmðŸU¿ÁÉK—ÁßÇPÓ+Ô'ˆßý'‚æti'½Rï?o= D£WG­Ù;IDATb(”ƒ¬DÿQWS©Ré™”C;e°ÇˆÐöµZBM”8wõ*ì;qÖî?¿„£.±ã(RD†mÉ/¨IPñûoȰç"(gçh‹E§óñƒÄ·Cá¦Uì-Í^¤.býB–ÙY÷h†2–9¶ØüÈ©£Ê·vî,h…D’‰ ¿{ŠYt:_H¸õvÞ˜U:yט¤©D4y‚þ¯Õ(sבJÝËÿÏaê(1BJ\÷}ÞBXYžEOMß‹H!¡˜M¦Æ!o j¡Ü¦(¤4hÇPëÑ£íæ½Ý—W(u±LHñV¡ˆÓ!^ô}tמŠ\¡ÛuΊG ×}µU‚PÜaöz ¼L(ø!ޡ裺ô÷ˆP|‚eê²¼L±×Qô~Ð&Û½B!?-i؃×qÚ}ù»W(4Ø(@ÌÀœc¼¼ëb»/7 ¥i›8˜›è[*‹@¶Œégj(H!¡DÊ,Æd‚ÐØÔ*YC'hßc aüÓû@Dv?Y„B3P³zƒÙl"º›ûB·Ykwâ%³¡ ¥ªì:œÚþ;ÛßK" p«öÝÀ/4l ö*d.b"4k훆£èƒ)}ëzŸB6m{%a|àŠ0\Bˆ ZîÇ ÁRïí‚©¸r Îæm]%:ƒ/…·…¦m*5:ݢͣ{Ï•TRI%•TRI%•TRI%•TRI%•Tj$û4ðÔØ¸:»äï/)>ÕP*ËGc´¶Ç=”aÿtå{îˆÒžôDCp#MDþÄæÞbäáõJ:/Ï!÷Fû!lI´—1 ¼'\ (Œ¼Ä¨=¸çI‹ÂøE†‚‚|¯Z ʈJ "GV•¯DemVr°Ë¥Õü!ÈaĘßu"Þ¥-³!§ ÓR\2Ç7#¯÷T¹í d$fh¹+9/4Y Cä«dû7ðý¯ðò„“gè ’ÎX…—NFKãùi¦uÍž@VzP¿È¥×ðÛ„‚JÓ´õƒøÎãXQßJ˜Ë<~ØyL£¥#Rå!a<,³‘QéÂ;ß`¾B$*ß4>§Ø" %*õ)2 û?Ÿ@.tÐWJ>é«ûy~ÊvwèDh†û;øy/òLä2«{¤Ä_‘Ä?yîÍÄÖ9Ö&­ðrÆæ¹®ñ(Ͻé˜þ§Á‡ŒÉKvîwÅüäð0^¤@šð”s,Ͻ³øíOñÏç%llsynßÀ´Æ ²²ðB—FãGI”o¾Vx˜On ÈÏJ$Œ¼Œâù)D¬"„ÎIX |Í—%ü¾Ñ¶¼XYO¢À¿âA u#RíÕÄg6ß…iTIî©ãGËen©kdþÖ—(”RäÅÈ÷!·X/.ã¹½ëm¡+žºÇ 3~EÂÏÑê˳<÷‡qL•h¹¿ù>ÌÃe…AÞÿ`›Ûfü^oGïÙCÈ1ì5„AÝë4¯PHåO¬Øº­+ÜÏsß)í!$ àMûÊXÁd$|.âµ7ñMøîo"Þ¹ß> s!:h „û2yË/{y²3]¸Z¢ä›£pÿëŠ@”â,†Ê(˜CÈÿ‡ÜÙŸEŽ—ÙÉK DI?Šßêê á wvð|… B*GŠ^âùù>ùgnÛÒVŠVé)æ¹÷ƒÖú¾£Ÿï\eÃŽ,ßöÕmE–ç ÷5°?aäŠÒ2çžVÜ9䬴Øx^ÿÎE^[9œcÍ}<ÏwrŒØeºaê®ãe,ÏïAX–YJÈìܧ–¼ƒ†Bçïºøí|ž{³±Bšò ‰ôÈDžç׊Ho! ¢Òª+¤ÀèžçîÁôú)R œX.Ó·Ißð™ñ— q "Sú7ì|ãD=óÐÝŽž\ƒéú(ÕÊsjËx‘ßèà·œŽ°w8I©Ì^;÷¯*R ØŠrtK’‰Šü#8·ù69vÿp![ÃðÝM•Â0ñüä‹(Y¡H?3ý:^h¬ã°ÍOs9A¼jõì76]‘Ÿ“o¿CÊ”¾% +Ÿqé-u!Úáã d9òiz å^·Nzþ6ÿPH_ýz­æFÓÎ:ñ†4ûZ¥úcó·™a Œfæë e“ŸmŸ|ÃyqK»üùÐá;|tšŸµM˜b…¨ ÓÁr´Üd¾ûáĸí^oò C›ø~×i4ªh•Ò'7ŒÎÇÄ0k/–U}6#¹Ì+‚àhì£ß…Z#Bm ª–’ƒ$û.•Wu{.#éº\iÈ6ÁÁ_¯ûñaÝ(õ}*)™4Hoâ£ûþù¢WiOr4oæoØÚ#ª±˜ªç1Ÿ$¯àÂõìní(Ëi¯²h“ÙìcF›Ra,;@|›ûùP;ö€˜¡{¨6¼J¢"O[“ ilЪiÀšQ@ÑgÕ{ TyfÕ˜Å<Òç4&MËj³—„´‡Ù nxœQA¯DDV¹È¤A̘qi¢6I•œ›XÞæƒ˜iGmÜ*Éßñy§“Î2ãþÚRIþ†­0Y±&–×iR{ ÐQ}"šX2„d óšÕA U9ºEƒ€÷ù l˜WQkÈ×E±(.-5@!*ÙÓ fo(dÔöêŒLÜ(p´ €r“ Ž—–£ÀÍjËìƒÈÛŽÜêƒ4f@P} |tZˆ „Na¡¾ºÚûýò;¸îÝ2³hÒ§ÇÃFŒâ뵺£ñ:€4æq*··V£–þ~Ð!¬)¤7k ¡¾>NßõCm²j{ìÌÝÝ;w…Ží3X¨A X¦q¦QE±¬M¥”Ð&Ù"Ú ÉäJ”;40}O«Ñ–9,ßÒ9:¤g€ÉdRaÁsój2«a^E™JFd?4Ú5 D04ƒÄ`4•¤Ù±'"4¤¦SÔpëi¶îØÎrW(íÁh27º¹ÿŒƒûÞ7›WæØ´;…bâL¥ð@èÍ›BˆSÉUŠk:mm°Y€² ABÜ%+Ú§µÇ¼™}¤ƒñΩ& —iï’©jÊ;€|†,4•¢‚Üš‡èæÍÁW¯g}ÍŠLËÿsvng9’ž–ަ—+R™›™^®\32ϲ”&`?½â›CvËf >ZÏnnÙ,BüáâõRÐÙY²lÊv qçÌΖšÞ(}†ñBË̃h5õ/¼T½ E•¨ÁEù£fha¢Jî¦ÁAç®^«cjÙÊŽ];XÎêÔR()êÅ0^:"·m(Ä]Z†Â­áan7•\%4¯âZµ€]G…ŸRdÊÎÝ;XîdŠÉÜà'4z©“^öÔx°WñÕkáåNÉÐÜÏÇ넞sa§è÷,@Ù !îØ1 R8P¯œ‹Å™Y›íŽ _«ª‚¹ÅÇá±ÔvlÊÛbй~Ò—(¹¹;YîÐ1’SÈô25¸è0k©ÈhªÈ£A¸5éžl—:ÐBîùËðin!<Ó! Z­×=¾u+hÞ¸Q/p[€²'w˪"exØÓš‰ðÆõ ž5±j@‚ ¤èÊuø`g¼ˆæV€^ùç/ž¾|ïÊ£Yºˆ”({÷ìb9=£$%§s“"½[¥xe˜—Ç#Ÿ.ZM.}ƒÇ©ðÚÖ<ˆ4–‰cGá¹A!¦…çOb AÈ͇Šaúæ­ðÛ¾¸TZÊ:ézNÛImZ€²oïnÈC ´G%19Í«Ãà xá@¡eÁ”FꞢTn4Áam\kÑæì;/÷u?@Î]½ w[¶Cþ‰Sì8 Zn€OM£-E'ƒiHéïC§£éŸ”ŠÎ¼Éëô àkÒ«Yq®±É þ~þ@‡ ¾ZÊÎ¥’SpÛŠ³ÚaÕÞ|¸pý:ø ã­çœoš{îÈmš[Ui2Bh@ HO…q·ÞÝâà•™saò†M5À‘(¬FဒھÄ£F1sÅ®Á( Þæ¥)Z3hÚU`þf< ¯tN±;Z-–.\»Kvï…_¶lƒ½ÇN°“¼Ú¹²ÊÈöÉ­áž.Ù02»D†Ö= ø‰}añî=p½¼BÖHœ%ûóv³œ’ÞâSX9z‡ñÂ…žˆn0}šx[ðçUXtø$ŒŒkãRZ;JŽÀ/›s`ùžî›ÛuøÜy˜²aÌÛ¾“þî pÔŠíßËr|j%U²GÆÅw¼Î©¯êóH§ÄT/–êœÕ^:º6 BíÐÌ­Y8~ñüøÇ&ôu¶ÃéËWØ(˜eÄb¶ =#Ä¥AR„Z%5J4 ÷›^^Å2sMã…³.”•CTp€[ÀA  q’é›¶ÂQ‡õ(ºõ !i2Ô3Ò`hÇ xsþbAëEÜä₽,Ç¡Fi›p3ê¥ØÀ¥ðw½ÎI—î´sI¨ï:¦TÀˆv‘’~›¢aÓÑùÿyãÖ|"gߢlá£×Aïä$˜Ô¯7ôFÓΚÈäúÉPrþ‚¤új”bÔ(Äí(QqÉn™ë%÷Hº,ÚzÄÒMáf7~¼x)U`/Ø;²<×!Ñe_êò0{ÛvøqÃf8xæ,«øtE•t:-tOhÒRj‡*“ Æ?VìÉs«_$¦C‹MA Ä'Ë:Í?[Xe4g.º³G©×dø÷ÄÕJ2ñ‘ž©ÂÂüànéÐLÀ«kåå°`Ç.˜¼~;×J«Õð5Eɺ´‹Çúö‚;2ÒYóÊš¹%ž6“ŸFaQ Pb(mÚ%Ë⣰1™3{@†-ÞˆÑzµ©e㢠ŸîýÚ´¬õÛŠ X¼kü°þØsô8Û@ù:…‡‰:E·e1´S†¤©RÌÇ#?ü«ó j-•”èäö‰¦#a\Ö¢A– ÷"€ %€h5  5…š U0yÝzØQr”½ÏÛ(ÚGEÂ#}zÀˆ¬Nèë+{\¸};ö¡öR2PÚ&¥s@©ÿ”$ˆÑÄd.õ&€ A€è@,¤E“éϳ§ 87ŒU• AÓ†ÆQŒŒ’Ã[Ã#½{Àè.YÐÄßßcyVpkUhŒ–[yÈ£ÀÓ·õ…§oëÚ3Èa:´Vfľ‚ÂÓçØñ*…Õ*‘”Öí’ki‡ÄÌd®º«—÷˜X·72€Ôô¦(P_0Ã#‰pgj‚W—…f*SØškQ´®ÂdªÙ/˜f7øú°;¯ˆ ><7m&LÛ´Õy©vÝ#Ó¡U,™^&§ùU&€ÈfbA#<@‡z]#š[߃v‚ó5xm`ˆkí}ý™^!,KEM.Hkáådá>–#(1Õ@aÜlbÉvüÓ˜ÏHÇ ngµ¡ðȺÝpù ®\<Ùqíàñ}`p‡ö. + Nž‚§ÏÀ]3kùäÓ|¹z:{Žá·°ëŽÀM œ*̃ðøth›X3àèuNú€ùµØ ÌqFZl$e¥plOܸr Ðvf§£´o £ºd"`2©ahÖqNñaX½o?l*,b—ÿR§c nrè)2fÐékÖæË©¡ á é“X:}檡]½ÇIï?=Dã:@¸ó¢Ñê ªì:Û[ ­Õ^…×!hè(¶N1máÖ„xÈŠ‰f7plËԮ´ëè…‹°ïÄIØuä(ì9v‚ýÿÕ²r¶…°R(rëVÖÑ- mžyèí¿{‘“Î@ƒßv¿^õcFWÞ×Úuíƒ@¹Ç (W/!0tàï£åL•ë°:o?ËÔˆ <´•'h¹9_4î@+)’€ìgг&Ýg7Ÿ3Ù‰‘e•PJ\YÁþM÷Lœy¢å6§Óñ„s•8]Ŷ§}ä\#/#@TrZO&3è|ü ¶K¨¼Q 'òr ìêŸ5ÅÚYµÞ,ÂB4fAL@ y³ßãnRGÒ¡QÌ ÷󇨮ýXÓ‹€RŽ@!SL¥ Ôsï]Ñ(FV£Äd“éU 'ó¶ój•¼ *I Q|ý!¦K_4½®Ã©}¨Q®]V5JCˆª@¤Š‰5½¢9 œÞ·M/ŠªQÜÖÖdsÒU’Ö™×û@Ûìj…4J…ªQT«ñöˆŒRŽÎæ4Jþv(^é¤+òp†C÷'¥mgÊ58C@¹~¥ñÅ`SÉ]>JD¡éE@9ÛØ¢D%G@ic”ÊFùZœVm^ (N£Du醀 g‹¼Z½´Lëê}NºÉò`³!Å/ÚtîURÚð4 µ±€°Ö×o lõ2« }ô2U©{^£èP£DfWå\~åjƒ ØÁ'° „D'•^=s¡Ò«L¬Ícú^Æ´Â2'«!²7ú(‘ÙýY6øz·é…ªCgð…–©Ùöž¿$³E•7ú /#ç©€â_­Qˆõ^ʯOPSˆÈîKù_k6?3=YG+n™¹F¯ÑhfâŸ£Ôæ©ÆÔË(_ (v¨RRI%•TRI%•TRI%•TRI%•TRI%•TRI%•TRI%•TRI%•TRI%•TRI%•yýîU©±qâåcp¼ÐʹfIñXU䊔á_ðòŽ.G>ìμ Z“ž¯e€¡ó~ݱ JV‚˜5ÆtìR Ï…¨MQ± P†n_¡·A2-lzªW¶¶ÜwóiQï"¿®¶•”@zIxY‡®€<™T±¨¤€ 8Òðº3UTRI%+"Ÿâ *©dßÄêîâ»Ç‘O"_%Ǥ‹ˆªbQIIi#âù-Èwì/)¾Ú˜+ ÍÒ@¼$"§ ·ƒêóà› ûˆøŒ÷N0rTGrü¹û¾Ü·®!wÀú>"aÞ)ß#!·Gn.ðÕ‹ÈÈ¿"/Ä<å5€¥ È£#8°QQc}‘ãænLšŒù /oUdÒ¢r÷àø-ü&Ý;ƒü¶‰_+»„,ãå)øçÃ_¡`Õp,Ó5±9Š|®‘ƒêç䇼0ïä_~†ü”ŒÉàf`Z“I+a£Z¥°:ˆBplÄ?Û ìŒÆafÚ:é*ñWn^x)8¨á–¶æâJL÷ ÕÍš8"Å ³‡ û•K>Ázä8/Ì»/ £¾²™¶ðLçÌ(¸9²ö üUDàäßܰ˯Å<Ì‚ê<…´ïIŒ.È—ëëƒ4&z•êZÄó§¸wæcE— hÄ‘xÙòLé¶xÝ‹ÜÓOYÆù»?Â<Ó ò6ä(ߤàõܯ{¨sˆA—m3ð¦€C_,ÿY©œôxê™0f&¤[Î9x'ÖS„ên¯<‹eø\d2þ ßDÑ(Ïý,&à‚ϞƺyÿœçäÑ$Ém^þ'ðñ×°<ï yP @h¢ØP ËDN)(HÀ±ŸýÊpÈM>w6ªÏEN ]Ái'Gà¾ä@Ê\äá§±»^bÂæž4±Ži@sY¡1xŽÀý…óOf…_…t;!_Æö g¯ņsÉ)㾯mO²Ú€ÜRÀãaþÿ*6 O¤(¿¤H‰‡5E|.;Ò"¥eÁ9l84æñµ@Sïu‹ÏÀmXˆfGÐG8{=ŸãLã¸ÀA…/+›æ)݃žk§Ñ’Ù2™¶¶i-söiîÔïNž¡±qeaÚN²Ü»¼„õý‰€_Äz¦Y×4U½•€÷?Ççç´É "hÏѽԨ?…êEBlïäÝ\賂‹²©B¿h7gÿ°íG=ü˜ßÁb´§„jK¹Æ‡hkoÌw1–£±šÖ˜OÒ Bf1Ób7š>ó2¾û± q6ù¬8„zFä«4Y/†cOäû8æ{þù¨“G"ñYš0I3·p¾×eÎc¸²´âœ\šò/tiö*ZÌe³Š6uð4–ë{üó78ÂÎm\×o^  ûôÕíeù&G”"OBfô¦s,¥_0šÙÃZaçˆÚ$Vu.oà»o[ÿ ®qÙA¾›ŠD½M‰oŽß¥¨ MÉ>.C¾Ë‘»áŸ¯¹±ºÈ¯¹[WqCA2|œóË„†îi)qIõ´y B+ù3®AÓºîS" ÅÝSè}ä(ä‘/qß%ç&Ûm•)ßïqù¦`ƒ i²&™21˜N R×¥c¾h= Eß& |…ÀA a5‰ÿ/à%ªhŠ/õ´úäq38›šìæ*¸9n‚›»4Ò½#˜÷_%J“*<›³Ï©‘StŠbñ41îs1é¤ÅÆk`hªv3¨;ŠOþÀ"nD\ªú"³‘¸¼Gq…P.~¦ùN´iMV¤E´4!Ú.WG¥䉯”º‚óÍ«iÆñšz¤“$—"åqúÿ‚`á{EEIEND®B`‚flask-3.1.1/docs/_static/pycharm-run-config.png000066400000000000000000003025061501065614000214140ustar00rootroot00000000000000‰PNG  IHDRŸW›‘þ&sRGB®Îé„eXIfMM*JR(‡iZ``  Ÿ W3Ó)ú pHYsÄÄ•+YiTXtXML:com.adobe.xmp 1 ^á@IDATxìœTEòÇ›‰KÎY¢ b x€˜õÌz†S1çÌßpwætÆEÏpž9`Ž`8Š(¢ˆ ˆ$ Kΰ„ÍûïšÝz›÷fÞìÌlâ×~ÞVuUuu¿ï™ÙÚ~o6                                                                                  _)¾:’A€¼“A•9I€H€H€H€H€H€H v¥±áˆÊ`1¤2Ô*Ž!Ê<Ø#      ºF€…ª8®( 'Ñá‘QtFŒ       ™‹S®> +DxÐE$@$@$@$@$@$@$7º8µ£^v´óûÿ&       H ¦ U—‹15ñÜjâš’ò“’ @ 'P‹?5qMq_ƺV ©®ó©®yã~0 @DÕUª®y#¨Œ³.MªêªjžÊ\GŽ!      ¨zUU ªªy’B°¶T’½îdçOÊÅdR       j'ìBQ²ó'`m+²$k½ÉÊ›ð Æ„$@$@$@$@$@$@$@µŠ@²ŠEÉÊ›p¸µ©è’¨µ&*O"/FM\S"ϹH€H€H€H€H€H€H ºÔÄ"M¢Ö”¨(çxòÆ36èúbŠ«i…xÖÏXZÐ\Aã$,±îzbéWÕ<±¬‰±$@$@$@$@$@$@$ ª* Ä2OÐØ qAxÄ“+ž±AÖ8¦¦*âYG?ŸŸ]Gò¹•_™1AÖ5¦: •™»2c‚ߨDÙu/é7G¼±^ãi#      @,…–Xb•«ß˜DÙužHÒo®D‰”//–¢H „ƒb7Öx]†ß¸Xì~±:‡-#ÅFòÍaÇQ'      ð&´0).’ÏÕ+ÖË&ãbµ»s¹}¿|nœöc×q•–A "•žÀc`¬sÆ/Súñ²µyœŠç<^ùtl$_¤uëxJ       `¢Y"ù½|^6¯•xŵI>¯X¯yl[¬cb·çŠYV ‰9a”±Îk¼Lï5Ƶ¹}¿qîé¸ãÜ~¤<^±nþXú‰ÎËÜŒ%      ê 袉_>/»ksû^<¼b\›Û—<^6¯ü¶-Ö1±ÆÛsŤWe#Ö¹ïæˆÖ÷‚mL4¿WNÛæŽ·}¶4ÎCH€H€H€H€H€H€ê2 ” qÊʵ¯ylY™î;Ÿ—žìx¯9£Úªª ë<ñÆ»ã£õmPÑb£ùí\ª»cÄîeÓø ~;ÖÖ£åµc©“ @M$kEÏ!Ú8/¿—Mó©tcbík‘±Žuãí\^z²ã½æŒh«ªBEÐy‚ÆéI¹ñ±ö5H{¬­GóùåP{¤\ãÎaÛ©“ TŽ€_!Ƶ»}™Í˦«°}¶îŽs}:Þ«LßÎå¥GšÛŽg‰Iw #1 KþXbej7>RßõÙK·}At¿±±®ÉÎã§Ûëñ‹qí•ãæ`ŸH€H€H€H€H€H€ª“@e "±Žqãcí+{\]Ç©ô#~ÛçÕ×~ÒïW™Ü‘rUð%»PKþ ±^q¶ÍO·OÜ/ÆÏž¨±šÇžGm"ýìͯq^2ž±^ùh#      x ÄRqçŠ6ÖÏïg×ü¶?ˆ®ãD‰·cb«ñ~9Ô¯2hœÄÇ«ùÉd$‚æ''äÆÚ}[÷ŠU §Òµíî;6UFz±c5Ôsû~qj§$      ˆLÀ¯ˆâÚ£õu;®¤ÜhÛlÝkŒíW]¥Æ«tívßÖ%Þík/46hœ×¾6¿â‡ï€AsW6Îç§ÛËÕ•âS]¥¯vWÚcÝ1Ú×1^±¶Oã]™¨7/û$@$@$@$@$@$@$P )ŒT&Æã§+//¿ÚDª®ñ"Õ¦ÒÏæ5ÆŽuu¯¾ÃÖí¹m»«sÇEì)pDLÁ$w™Â³û~º.Mý*í|¶Míjó“šWãµïÆ»~ó³ëx;Î/VcüƨŸ’H€H€H€H€H€H€ê H…?Ÿ—ÝË&Œl»ê*]¿2U¿—T›_¬Óµ}‘t×'}¿æ5‡$ƵŸŒâEМ•³Ç©®Ò>aÛ¦º+%^lj×¾—›4Uég [ñvœ«{õÅ­ÙkˆK? Ôf•)Œ¸c쾟®ŒÔ¯R쪫ô‹µý¢»}¿\vœ_n{¬£}?é•ß+6hœ×ØílÉ(^É$FëÆÙ}ÕUÚ'§6WjNµékLécÛ\]úÚì5©Md¬v{l4Ý/w´qô“ @¬QÐðËÝ«ºJ9GÕ£Iå!q«ã£õ5Ζ¢kÓñ*Ånë^}ëJwœë%—רílÉ(>ÉY™£RNÆÖõäÔæ%Õf›ÚUzåÒ¸H1v^Wwsº~¿¼:.éæŠe,cI€H€H€H€H€H€H : )Žø­Ïk÷ýtÍååW›J;VlzˆÝ+FíêSégW¿J‰ÓfÛTWé£6Wºc\¿ôƒÄxó´%ºH$_¬1v¼Ÿ.'§>/©6¿8/¿~›úTúå°í¢KÓ1šË¶©/èüðóùÙí¼N*vI€H€H€H€H€H€H ÖˆTñóùÙ‚úTŠÍëì¼buŒJ¯¯\¯c\©9\»Û—8ijª‡yü°óx¸C¦ 1~c+ØÓ+ôjOÇ-ÂhßKzÙäLÅ®>=s)©ÍõÛv‰·cu¼ãö5N¤ú"é®Oú•mö|•ÍÁq$@$@$@$@$@$@$HÑ A—uóØ}[×µÛyU×8Wê±KqIâ5F}jÓÍ'+~ñ©_ãUJœø¤©î%Ë"üó¨¿FJ=ÁD-.Z¾Xý¯RÖiëv_í¶ôÓíq¢Û…$£ãì87F|ÒìxTêøP"ç‡æp̾ÝXã%QeÆø.€      ˆ@@Š*±¶XÇøÅÛvÕc‘+ëW]dNãŽÕ¾ÏÖÕoKÑ¥iœ­Û6Û.ºWsãݘh~7Þ·ŸèâC¤|‘|º@;Fu•cëv_í"ƒè2Vš[PÒ±Ñ|§ÕL{N'MóØ>µ•E”ý bóбs¸z¬ñîxöI€H€H€H€H€H€H º ÄZøpãݾœO$›øÔ¯RØ>)$Ù~Û'º[hÒX/ŸäwÇÛñ¶®±"µ©_úª«´mï%íx×ÉçÆFì'ºP)_$Ÿ,Òö{é¶ÍŽW»ÈHºúd¬[8r}Ú©º;Fò¨Ï–¶.1Ò\›öm_(°ü‡í÷ŠqýöX¯x×Ï> ÔTÑŠ®?Z_ÎÓŽQÝ•vœø\¿ÝW]‹Mn¼Ú5§íä³ã\]s‰Ô&1ÒTFÒCÎ{œãªÓõÅÔVÄš,ZžXýïJ]k—¾ã§‹Ow;¹9¼ìšÇŽUÝžGm*íyì8ѵٱª«”[×1¶Œää³sP'      šF ž‚ˆ=Vu•rž¶®}µ‰t CvŒÆÙ6[¿Æ¨®ùüìöx?Ý«1"µõk¼+u¼k×~4¿ÆùÊD)¢å‰ä·}^ºm“Ѿ-]Ýíë8±Û‡ØÝM¶_óhL¤<«ã5Ööë±iS›J×î×W;% ÔUn!įïÚ…‡Ú\éW rÇÈ8û¿4{¼íW=ZŒøíXÕEJÓ¾ê¶]š[fÙf³ýê³¥Žµm¶ÍoÇzêÉþ¶;·€â¹£ß8µÛÒÕ¥oÛ$½½«Éöi¬H-0©Mû2^šÆ¨_mÚW .›4‘v>µ«Ïí‹ÝmAbdŒçæpû±ÄºcÙ'      XÄRĈkûlÝ^‹mÝík¬Žôwc¯8µ©”qªËXÑíß÷Õ&viÚ—µÉ|šC¤½Ó­ÐÄo¯OǪ¬\«ó¸>¿¾ß/&.»NW3Ø/Ÿ]ç³ý^ºm“1ÒW›—îÚdŒ\`µëXÛ¦1"ů>éKÓ¾ÃK—XÛ®}‘’CšúU#MeYoû¾ÚUºñjWͯq”$@$@$@$@$@$@$PU¢Jbõk¼J÷<Ô.Ru)þHÓ¾ú¼ú¶ÏÖ5‡æÑ¢“ôÕ§ñn_ì¶Msh¼HÛæêeÞŠëwm:Fí^Rçq}~v7ηŸìO¾Gph‘D¥†J_m¶.~í«Ô1náHì¶Í. ©Ýk¬Wœ½«6É¡kQ©y5Æ–ªkŒŽ×¾úUª]¥Ÿ]ý”$@$@$@$@$@$@$P[ ø?Ô®RÎÏÖõ|ŦvWJŒüN¯1ê»ÚìB’—Müò{¹øTO³úZSГô5Ψ¡æeSŸ®I÷—¾­kœ®A¥Ú«]êbã]ˆW/›=íWÝ•oÛE×¾øå©Í¶‹nûÜXñ©MõzÆ 9éëX±çP]cÜyÕ/cÕ§6í‹OšÚUËíª«´ÇÚºúEúÙíÕc‰Õ1”$@$@$@$@$@$@$PZD 2Ö/Ö¶Ûºæ´mª‹TÝŽ³íêW›ö%^mR0RÝ–ZT›èr™#ÏÒÔ¦ºŽµÇéX‘Ú4N}^v±Ùcì¾ÚUÚ>ѽš«~/›úÉêÚùd=lÝkÑꩇÆÙÅŸè¶OlRuÔñ¶”8\|ñÅýîºë®K›4i2055µµØØH€H€H€H€H€H€H€H %%%+7nÜ8ù†nxìÉ'ŸüÍŒ ÕŒ”"Žè"åÚDq¹4"ä“‚”{įMÆi±Ê¶k£ñ¶”xõÛº“TÝ^p<yåñ²é¶OuWÚ±¶ÏÕ¥¯‡Œ].Šmó*Êë„•ÅH-)6G JŠ ñMÊ s2ú6X†¥)X_Ô˳³øT$;$@$@$@$@$@$@$°C„Ô¤È$º4û¶)±iQJu)(©.õ -4Õ·å糋S#Ms–õ’ü3ÑÅ'Y|,Í+ÞË&9Ån®ÍîëÅ”‹k»bSŸÆSY+0;›æ¥OÀóßÒÕ¥˜º`=Ö¦›A©H-2¯•‚"¤ç£):¥¦å!ÇÜj7mS=¤æç⇴oѵÏNhܸ±¦£$      Øq Øu¯”úÕ§Å"-8I CšÛW[ÈiýxÍ¡f/›ú¼d¬ñ^9¶DŸÂ‰=…¥.í«»ê"U×x•j×/)±^v½ "U—"T…–Ÿš‡Ü”|l,NÃòœU˜ûèûHË7×­ %Få•b‹Ñ?5ÒÔ©ZœŽ¼Mù(6¾éùï¡ß«{`ï½÷©“       ’€Ô¤!!9d—“önRÇ¿H»Ù}õ‹t›Æ¹1v^{œÚÝ< ïËÉVuSAæÕX‘ö¡cÕ&}Õµ¨¤R}Zd»½ëIÆUh[K·bcI6—¤aKI*J K±hìFLye¾yégœ}ü…æÖ»tüý²[0müLÌø~6õ4¬×ò¼ñ‡sëcFQ:JìKZavH€H€H€*Càˆ#ÿŒE99•Z£Æ¬]·׌‰£Ž>ÿ~üßxhÔÃøö»ïª}¥¥¥¸üò+°råÊj_ @$@$@ÕI`Μ9˜1c†ïÄ'1›Ô´F¡…(ªµ ÑíÕz…HÕ5VmjW)þh-–Øh¹ù«r瓽 ¯U›H=dŒ«»6»¯±"åbIÓ‹&º\X9¤‰Ý¾Ð!£üÈO)À&SEÚZš†Õ…™(1»›6mÙ„º-²[áøCþ‚¥9Ëpò1§á¦ÛÿŽY3g£M‹¶ÈÛ’ÊñSF./m†³‹7áøô<4Ün·[x**$@$@$Pm.ºèâP!'-- ê×Çž{퉋/¾Ù ¾uü‚ /Â…\€½Lþms¦"#=½z÷Âi§žŠÝvÛ­Ú8Dšxò?àÕW_Åüù Ba]»vÅ ×_‡¶mÛFÕ÷ÎÛï q£ÆxïÝw——‡ D“¬€_~ù-[¶@»ví’’‚Ñ£IÖTÌK$@$@I#0uêÏ¡Ü{ì±{Bæ(((ÀÍ·ÜŠÿñ¸9e®;ïº ·ßvkй¤F¡µ¹µÎÖµ~á4‘ºµEmöÎ&×T>Æ«v/›ú’&µ@“´ Ê+ w×îöíxõ‰´‰±ûª‹] O*Å'M.´^ìí ›ÍµÍ5»›Ve Äì|ÊLÏİAaè áX±b9ºuêŽÙsfá“O>ÁOS¦bì‡ãPoªT¦•¦¥`YVîiÜ·òÙO!(üA$@$P# \{íÿáƒ÷ßÃO<Õ«Öà¹gŸMè:çýñ6nØPáC[Ùœïã¹çŸÃàÁƒqãM·àûï'ž×ÔGª¤}ýõÜu×Ýøó‘GbÌ«¯àÕW^ÂI'ˆV­ZÅ=ÿ’¥K°sß¾HMM­Ö“œÈ˜1c°lÙò¸Ï‰ H€H€H@ DÛ5¤q1îÒaÛI)]wýõ¡C‹PÛÅhèׯ_¨ð$E&;§ž¤(%1›ÖìZ„Ö.´^¡1j·¥Lc÷E—¦²¬Wñ§ësûígWB¤ž\B’HR™“’1zØSh.½PâÓ8µi_¤\d±«Ï¨á¾èáV(ŧâ¬*6»žJRPlžïTjvBmܰ~ü>FÞt&~û ºvîŠA{îƒ.»bèС¨W¯^(ÇYópxýÉH[¹]sËvC…“S!  H Y³¦Øg¿}³xqhuò×¾ƒ9Ô¤V‡W;úÑÇðäSÿ ÷å–±IßM™g…cŽ=·˜¿æmÝö‹òùçŸã€áÃCE– Ó‘VGýùÏ8í´SðÔž »sç„oI“R³fÍûD™òãOøëégà„OÂ#£EQQQÈdÍòaøâK.ÁQÇkþšy þóôÓxñÅ—*ä—ŽÜ~öÄ“OâÌgâÀ ½Çׯ߃÷Û²SLÚâ%‹qý 7àØãŽÇéf=|86d—‘ØÇï ·çŸ>·~ýzüíoÿ´Æ ç(sÏ™3çœs.¹ô²Ðš>ÿâ œgvŸÉy‹ÔÛäv¿~ü7Ý|sh-rN2~Á‚¡q•=Ç’âb<òÈ#8᤿„^W_s 6oÞÊÉ$@$@uŸ€î²‹6îY‹OvIl¡}ùPa×-Ô§6;^u‘Ú4Nd¬­2cb#/'\]Í>Q&kqu{}êÓ±vßÖí<¶]t½È"µ eÏ"óí‡[L˜<ó©oF®¹í®yùùxóý×ðò/bÙ’åøîÛIxó­7pß?ïƒl—ÛtËü-Ù/áÆô°³y^ÂÁõ +äf‡H€H€j"å+Và«ñ_a¯=÷ ¼¼bS`øèãñøcᥗÌûãòåkúÚJJJðÅ_â ‡«ÉSî·ï~X´(ò$Éy“Ù 5hÐ ¼÷ÎÛ8þ„ãqëí·…ì:xÚôéøïÓÿÁLÁjú/ÓñÞ¨+¢”‚È­·ÝŽá Ç{o¿…Lî·Í{¸WËÉYŒU«Va˜ùã’W“\7ßt«ÙѵÞzó ÜqÇí¦ˆõB¨#ñ‘Ø\~Ù¥4p Î9ûìЮ³¦M›†§ˆeáAŽ"s?nv²Ýwß=ýÈÃ!o§ŽÍzo 1²ÿŸÌN·'Cö«¯º­ÍN®;n¿=´–Ž:†³ÅsŽ“'ÿ€iÓ¦ãåžÇ¯¿†¦ˆ×°aÃpn*$@$@u›€ß®!=k»ˆÃî!–v)¹Å¢p`Šæ”Ï·Þv›çmxÒÛõ­I¸µ íK:Õ]éúì©%VšŽqu틬ò&']“šÂ²×äNã¼|2Ö¶»Y}b};S¶†4Þ3c3š¦†ŠO]µÅ”©SB» ¤Uœo>ßx³yqï‰8§šçU¬]»VæFºÙ9Õ5k Nhµ½È­˜l$@$@$P3 Üÿf§ËÑ8ãŒ3±ÏÞ{ãä“OŽi¡Ç{l¨¨Ð¸Q£P!f‰õ0🞆ÆfwS÷îÝ#ælÖ¼YÈŸ»1³gÏÆó~zÒ‰'"Õì0:äàƒQ—ov?Í ç8昣B;‘š7k†#?ßOšöERf™Ü›6™ç1\(wÿÝú£ÿþžC6lXzþ‘ÌáÕæÌgÖ¹&´NÙ Õ­[7lÖúÙgŸ‡Ã#± 9J,kt†VèžrÊÉæ9N-û´zõê…Î:…v  8À WTˆ÷êÄsŽí;t€ìèúÎ\á³{ÿÄ<Ãk´‘ ÔLZ´Ñ]CºJ·`¤öX¥_¿ycÍ¿}¼ün/%„J5»þ`×$ìz…$V_]c±§~ÛVmúv…—J®$ÒÉê ÛRu™ÎÖíéÅîúì¾ú½¤{au×®}{^l(ÙŠ¼ÒT³ÿ)«ŠÌ­wfó’<÷IŠN¥fG èÅFÊ7ÛbùK­lÏ×–'ËN)Á‰m~U% ÔHúü%Ùi´ÕÜ2'œök¥f'“Ûš7o6ef˜?¿É34ËÚgŸºeMû~rÕª²oVkÚ4kÖ¬A¾y·ÜÊ&·É!»7šÂ”¶&ÙMTEsS¸’S~Í^³Ä53Å$½mNÆ´°Öo碙¼·¯_¿Á6‡õÕkVoŸ«EKó™ ìQ‰M8‘£Ä²FjŸ£ÚÜ¢¿òÊ«avZÉmw×^w}èqë'ã9ÇÎ;áî»ï†Üîwö9熤ß<´“ Ô]n!ȯ`+¹}\ŠZ~·¿Ùóê­æ±Î¡ñºæ[Ííú·Þrsø<õ”òKë®”^~±y/M|vÓXÛ¦ºkǩݶé‘~v;&ôÛîtA’ÆäÎ¥}/iÛT× )}ÕEÚºúÔVaÉ«JóÍ·Ýe ·(¹¡o»““Ì6zS€½´h[±©Â`ÓÙj W%æ5Ïú ë·uÝì“ @#pÎÙg᪫¯ÆÑGz vºÙ­"…¨¢â²ç)É‚¥0$;Zì–’ªo¿¶æÜòñí·ßáì³Îªèðè}ùåW¡CMš4 lggã­7^÷ˆ,3Ù!Ù%¥»“¢­¹™¹½MvãÈí€ò oi¹›rÑÖüç¶Nf—P¶YÇ7¿ =—ÊõËœk×–Ý&¨Å,¹M¯uëÖáP?6á%Ú££GÊÐs¹Þ1ߪwÿ}÷¡K—.øí·ßBÏØÐX¿‚c¼çØÛ|“áwÜyóæáÿ®½Ö\ã®Ø©ÛN:-% ÀB@ Aò@piò\&±ÅÓ233Cß4é–=™C¾Nb+Û´ðd¹ôP¶-@~­AH!Aù b»ÚDJ¿üEOÆÉ!vWW›-MX¨éí'RJn·éº]{¸_öÉ+Ü­6E!Ú'¡6{Qê·¥ÆÙRÆh_ÎQÏSmò¬'9¤/²B[T’‡eEYXbŽÕ…æÛîLÁiÿAC°ÿÞC0d_s ‚6mÚTcwäz¥­AZªÿ_cí1ÔI€H€H : È­q{›Ûîþû̳¡eÈ-omL!E¾ñMŠ5R°øiêO—øí·ѳGÏˆß '…#ù¦µwß}]xa(wïÞ½!·ð½üò+¡‰Ëó‹äÜv“ø­[·„v<7òÌ(iÑÖ,·žÉóßyï½Ð3™äAæò¡Ò«IAéŒÓOÇ3Ï<ƒ &„Ši²–… †Â%—ìšzÕ¬_ìÌÿÃÜr÷?sëÝA^éÛ¢­1Ú9zM´yËfskd#´0·ámÙ²%ü`smœÝØ|ƒïœÐN/9mñœãÒ%KCÏñ’×Nóæ-•UÏ<Œž_¢l)I€H`G# … } x¼…'a׳gÏ@ß4'Å)‰­LóÛ]%ë×T »ªìúƒêR¦5 ­WˆMuWªÏ–¢KÓØ²Þ¶¾Ø«½Ýù”¨…=i7N!Úv/ÝŽS¿Úô‚ŠTŸ­‡ÏqiIòÍ·ÜIX‘‘r›Ý-×ݺÍ.To4ÙJþÑG…ÇØŠžäHI[ŒŒÅæùPÞÏ‹°ÇP'  ê&pÖY#pÞyà„ã7ÔzàŠ+¯À£æa⯎y}ûöñæíô›å¢­õóÏ¿À ÷ “çL=øà¿eþÙw—~fGνèÓ§O(VŠ>ÿ4Ûèå[ìÆ¼~"2L¿g¯ž¸÷ž{Bþ† „ <çœw~èYPòMzGõçð<‘ÖœžžŽ[Ì·º=4jž{ö9ó¼§Ý0Ð<øÛ¯{ì1¨W¿^|éeÜwÿýf·T:uêˆGGݺwç·cÔ¨GðÚëo ¥)°œwÞyçHÅÓ‚¬1Ò9zÍ=xðþørüxó­‚§¡™)˜]zñ%øyêÔpè©§œ‚Çûw¨øàƒ„ír-*{ŽëÍ3³î3×YvË54¿#Ž8¼†ú†sS! Øñ$¢èT•Ô"í®’s‰qW•¤þ »™D—&;†BåƒPo[ÁHº2Fw5•»Ã6õkmÃÞy¤ãtŒŸ ç7>&».4Ú hq~~µG’¶ÏÕõ‚ˆ]t9DwûZ9T©±Ò]¤úTWÙø‹>4þp;zë›Ø$ù3ßv—ÿÓ¬¹êQç•?óI^QÚóÆð!Í‘ZŠüÜã°ióÁQFÐM$@$@$Pn¹åVì¾û8î¸cªcú@sÖ†5:‘ ÀJ`§®å/eòK)>Ù‡´/ÕÑEÊ¡>[Ú>±Û}Ó Ÿ4‘~ºú½¤Øì¦9l›­Gó›/f«M Jº»oëâ÷ë«]¥]¨²mZ˜²ý:oH^ž1o–ÎÅü¢M(Ù©3š=p%Ú§mÅÉ­f¡uz˜kW/¥™F¦›C’K“§bÌ-É@ݲCW¼Ø<¨´^½i,>…èð T?)S¦˜T½m(.[ååVÂ3Î8£úf­ 6¬ÑZ.U   èìš„„DJñH}"¥N!v-3ØR Û¦º+MX…b”øå&¹¤iß–ªÛþPpùÛoÛUæ¯öÛî¤HäÕ\»öEººmÓ\j“⓯*õ‰ôlæK¦Ñ%%—gí‰f'Ô›ù :£Þ–aØT”†4³3ª4u5êeÍ0Ÿ‹.æO öGÃÂΨ_Ø©¥ <óÒH$@$@$@$@$@$@$°Ã°ëZƒb]¯»qÄfs´¾aÌâ5N¥ø¥¹ý2«¿]ýI•ɼíN.'¤Ùq®n÷5—ØÔ®ºJ¹`z1Õ¦Rí¶T]so'¥58½Cè@Ã2·Ùì„¢ùÊFs˜ÝM-& X¤î ‡œ\©õ ‘Ò´F¡…(±‰Oûª‹]›æÐ¢•cëïJ‰‘¦ãËz ü©'•À”Sé ©tƒÕ®Òö‹ÍË®1ê÷’ržbië«9(I€H€H€H€H€H€H€H ‘´ö`×#l]ý^ÒoëúÅ.MeYoÛOµ«ÜæI¢&'[ÝMOX¥½±©]õHRÆÚ~í‹Ôsµý¢«]bØH€H€H€H€H€H€H€H ‘´Ðd×#$¿Ö#Ä.ÍõÛ}?ÝJbý°óZæªWõD+3³}âÑÆë Ûq^6ñ«]¥mÓñêéw¸Wú^6Í¥¹)I€H€H€H€H€H€H€H Q¤îàUð²ùÕ8´v¡R×f÷UW©1*½ì^6W)1z¨-&)'šè˂ܓŒÔWŸJY·ê‘¤øl¿èúíw¶Oò±‘ @" híAj0Rо̡ºHiѤWŒm]šæ)ëmßW»+u=®=®~²8ÏâlP¶.9ý`¨]¤\T­"êÅÕ±*C1-›7•> ÔIóçÏG·nÝêä¹ñ¤H€H€H€H€H Ðú„ñ»‡ÄØ\Çh.éjº{¨6ïJöÚ•‘mÞ¼9®¼òJ¤¥É.êi©©©¡‚SFFV¬XœœŸª=g!     ØF@ëbQ]êÅV_í*5Vw#‰Ý¶i¿Ü\¡ð$>{œêöx¯qjK¸¬ªâ“×½@iœë»Ú¼¤ØôpsØv+1ZˆÒøNº"/‰*R­]»'NÄ!C¼¦IŠmË–-¡‚“쀒VXX˜”y˜”H€H€H€H€H€H€¢z„œ$Të~5 µ«tÇØ;žÄ§MâµØdëâwû:&é²:‹OAONà¸Mm*m¿ØìC|ÚW].¸ê^9Ê݉$ðé§ŸB¯–¨"—{õêÕÂSÿþý±lÙ2,^¼ØvS'      ª `×$¤Q¶CbûZ…ÆyÕ)Ô&R‹Kºvõi¿ÆÉšV|ri_¤­ Èh}ѱ‘¤Äî°Í«ðdWTM&·ú­[·Ó¦MƒÜzÇF$@$@$@$@$@$@Õ@ R=ÂöéÒÄ&-ˆ”Ýe¥ÔJTÝ?ªºø¤àìóvmnߎõÓuŒŸ”qâ³ý¶î—7d÷+ĸv¯"NÄÄŽÓÍç¸ÃÝxç '*W‚Î뎫éýF…v=Õôur}$@$@$@$@$@$@uš€]ðÒåäm»öµ¨+É­åÆÄ:GLñ‰.>)¬H‹ðŠqmn_óÙvÕmIŸ’Ot½ýNú;ló*fUEAÊkÞö"ðÄI€H€H€H€H€H€ê2¯Ûí´F!Ršö½tµ‰”&±v)dô°{ÅyÙt¼Ê 1U&ºøuÂäDµ‰®}W—õÙºÚTªOû"õŸos‹$Z˜qí¾ :/à´Ðó ŸÈ8;ç¾qãFÌš5«ÂrX¡Ï $™€ÖÜz„NkÛŦ}[›»Jlj×X¯‚”øªµUWñÉ @¤8õ©”<ªÛRt=4FýÚ¹Ã6¯Â…’Åk¾Ê̳dÉ,]º´ÂÐ~ø!Ôoß¾=ÚµkºínëÖ­˜;w.Z·n]!–      ¨n BûZ«P)K±}º4/›úTJLÂSÐ8Í›Y]Å'¿Å+PÛoÛl]b´/ÒÖÕçÚl»—Oüuºy~’YhòƒéΩ}¯õùåèСCÈå ¤ð¤> X°`äáã:uòKE; $‹€]]û2Ÿê¶ÝÏf—$Fû¶.9¥yÙÊ<Õð3Ï<’”#HóŠ bóŠ‘ùl»«K_m®d­Œ©¤È$Å&mv᩸¸³gÏ}ÓØ 4Œ’H€H€H€H€H€H€ªŠ€_-®MÈZ4ÎÕíuÚ1~qnŒ_œWuwMjKÖ´O•=¬ *šÝöž;–Ý9“Ö±ÀhŒÔ_™O.*{—“­oذrË´éÓ§‡$ŸùÂÀ$@$@$@$@$@$@UK@ë¶T]V¢ºHÕm»èµ¶%³ødÊÈ/Î.9ÜX;Æõ霮]ûöXÝ¡¤€êÂIÛE'=¹ÕN6      ¨fv Bëº$·oÛåÖ:{¬íS]¤Æè­x®ÏËnÇhŽ qýdŸ¢Nnø¶Bª ÅÎeÇÚvbûœìVºT«*fœ‡H ùt·dògâ $@$PµvÛm·ª³‘ (¯„Ö)\ŸÚu¬-56h‘HâƒÆÚó$TOvñ)0÷Dübl´x{¼­Ë8»ïæ³}îì“ ì ¼ƒž9O›H ®˜8qb]=5ž ÔtvÝ!ZMµÏÍö©]óy˜Äçe×±*5oX“LÆÇcZ€¬'ì˜C]/ŸØl»­kÛæoûu % $‚@´Z„W]¶¹ãuMvŒÚTFòiL•ÉšV|’(’φ&qn¬Ý÷òÛã©“ @" ¸µ»N!ó¸þHs»cíØH>;®Êôª,>Årò‘€ûù¼ì^sº6·_eð9 @'àÖܾpmÒwmçeæ¿Ûüò¸qq÷“ýÌ'w‰<1¿\®]úzØë ÛæÏŸoÛ©“ TuëÖá÷ßÇÖ­[ÍV¿~}ôéÓÍš5 Ï ºO€¯¡º“q†ùùù׎4ù÷$+++Ó¸9Ã5Ë¡6‘vsûêó³«?)¹’öŒ'w!U]|’ùc…-ÞË/6?»ËüÖíÐ@$P‡ð›ÛêÐÅä© @# …§ yóæÎlíÚµøñDZï¾ûŠgPÝ'À×PÝ¿ÆÉ8Ãõë×#'''”:%%mÚ´IÆ4ÑrúÕ,üì‘òy‰_e…'YHUÞvéăø"Áôó©]¥Î#}=ÔFI$@$@Q ”nø÷Ý-5B‹=ÏÆiÝ»àÒ/ ¢Žó (œˆkzvÇ•_zºi$ ºN@v<-< ‰ ºKÊ“]á$ü­_ŒüÚü»mëžÁµÈhŸ‹­G:… q‘rÔ_•¿†jÀ9WëЦáÁ£ÅÝ?$ï³KéÆï1êä½Ðµ];tÙÿ \sdù|q¾fõߎ‚‚lذ!ŒQt±IÓ˜°3±ŠÖ ¼ê2“k×Ùýì‘ÆèØ#kSñ)4½˜^±~¾HÒ+m$@$°c(]‰gk…æý¯Áì?”cÎCÃÑãâO_çÉ”`ásÃ]+NÃg+7 çÓÓКï"uþªóI€j#0£’Kư9/ߊûWž„÷ç-ÆÌqwã¾±ñÌW¶Ì+Và×_ÅÌ™3ñË/¿lW|›ø$Fb“м>1ŠÍË.ÓGò%ayÉOYŧÊÂô»8.-¿8µ«tDZO$@$àK  Ö¼Šk\ߘºì(Æ¢?rÐlÐôk†MÔª-ÄuùÊðÜH€H Ÿá…› o½wñÂøM\ì@$%K_ÂågA£k>ÂÌ%Ë3ósõ6J®-_bä®Ýqü¢ì#„Ïrh~ïêa8ÝÌ—óähÖ¸Îkø,Á¢w¯Å‘º£mÓ†hÔfóÏ X:ïyxóêC°s›&hÔ¸9:ìr(îøÊ-ß•býÄ[0¸ëÜùýÆŠO”Ûóúˆ¿|—ì×M›ŒÇæ¼3[î‚L) Ÿaá·#ѳó%øBà…Æ Ãío<Œ3uAóÆÑ¦ßѸsüꊹã© @]!PŠåï?¯ú]€QçôÄg/ŽÃj{îÏi.þw:}:·G‡nýqØåOcªìô5;_8¾=¿üý&ïcþ]Ýåz|[þfU8éoèßç |žg%½ç‚[_º§íÓ ÛuD߃.Ç‹3¶”E{¿< £¿~×Ô;ƒ§rŠüß_­i]Õ÷}Ô Ô~hÝÎÜóÆá‚®{ãöŸ­÷œïÿŽÝû^ƒ¯Bï9òþ~îyçqœ?¬/:›Û¥º:÷MXSëÞsJVå`qq Öõͳ€2³;a¯Ã† GæJ|`ÞË/o>¼¸;ڵ늳ß*+Vø¾v„©Ïœ».‘^#óñî Çb@÷f-Ñ{ïãpï7N¡5ÚkÐgmQÏGo]5§é<Š›Ïp#†¢o—öhß©7þô÷wBŸ‡¼†^~%+0ö†#pþ‹9Xò̉æuÔ—ްíVÙPж‘æÞež5”šz¦“<ÛI[÷îÝѯ_¿Ð!º6}þ“ŒIp‹V{ˆæ—å‰ñ[ö¶“÷‹H²=áD“¼^¯ôA/@Ð8¯9jûaÉF—à°-°¹ ¹ùÅX’›1¿®ÄÌUúf”Ð)#'˧5NEZËs0NÛ‹<‚^ –@Jvºl†—GÞ<ÿÉJE‹îÃqî#cÚÂ%øíÕ#±üÁ[ðÒüòQá<õR1®ûræŒÅˆüÇð×ÃnÇÊ3ßÀÏ‹âûÛºâóÀg’»x6?ûŒí| ¾˜»¿¿¶<47}a 7éMбwoìÔº~•ý 瘇Æã…³;¡ë¥Ÿb]î2ü÷H{Ëy*Zõ<=ù5æ®Ùh QÇ`Ù?oÀ³óJPð¿»péǽñÈŒ5Ø´n!¾{îï8ºo#ëUTŠM?=€ÿ:ûþ÷ücïìíÏ©h*î¿îS xò¬Y5v ð¢ð{<üØœ÷þ¬]¿ï°÷]õ(~Ùö»ƒµª$@$PGÏÇ/üŠ!g‹ÝO8ƒ&¿€·Gù3EÑ <|Æ…ø´ûøbÎRäLƒ¿n…Ó®~+Ñ: /~ÿºü)›'`ܤfìx|úµó&LÄÆ}‡cP=‡aáOxfL ®6ïy‹ý„Ç÷›†κ?„nŒò~iž­óðMŸc‡'aáü7qNÇôÈï¯ÎÔ¡n¤÷Q¯xµ¹swòžó#ÿÏZŒxíg,Z<cŽY‰Q7<…µì='½÷á8ªÛ׸íì»ñÎôÕÿ™)¥5Žzà%ŒÜ½!Ž||–-[€gO0ïå‘^;Zôty¶›ëÿùF¯AHú¿F ¾¼#?ë‰û'/IJœßðù#qDŸ†F‡:_ƒ&Â][Û™þÿ/èùè,‘^_†Ë#gžqnħ³– gÖWxéòCq‚CÍ—ÚGÞ3OžÞÏ‹–ÍÁc‡ØŸµ4ÐÈHs[a¢JA©}ûöhdž×)M¾5Ó~Öœèb“&1kªBŽäüZ£—œU&(kM*>Å 4èø q B¼}š¼¢RLY¶ Î^³sõÖð?oÛùB†â_pû™¡ÿRL56½^tØõ0\ñü/pêÜžãKVŒÇw=‰¯Wºÿrx†ÓH$@󗺢4ì|ÉÃÙäŒ5öy5¸þÎÃqøîíÑ0- Íö8ûµY‰ÅËuûr&Ÿ~ölš†”†}pØðî(íw.ڙ濎‡‚Ýòr°h™kæÛóÛ>¸ü†ƒÑ±^*ô8çVˆßÿ™{âê7Çá£Zo_¨Ñ…T±¬ßïµW42çÝ|À‘øSÛÈYVŒ´®;£Çºo0æ­o±¨ :Šþ­äíHZ*Jþx§Ÿðvzä}Üw` ïó)-Äž݇³wm‚´ô ¤ëð²$>?31ô‘Ú6Hk†'†n9ózl…ÏšI€H ¶(üåe¼²ò(œup¤6?gü^zåw&#O@IDATè»×ùÍ|oÏÛW\7íÍ7®§6Þ§ýíl´ùäe|º6† C§©_còV³ñöÛ±˜°Ói¸êÀb|ñéL™Q¿Y€½††ýg…²yêaØÙc` sãPFk ¹æR Yú1>šYV‘‰ø~iþÝïî8½_vøßýˆñ'ñ}Ô#>lò˜;ìóU2°ÿ9—cÿ6òžÓ{}0º.þ‹"÷ÍUŽzð>ÄßúLÇýÇî†~ÃÎÅÿ[äû\Ëȯòß¹žø=ÂççÔý®yZgó¸õßáÍ÷¾Ç₆è¸Çþص¥×‡ƒÈ¯Al·¶Hÿ/Tü2ÒëkËoïâ9ƒqeè3\ Ò´E—v ¼?ã8ç¤iî ã«9&hM"hœßéÄ;Þ/o¥ì5©øT©ð ਱òäûÍ›½o-»ý”|µT0Í[»ã¬íz:´{3 ëÚ'õk]!&£d-°æus;ñ„ öí:©MÐkðÁ¶Gkl™ù)=÷Üð…÷:·5Ês3®¾MŠOý1Ŭºø‚ØvþÔH€F £.5ž‰Ñ¿äWüëPÉJLxø>°7:·o‡vÆ“ ‹PªŸWR²Ñª¥ùd_ÞÒL¡&¥i34•‘MKIKGš¹±AnÇ/Y¹+6}‚Ëwno¶”›\íºàÔWÖbKîÆ*»Õ®lU~š­âãï?Cûv@óìÆhÔì Œž_vÞi}ÿã>û;ºLü†õÚG_ÿfê®±ÒUsãm˜°±>Z·Êöÿw8¥ºvkùC[rùzSš m›²¿ä‰%%-i%Å p¦ ! L`+&¾ð*~_òNïÓ]»öÃùo¯Ä¯¯¾X¾ÛÈ{é%«W`Mv{´ÝöO&ÒÌmrmKVc¥ùcHúÎCñ§ú“1az.¾ûÚx(?tÖýïøcý$Lø¥/†ñøãAJc´haŠ1å-ÅüÞ.{Ö® ½ÉEy¿l†.]šnûw?Úû«NbÉJ¿š÷œ s[9êÇ{N›VÛ¶~…ÞÏké{Njó=pú]cðíÌŸðâ™éx㬣pãxïßµ¢½vB¼ž¯K„kžfnó|ëý‘è4év¾ç>8ùæ·0K?[„/’Q"½%Î][”ÿìԑΣXò4©øÿ”=6^=ÒÜnîRóZ•ç8é3äíÖ®5¿w—7Ñõ[î$FbeL‚[ÔÚƒ5_,±Ö°š­ÖÔZC¢`k‘•jcÇŽÅóÏ?¿]J Ob&E§ë?ûN^zÞÓë3VAŽ7f¬¬0<«x°è*`îÉÀª§+ø*tÒzáìÿŒÃgßýŠÿ]ßiÅ9øßÇâΙHMk‡ó?6–‘Vô3nÙ-)é=pþ—àäÛ'"¿p*nÚ5)™ƒpÏïÛþ‘’U„£OFÿ6 P¯iw8ò]˜ßíxÙçøç)ƒÐ¥i}4hÖûüõ~Œ_Q>>të^Úž÷2>¹óXìÒÚähÒ \õ6X9Âɨ Ôz½/0·ß5ÀÓ#ŸÀÜ”m¬·üïVœóÈùÄטµx–-z 綯øvcÝr‘CjË6hÕüx<3ÏäYV~¬X‰œgŽÁ¶·ST™sËG鬯1ÏMÁ’õ¹æöºpQG=ï4Ùå$ÜüÂÌžúöùñ üåî)(ÿ»7†?8ßÝÝÿ1ã|L’bŠ|öé˜?¤˜BÒ¶Æ‘¿bÌïHVsÇX.ª$@$P ”®ÿÄúª%;l'4t(†˜ÏÇ›bÔ†aHøß| lé¬XUþ¹Ü˜Ks#gCS´lžŠXß/ƒÄ[3‡ÔxÞG£½ç¬\µÞsL£Âû”»šZØÏj‹½Ïù®¶ ¿ûüo›÷UsÛ^%f—\´×NùiÛl‚^—È×<Ù}à O}‚©ß<‚S¯Ãˆ.ÿla±ŽðÔ¨ k x>26ÒyÔoÞM7,Ãòm/ÿòé¶g¨ëˆEFšÛÍ#—o°³ JóæÍÃŒ3B‡èÚ$Fbõ¡äjO²”—•ñN•¨<ñ®c»ñú©x;G 1(8‘‰h1ç9æ˜cBóÚ(-<‰CýÑ÷é¼uÑBBþiÛ*°X?.À˜bäç•ݪ—‘Õ'Ÿ¾²Ì_Ñ?þà»ÐÖÐâ9ã0ö÷"¤÷8»çÏE®ù—²45=‚CÙ;5Ú†¤dÅÜxǨ×c'do™/F‹ëÞYWökÑ/¸ÿ¸cð×§bs»½°W‡­˜úêõ8êøñkøNÁR¬|ù|÷À/hÔ«;šæ-ÄøÑçặËs8† Ô&èyþ(\]ï)ÜúÚÒðN¤âM¹ØÚ°zvi‰úÅ«ñóKÏà¯Ý–N5½ïñ8¥ûxá–`Ë’éøm©)ÛLÁ¿Ž?#ß7&äJvH‘ÙµµQGôîÖÊœ÷*LyöIŒ[^ö©¼xÁ$|þË2l6ËNolœÞ* y[òÊÖÒmÛµB¯óŸÆ#>Ƽˆ…VAÉwÝé]н“ùÃÃÛ“°ÆÄ®ú£F‚M5†ï¢é  d(Á²w_À—½OÁé{¶ =`¸M›6hÓa(FœÔ½0Ö÷Áãé;Ÿ€SûMÂ#÷~†%æyL%¹3ñò?ŸÅòCNÅ!Íäór}ì3|þxóq|ÙÈìzêi¾ù¬Á~8tß9xqô×ÈúÓ0ô6¦íZi>¾zúqLZm>0.Ç÷ÿ;ÃwNG¬ï—±ÆËZ"¾n·Ø†ôNèÖq ¾xÿ¬•÷œÕßã±'?ƒU«‹0¸v¹ŠNÆ—Ór°.OÞà ±vÚ«xwjcìÚ¿ ÒÍÝ'Í›•bÎOÓÌïUò(‚R³+.Úkgûóz]"]óâE?`üŒåØ"Ÿ-µC'³«<üÙž2ÂkÐS=–ó‰té}Ʊ]¾Æèû¿Ä2yÆYá:Ì·Å uîXd¤¹Ý<²Ë^žã$Mžé¤ß²e ä&6}&”Äʘ*hÛ~o2É£G|™’8º¦ŸìSOô…ÑÜó6lØ#FŒÅJjåÊ•¡Ob»øƒ´%CO ‡6ÈHÅέ _ë†áã¬]ÒÑ9ÿ‰p òænÓ]Í<`íÙóǰ={ã‡f¡8«/Ž;fWt?é m`Þx?ú“ J°à£±æÁ²éèuâ_qámOáü¾æ~ó´î8ó‰qøøÃGðë/4¥%­pú˜ŸñýÄ)xÿê^H/ÙˆiSæ„nÏ(œô žúq3ÒûÿŸMû~ž€»öÍÄæIãѯ¶[ia+œa28éÉÑÇäÈ çpO} :@Àìª<ï¡kÐ:gQøV®Æ‡ýn0—ïf¾}Å<´óžÕ'àƒ*yúθìù§päÒ{pdßNh×¾+ö<ñ6|²Ø|Ò*Úˆ¥óæ`ÁšüQ|Êþóßq×ÞSp~÷l46¿èܾêd\vXÙy—®ú÷žÔm7@£vÂCEà‰ë÷E…Gh¦¶Ç‰=cgŽÄˆQ¿ú>["üªIë‡Kþu:|p<º6o‹¿#.ÇŸ¶mB ‡R! ‚@ñ<¼þâOxʱèPá·ó¬Â“OAï Ïã-¿›» .~îi½ävÔ³=:îò¼ÔàJ¼:ê8”=¢Ïì`|vY0)‡>¡ï~o„!‡ÄÒÙ¹<¼ÅÓxZ'iž 5êȾèÐi/\>e/ÜÿìµØËü[ëûe¬ñ¡%DzÕ5‘i;ãü»¯FûþŠ~{`Àñ£PxÚEدÂYD5?¦dÅ×uá!èßµ­ùãÐNØû¼÷Ñr䫸÷ðlóë} üùÊËÐvì©èݾÎ}{-J£¾v<Î9àu‰tÍKWOĨ3÷C÷ömЮç¡x´è,ŒºzÐö¯Ã¯A•™ß£ý¿`Št™»cä‹`ÈÌ¿ahóø„.qÖó¿£Ð‹¡•2°in$RˆÞe—]°óÎ;c×]wE“&MÂQ¢‹M|#±IhvÝ!‘…";o–¸”AêçgS»HÕeÅÚw¥¾5ˆtu‰•’£Jñk RþÉ—¾J±Ë!}ÕåŸCÕÅ.}õ‹”£±ÙZ7ÊȈMw;­Zµ ­ZµŠ©ð$‰Ï~÷wäšo¶“&·¤ÝØã%¤çM õC?JÌ~Ä-¿™mIÛ 9òPBìòó¶ÑäãöÂ-?›¿ž˜ý‘©™Ùæ+»‡cÄÍ⦣»!³t5^:¾Îü° ®ýz:ý½®ø¦núñ'ܶë"ükÈÎ9¹îøéܸ‹ 1Mn™kyÆ‹—W¾S•bͳF‡s>A»«ÆcöCƒ±þ¿G ÓyŸ¢ÅEŸbþãšGbÒµ}±ÿ‹°ß¨YÑTœÊqœyÀã[8e»ûoÿaÙìüI$dÓ§OG·nÝ’Æ¢Iß`fÁAèŸ:ß|›ƒ’Ôf{bÛ𖵺÷”Á Ø!   H fÍšÕŽ]L ‘r¨_û‚%Šdå©—¦mD­}ýeøGÿGCqÿv6Þ˜dxLûæõ°1g=æ–¢¨¤ܯFC…H€H€H€H€H€H€H ÁX|J0Ðd§kÐ4+ÐÍR׆ㆶý¾¬ø”’‰ÿÜ7 ÊB›´Ýðã^E7æ7ä†#Í®¨~;áÖ[¢_ËL4,ÊÃwfã†ñ°!µ>=¢®Ú£1Úe¹ë7âÕwfàß ÃƒCJv×®xêÔføêå_ðÄ¢¢m…-¹=ïš®Xñört>l'ü¹õ<4z ú_Ö Ëžþ-.+¥wéŽqMÃÍ÷ÎÆ$ȘnØøÉjtÚC[g hýz¼øîïxb^á¶Ü—À @ "ÀâS ºA–’n¾ÕÎn%fçRQÙ½dm_jP°§õz=֥ђ2½´Ÿð3rÓöÆÝÅ3qØP$E¡?kh)Ö®^‡Wß^Œï— £SWü÷¼pÂôŸñb«.¸¥÷\ûàtLÜšŠöí#{]±42 :tÄ#§¶ÀÔ7¦W,þõú¸c¥›ÖýÕç'Ó²1b¿ ¸ì¹É¸vs*v?h7Õô+ä¬oˆ#=3 ¥M·í’[ë>^<,½`SG¬Ëo‚å[[aêš~a{$%-+ Yù˜¿¶y©è7 =†6Æ›AiͲ1(+?¯,ÀÖü|,Ý\ЬÌÔ²úÌî¤U¹X0}îØiÜqB[œòÒr,ݶ<ïiKò°h}Ú5M—lÀ¦úM0bps4LYéO+ @­#ÀâS­»d@áÖâÐQš½í)ã©f‡Ð¿Ä¼Ü.h–µ?¬Þ ÿ[ò§˜ÎnÓÌEx¨o/ÜyÃþæ›îò1é›?ðÒ¬Fè Y5Åù§tžÍÌ o……øcîRÜòåFÁ<J[i>>yg]Ú÷s¿ÞŒõyÉ’ÍxåÃEØõØ]ðå>¥X½z#>üv1~<8Ó+š6       ZH Â NÖïçgS»HÕ%µö]©·þ‰tu‰•Çû¨¿ö¥p¦}±É!ýŒr]ú#‡­«_}z]ûå(¨¿º7†=4+Pl²ƒvkþ;þ½ï¦y䷳лéxsþ˜¹¾G;$@;&7NoŽnݺí˜'ϳ&    j&}•YÂ&s•òÕñòL»oëê—û‰D×Cú';QĦ~í‹›Ò\]ü^G(ØòI_ãÔ§Rìnó²UˆÑbO#;µƒÀôµ}ðà¯çcÆúžØTÔ¹… 1¼ý·øyM_žjÇ%ä*I€H€H€H€H€H€H ÎÝ?lµ˜À; …l$@$@$@$@$@$@$@$P pçSM¼*\ Ô,>Õ‘ ÉÓ        šH€Å§šxU¸&       ¨#øÌ's!gÝ7,ðå\¶lYLñ3H€’D`úôéIÊÌ´$@$@$@$@$@$w>EgT!¢]»vúì ø`ñÉŸ =$@$@$@$@$@$@$@$@q`ñ)N€N$@$@$@$@$@$@$@$àO€Å'6ô ÄI€Å§8r8 €?ŸüÙÐC$@$@$@$@$@$@$@$'ŸâÈá$@$@$@$@$@$@$@$@þX|ògC @œX|Š ‡“ ø`ñÉŸ =$@$@$@$@$@$@$@$@q`ñ)N€N$@$@$@$@$@$@$@$àO€Å'6ô ÄI€Å§8r8 €?ŸüÙÐC$@$@$@$@$@$@ÿÏÞDUíÿ *"›ì«ˆ¨¸/‰¹å‚)šK¹¦©¹Uff™öLSÓÔ4—ꯙeõ´Ì}­ô¹¥iZ¦F¹ï¬‚€ ˆ ÌÌÿž Î  Ã÷¾7Üåœ{–ϹÆÌsïP€(¦ƒOÅäé (@ P€ (@ P€ú|Òoà P€0aÌ[[0±}x{ lù:¼Óu¶%¨Ë®ÅaZY·¡ìz¯»fm“G»0ʯ/¾¹S†c¤»••00‹§(@4\Ú2 CžBMooøÖ BÛáXjqd2pò³Ùø­Í—¸y?ŒŠ…ÿ[‰¾ÎŠâZÄsUˆ>¼'³ƒ)U;aA©·¡ˆM–³g†c^g¸xxÃ[ñòk='2ä E\ë+ÏÊ„MD›Ccê±'ít˜ (@ T`« Üwv J\@‰[ÿ} a¯®Ú‡5O{£òý›øíxêØ§ò‡ˆŠLBÝ~ÍPÝ¢ ý¿ÔU4v}üÒ÷B›–¥^}±+´ôÂÈͧ°(¤R±‹Ò`ìòŒÓ*–B P€ (`œùdƒÀ&P€0[G±|Á?èºt ^oï ib’¥mM´ëÖ®šß@j¤þµ¯ti_?ÔiÙïÿÍ\é6­ÑÇá³5й¡?¼|ñ̤-¸‘‹]o÷Á{R±÷õúðmô¥jßÒ¥FâÑEx¡¹?|j¡ãØUøt|sŒÙýê;ß ¯÷l$‹ßǦ!Þ¼Qš†%Õ÷r‹)زwz7®‰€QÛŠtü½fº4­ _O/Ô}zÖ_NT·±cR?Ì=z ;úÁ»öxìIÉÛ†"÷+SnS¯õ¹?:…O×Ç+{Ór˜þÛt4mö.NJ$:·Ú¥û»4€Ÿ?‚{LÅö›¦2ÉÀ5 ý×ÒþÆÚñTÓžuðÔ‹_àœ©Œ©ÎÁáA P€ (Pz >•ž5k¢(Pá2/ÿŠã: w;è¼!.íWÌyi)Ý… ×qju;üùæX|qU©±RÇoÅ7ŸÃºÓ7pëÏ%<ü,;æ„°¶av'[<ûéeDü½´'ïÜ?ˆÙã×ÃaÆQ\»yß ¸†u[b¡*„¾*n+þàŠ¹G®ãÒça°…Üš Å;ÏàFÔßø¼ÍiÌ^þ Z¸£ÏÇK1Ô§ Þ9| QWW¡§µVOÒ¯£91­‚ÊfS§ûñšèÓLJvþЬðS:NïÜ «^ýÐL»ï…iòƒ˜7r1RFíÀÅ[ç°¡ç%LŸü_Df S~qò;×VêÏ‹17v ~¼ƒ¨+°â•öð+‡âŠCÇs)@ P€ €>ŸôÉð8(@ [@õo’íQCÏMÞîÄOU_Àëýü¥;ç,aßt^íp[÷Ü„&üdUÏîO)¸aåÚÝ›?@Ä­diþ‰þ%ãÏŸpÐþ¼æ kéžÏŒÃ FÚÑ)ýçB튰I/£±½¬*‰F[ÀµÙ3hãS –NhÛ© 2ããf¨ÒY%Ñ/­~²$e4ÖðÏyæSÀÈ-H–KÒé~µûôç¡8&¢Oég°sŸ5zõmn¾òj¿¼÷åòò­3þÜŠ]ðÆ µQÕÂâá©}ø5§ùN(Å]Cc÷ÐÀµUÙ§&œ¯îņCWVÕ-Ÿª[×Rì «¢(@ P€&"Àà“‰ ›A PÀ,ì`{/IY™ë¢2ñî:»Ã5g†ˆ5\Ýq7>1k¦’…ôPìò¯*,-- Vž£¼›€{ÚeJe¸:Ëeäo‚jí@’¥'|=Kwý‰cŸ4Åñ7ÞÀw±º®ŽÇ8x€ (@ ˜½@Ù¿Ë3{bv @°éŒWÇ»bË›ñåqÐ<ÕH™†ØˆÛšçUjÞ}Ô›±|ËuÄì. (@óPßǹ¯_Ŭ¯á^¦U\›ã…W` ›˜DË…ÅàµU <žJ P€ @E(ì;q]ùô“‹µ¼-Œåýükyö•XçßyÅ-sòZ¤Ëû"p&ïË·Ù‰}ù¶:í[í´·åtÞv'aq¡Ì_ ÂßvgþCÌR€ (@ ˜°o»Ë ö˜ð0±i (@ P€ (@ P€åU@žiT^ÛÏvS€ (@ P€ (@ ˜°ƒO&<8l(@ P€ (@ P€(ï >•÷dû)@ P€ (@ P€ € 0ødľQ€ (@ P€ (@ò.ÀàSyA¶Ÿ (@ P€ (@ ˜°€• ·M£(@è½ü‚ µ†M‘vN ”7¹¦(@ P€ €I pæ“I E P€ (@ P€ ÌC€3ŸÌcÙ P€¨ ááá´çì6(@ P€(=àààÒ«Ì kbðÉ •]¢(PtÊé–„,ˤ(@ P€0eþ±¯ø£ÃÛîŠoÈ(@ P€ (@ P€ ô0ø¤†‡)@ P€ (@ P€ Š/ÀàSñ Y(@ P€ (@ P€ €ŸôÀð0(@ P€ (@ P€ @ñ|*¾!K (@ P€ (@ P€Ð#Àà“¦(@ P€ (@ P€(¾ƒOÅ7d  (@ P€ (@ P€z¬ôça P€ (`²ááá&Û66Œ (@‚‚ƒƒ fR«ÕHHH@bb"ÒÓÓ æ-l¢µµ5jÔ¨ggg(ŠÂžÆ|F`ðɈ,‚ (@Ò )ýJY#(@ P€Å8vìXeˆÀSRRêÖ­ ;;»b‹D0+99W®\ÑÔíââR`˜Áx¼íÎx–,‰ (@ P€ (@#ˆOµk׆½½}±O¢9b¦“(K”)ÊæRº >•®7k£(@ P€ (@ P q«{eë6>c·ÍœËcðÉœG—}£(@ P€ (@ ”S’x.SI”YNyKµÙ >•*7+£(@ P€ (@ P€K€Á§Š5Þì-(@ P€ (@ P€(UŸJ•›•Q€ @qÒLAPãé8‘Qˆ’2N`zã L9bœ¯ç-DÌRÆê{'ðñ€Æð’¾BÙ³ÍXŒª‹7~yÂñÏø ï4 Â[¿æb+㎳z P€ (` >™ðà°i Ê€:kûºÀÎé)Ì ÏÌ×|5nÿw<íÐüýÓÈŸš/3w)ð*D¬{ ã⧈8\Ý=®Š'(†§P€ (@ U€Á'£r²0 P€ª¡¦O6}÷;is¨nbËwíV ð—6 ·' Dä(8´l‡@KØØÛðZ3.K¢(@ P€O,À÷ÿOLÇ)@ P@·€Mú÷CåßáÈýÜ™ç6`C\/ hm™{PÚʈڋyƒCPß×^þ:q ÎÜSçäI;·»ÁÇÃõB^Âg=€µ<›åÑO[³5æœÍG•qò]4 zºî´ÊŒÙCBP×Û¾A0nõi$çV•U§¸U¯ù³X°m%Æt ‚¯‡Z Ä¢£‰ÈʪBÔîÐ>µ½ÝáЃ–Ç]‘¨9·lY!õàíYMûÎÇ¡+'ðÙ¨öòõ€g½xå»ËoÓÛ¦´C˜Ò(ý¾¼Uކéo$%%aÁ‚˜1c†Î—HyJbÙ=µF~¨5ÏÁÃ¥&Æÿ¬}»œ ‘»¦£oÛ ø¹×€³os<ÿѱìq»ŽmS{¡‰¯œ]<𒝉jÜûm.:Õë‚$g_ ù²p— (@ P@§ƒO:Yx ž\@   ÷Ÿ±~ïÝìéðûúÍÀ CÑÔB+ yË_‡}sqðJ "Ã7`hê2 ™¼ñ"˜“~ ‹F¾ƒ¿žZ‰?#¢ð÷¶±xðã.D?I4Fy+GŽÅßY8x5 wŽBÚÒxï`þ ƒToÆ)¬ü" #~8‹ˆ¨ ØÐ'˦­Æ9MŒË5:côŠŸñ×­hœÿ¾'n/™…u7²•ñ'V¯SbҞ󈼲#}†¡¡s?|ÎFÜÂÉÙ5q`ÆbìO“ê1Ô&+{x׫‡Z®U!ÇÚ¤3L~qrrÂðáÃammýX[Å1‘&ò”ÄÒkÑ>|9Ü~ãw#öÎM¬ ­¤UœžÁËŸíǹèx\ÙÔ ±ÍÀ·×UH?°“öÕÅ’3ÑHˆ½„_¾˜Š^¶Z窑zv¿ôZ¯ÚˆwZÚ•«1Ñê7)@ P€ @™0øT&쬔 €y ¨µñüІ8º~'n‹ Rêa¬ÛíŽ!ƒa©VçÌɼ°[¯µÃëS;ó2`Q=C¦„ÛÞõØ—¤Fæ…ŸðSlGL|³=\¥8‚µk[¼örGHY‹¼d^ØŠ çÛ`â´®ð®b›Ú1:4?ï<•3 )·ÐJh7j"Ú¹IK4íÝ5£®#BŠ«‰¥j`gôhâ‰j––plÚmÝâu;;Öö2š9XBQ­>B;@Ý ?Æwp“R¬áݽ‚F""Q%õÏ@›¬›aòæ±8̵Ü:<==1tèPXJ>ò"¶Å1‘VVKÕ .èÙÔ¶bÜšõ@Hö¸YúÕGÀÝãØ´í"ÓmáÓìi4r–C~PÝø£ýþoÁüNNån<ÊÊ›õR€ (@Y€Á'Y‚k P€0¢€^Ï EÛ¿¾ÃÖ›™Hüi7Šþ¾yí¨âhç ÷ª¹U[J·×¹«/gTI ø×Þ žÕä@€øqu‡CÞbrO[RpK×¢ŠA\ê^L ô„‡t+‡‡—„´”äÇokSØÃÍ¥JN1 K+Xª”Є—Tñ8º|,z´¬_O©ßÞøüVfnµ ;¸8ç†ÇDÐEáà‡ì.hÊ’jTI¥ŠÔ¦œÖ”€€ôïß …BóÛâX™-Ò¸Y:]›Ö‚§« œ=žÅJéÚ—‹eàØþãTøþ6 ݵÀ€ÿlÆE13M,êl|~M®"ku>C*K…?)@ P€ @‘ ½}/RAÌL P€ÐPÔxúEbÃ÷°yý<ýbØcßÒ̵‡f¹+làîîŒ:£Vâãfûðê«ßåÜ~YÖ}bý (@ -þ;ôè€W·Þ†Zßv…2­Î3ødZãÁÖP€0+Ê­†b [ \_†§tM/²¬‹ _¯Aïè9èRÇÞ _À:›Iø~Y_¸ˆ™G•[aÚÚ9¨sp4šøø A¯¥È|a$ZÈÏ‘¶ Ęù“áùÓP4ð­ý–!cÈx´•Óµ5­ñÚ7«Ñ3fzùÀCú&ºffcoTÑ‚OÕCßÂÌg01Ø õ¤o¶Kèq]lžì9@†Ú”™Œ˜kWp3ñQ¹>iAYnÛõxï·ñ´Ry-Òå}«ìüb_~‰tñ^QÞyÄKìËÛrº¼o«V«—Ié\(@ ˜¥@xx8üýý‹Ý7QNYß:UìN°³×£)̨2Pv„ (PŠÇŽ+ð}eIþ®/LýÚÅ}lgg÷†Tž˜¼/þê)^â¡â+ µ÷µ·åt1M[lË/±/ò‰çˆcrº¼/Öâ˜x‰%ÿ¶H×õÒdÖJûr>9M^‹ãù]Çò䑃=yr‡ (@ P€ (@ P€Æ`ðÉŠ,ƒ (@ P€ (@ P@§ƒO:Yx (@ P€ (@²‘côêK¢L£7Ò dðÉ •]¢(@ P€ (@ ”gkkkÜ»wÏè]eв¹”®ƒO¥ëÍÚ(@ P€ (@ P€(@ F¸zõª&eŒÙJ¢ xeв¹”®€øÆ7. (@ P Ü ˆoªáB P€ €y 8;;k:vùòe¤§§¥“bÆ“<Ée¥PR(Ÿ ÅÄL (@ ˜’@pp°)5‡m¡(@ PÀÈ …...š—‘‹fqe ÀÛîÊUR€ (@ P€ (@Š"ÀàSEiö“ (@ P€ (@ ”ƒOe€Î*)@ P€ (@ P€ @E`𩢌4ûI P€ (@ P€ Ê@€Á§2@g• (@ P€ (@ P ¢0øTQFšý¤(@ P€ (@ P€e ÀàS ³J P€ (@ P€ (PQ|ª(#Í~R€ (@ P€ (@2`ð© ÐY%(@ P€ (@ P€¨( >U”‘f?)@ P€ (@ P€ @X•A¬’ (@ K <<¼Xçód P€ *¶@pppÅ(åÞ3øTÊ଎ (@ã„„„§ –B P€ @…8vìX…ê¯)t–·Ý™Â(°  (@ P€ (@ PÀL|2Óe·(@ P€ (@ P€ €)0ød £À6P€ (@ P€ (@3`ðÉL–Ý¢(@ P€ (@ P€¦ ÀŽ›Â(°  (@ P€ (@ <‘ÀÍÈ(ÜŒŒÎsnǶ­óìs§l8ó©lýY;(@óPÇcm_8º¸ÃÃÃ޵љêȰ] Ì@ÀÂÆ mGD‹‡—p1J)õH…¨Ý30 }0j{K³£ZcÐ’ã¸+âR'0½y>9²ov ‚·O¬ŽÌ, ,ز#BêÁÛ³&šöCWNà³QíäëÏzðÊw—sf/dÆìÇ‚!!¨ëí ß N·ú4’UñØ5¹&îˆÃî ÒŒ­š¹%U£¯3¿¾¶Þ¸†íÓžC‹/© _ÔkÝ •ÊI;„)ÐïË[&>ûË .8³é‚ ³éL¹ìˆ˜9V´EšiV´˜› Œ(œ’Šó—¯¢Q`]΀2¢«1‹bðɘš,‹ ´Tx˜p?/ý']ŸF»Úâ/Q¨Ð£WüŒ¿nEãü÷=q{É,¬»‘}S^æ_XþÞ4]~·nlÆ(o+Ãù3þÄêuJLÚs‘Wö`Ä£Ï04tâ‡oÂÙˆ[89»&ÌXŒýiRÕÒ­1+GŽÅßY8x5 wŽBÚÒxïPU„-^‡)Mª¡çÊkˆ½‰µýmõç?˜"&-ùÚ:ìòLÙ_ý~ ±‘çq`Õ<[¿`eïzõP˵*?œfÉ™üχ7öbñËÏ ÈÍUl\Ñ Ï<ü’˜=sOÜbÕ®^^ð.´ò‡S5{øµ{ß_ɾA« ôÂô^Q–q;1-´.œªVEÀ¾Xöçýœ3Õ÷NcÍ„®hèmªÕÜРÇ[ØtõQNzq72#ÂûZ¡–su8x7B·‰ßâŸÜê ¯º± ÚÎÃñ“«ñJçzp®VŽOÍÇÙLé´G›ñ‚C¬Š‘gAf"|v3Ô™ü+4s…ÝÓ!˜óÓfLí®»ï+/Fbúm˜Ú³1¼ì«Á%øy,;yr+E±šKÁªøý3ѯ…mìQói­k¢ ~‹ò“Žañ Íámg-¥@–xYØ`àãk1xx*(@“x”ž® <Ö €}õêRð‰·à™â€1ødŠ£Â6Q€(×™89³¼<¼Pç©!X|­-–m™ƒ6YªØ=šx¢š¥%›vG[·xDݳ¢¤EÆ£çbX;XZU‚•4•À`~X#dØËhæ =ã¦Z}„v€ºAŒïà&¥Xû{7?ŒDD¢ ™¶bÃù6˜8­+¼«XÀ¦ö@ŒÍÀÏ;OåÌŒÊjDÖÏóçkke?)Àôïoؼã$¢Ò«Á»i;4r–:`Ý “7ÿˆÅa® >i—ÔvÆ1¼YË*ëûü!^ZWíû4óÙ J—æ§Å:”vÿÁæ¿nã^ì1Ìpø¯.>?Ñ,™RPõ`% ûêwDÆþ‰¥ Á+¯|‰ùÁf¥Ëåè[«bðý’=˜sщWðMÇ‹˜=oGÖ Aõml7{j¿ƒ-g¤ö%ü /ÞÅÜQŸàrö?#}Åê¸T]Ðo~ô}×óûJt¿þ.ÂÞøÿjGc ¦¼¼c¦‡£Åûpãî]\Û1 {DÆY|~è_Ùà™L¤(`Êò3žäµÜVy_^ËÇŸt]ÙÚMiOr"ÅÅ´ ûVÀ´ZÍÖP€ € ˆg>Ä|¥yNùé·£Ò³•æ¯û碓‘¡V!ý‘3FËjŽðó{±FŒ@IDATsÈ Ò˜ß.ιÎ,¥€–ÂÁÙ÷¿(,­`)}„TIAU| âR÷bb '&ÉÍR)aÕ39÷C¦|\ZÎ_È×VËú¯cËN_üß²9è±è_½0s¦õG½ì ›VÑÜ,IJ!øøz&>ÖWGAéÒUë×kÚó‰?þ¾Œ›Ñ‰È »†Gh Í'…º¿ù<×ÐZªÅ}¦¼„ÚüŠÓéà+³d(½Š¾†iWØ£ßüUÓ*ëÚîÖ'6sc ÅPáp÷g¬ßy{~èŠío垣¨>¤èX]ËÜcO²¥¼¸Û"C1knoÔ–&îÁ®ÞX0ë»nÆñŒgñ¬èr‹ZÙomø#]³ÿ!º¦ÓÙ…*ìÐwþç:ûoiéY}î÷FcÑg—žþlÖÝÌj“:±sqQól†¬ÒtýÌD|Ü]¸7o‹ºŽ¶° n‹Šu8#EÝùV]—Q€¥£rå¬_L"ÅÅ´øʹLJ­£(`Viÿ{£V$bÒÆ#ØÚÔ U•¿bjÓ yú(MRÉYŠš?çDÎnpqê‡åžùBÒÙEµr Lœn0¿ô|*±h·UÚƒ]P_L[Ýo%œÄ²‘C0bI~}¯IVÀBs˜¼€*_jÅU'à?“VàÝ Øüø"‚v«ò\yúñè!Ò­« òcÑÖì\¥ç)LìHÏ|²Ôú‡ U-¥@­fb•8lÝŸ^ÚW}ôU˜U æšÖ¾¨³çüÔ™®VC­°ÈûÌ)i_!Ž(+§PiCQÉ ÎöZí×JTHóÇ22EAYéÊÌüÓµ ô]«cnª33i!Í•ÔÜM5dž¿ ¢qÒðPº&¤I–šÿ°îwUt{m4f=Ó A›Â>öÜiööómº äB ”_Žm[çi¼˜é$–üÇód2°s32JsKÈ"žñôÏ¥ËhÚ0U«áÊgRÉ ~×R²u³t P€¨`ÊÔ<¨&ÝŽçç,žpvÝWØ/ß«ô8FQó?^Bî« ~pKæïÅ­4Q§ iÑá8#M±°‡“£WNÿ…éC~¦ôáØ`þÜbs¶”àð¹ÛH“г²õ€4#ëaÚC¨Ó¥[púõÀ”qúƒ9¥p£Ì”×qúïªxjÐPô©…J‘ÿÃç›ÿÊz&‘Ü8u ޝÿ¿Ç¥!5êW|òî$<ÛOÉ_.WPº\άŽÝ1ø™ ˜ÿò\ìü'iéiˆ9µßˆÎ;ƒOš‰SÃ1 gŸDƒTDGÄç½½TOºeýÞèëõ#>˜µW“3ð ö¸Ô¿ÕHì5!Åý£²…¼œÏáÇíüè.oÿ¦|y ºb7O@óħXÕëŒUwã㥿"6-1'¿ÁGëÏçÜfY(su2ެýÇbR‘-™ÍXƒ$éšh#®‰û‰‹?Ïœø~Ñ\|²ç.lÍcòž¸W<‘ €ù ÜŒŒ†@ɧzµx*GÃÌàS9,6• @y¨úf¶8ƒ‰ÁÒ7ÂÕ“¾©.¡?Æu±Éžñxÿñ´ŽXâµoV£gÌô ò‡ôíxÍÌÆÞ()Z¤¨^“^ƒûžÁ¨çY£·&Am(¿V±ò¦:á– o‹O7xÔéŽO3_²ɭP)31×®àfâ#Ÿd,S^Wj‹×?è€^oçõÐóýcpïÖ ÞÚ³b,ìànqSÚúÀ³ñpltx›?~6çvO”^œþKŒ!_íÁœ:Ç0½‹?ª{¢íkÛ­Vä½¾¤ÛBŸ›ö<6÷‚§ƒ7ÚOÜ‚ëÚQ}镚`Ú–¯Ðíúltõw„{ ñþ9ع\«OÚþJ­1ñƒçðaK¸Ôh€w`îìȽqöI .æyU;`ÎwïÂeó ¨ã䉧¦Gýn-¥§Æe/…1·ª‹¶uÿÆÌŽ5á;‡äÝËmáš @¨¿Î_„<Õpt¨àå«ûºçD?Þ]ùô“‹µ¼-J”÷ó¯å˜XçßyÅÛ=y-Òå}1YÞÇÄKì‹¿1Éû"x‰}y[N—÷mÕjõ2) (@³‡¿¿±û&Ê .v9,€Æ×cHHˆ1Š*Zšod댫ï_Çên9á‰Ü2 JÏÍÉ- d ¨¢°éÍ%Șò†øˆ·§jdü{ó»wÁ…©±ØÐŸ·“ðR¡ÌC ¸·ÝÉç š>^9·à=‰Î±cÇŠô¾¶¸ïƒíììÞÚ)¾ûDú«§æ%þ$$¾ìU{_{[NÓõŶüû"Ÿ¸‡]“Óå}±Îšâ/mèØéº^šÌZib_Î'§Ékq<ÿ¢ëXž<â7 P€ (@ H·f*ÅÛ:}KAéúÎãñŠ)pÿ6nøu{Fàž{MØfÄãüÁM8x§5ÕଘJì5(`Oú¬']]3 ÄÂoµÓ¥cšÇ|2Íqa«(@ P€ *‚@õ0Ìœ“&¶çµ$dVuEݶ}ñÚ†ï1¶–|S@E€`)@ 0fðÊpML- ŸJB•eR€ (`ž–u1õ·ý}+(]ÿ™L©°6h4jJ/. (@sàŸSÌudÙ/ P€ (@ P€ (` >™À °  (@ P€ (@ PÀ\|2בe¿(@ P€ (@ P€ € ð™O&0l(@ P€E_“Ì… (@ PÀô|2ý1b )@ P€È'œïw)@ P€ LU€·Ý™êȰ] (@ P€ (@ PÀ |2ƒAd(@ P€ (@ P€ €© 0ødª#ÃvQ€ (@ P€ (@3`ðÉ ‘] (@ P€ (@ P€¦*Àà“©Ž ÛE P€ (@ P€ Ì@€Á'3Dv (@ P€ (@ ˜ªƒO¦:2l(@ P€ (@ P€0ŸÌ`Ù P€ (@ P€ (`ª >™êȰ] (@ P€ (@ PÀ |2ƒAd(@ P€ (@ P€ €© 0ødª#ÃvQ€ (@ P€ (@3`ðÉ ‘] (@ P€ (@ P€¦*Àà“©Ž ÛE P€ (@ P€ Ì@€Á'3Dv (@ P€ (@ ˜ªƒO¦:2l(@ P€ (@ P€0ŸÌ`Ù P€ (@ P€ (`ª >™êȰ] (@ P€ (@ PÀ |2ƒAd(@ P€ (@ P€ €© 0ødª#ÃvQ€ @É dþ…%aÝ1ÿŒ’­‡¥S€ (@ P ‚ 0øTÁ/vŸ €É ¨ã±¶¯ ]Üááá‘ýòAت[P§ñV1e×^¼Û²RqJ1‘sÕH9½;.0f"ÂfP€ (@ h Xims“ (`¢Vh=ç$~œà þÕD×¥àÀ'3°µW/ô 4‡`š®>ò(@ P€ @yà{øò:rl7(@HJJ‚ 0cÆ /‘&òkÉ8éÍŸÅ‚m+1¦c|¥™Q­bÑÑD¨•—°¸ƒ7ú~ µ\‰ò –vòB¯ÕQP‰saÊ‘t@SN>9²ov ‚·O¬ŽT!#j/æ A}_Oxù7FèÄ58s/»4Cu‹ú4é=°`Ë Œ©oÏšhÚw>]9ÏFµG¯<ëuÀ+ß]†ÔÍ’³ †„ ®·'|ƒ:aÜêÓHÕªKš¶kr7L܇ݤ™a51rKjv‰\Q€ (@ P ì|*û1` (@ ˜¥€““†kkëÇú'މ4‘§ØKÆ)¬ü" #~8‹ˆ¨ ØÐ'˦­Æ9u•<«¥(@¢d?óIú6;ÍËa«nIá™B,UÛbPïTøÅ6’žÒTÈŲ.&|½½£ç KOx7|ël&áûe}!eôÅ*¯}³=c g<¤oÇk6`6öF"ø¤¨^“^ƒûžÁ¨çY£·ó[Þ9H P€ (P‘ ûöYW>}Çäãb-o cy?ÿZ€‰uþm‘W<¥U^‹ty_Ü2(ï‹câ%öÅç y_ä/±/oËéò¾­Z­^&¥s¡(`–áááð÷÷/vßD9%qË\±Æ(@ P€ (P‚Å}lgg÷†Ô¼Té%þª(^âñ¤âé¥ÚûÚÛrºø[«Ø–_b_äÓÜÅ19]ÞkqL¼Ä’[¤ëzi2k¥‰}9Ÿœ&¯Åñü‹®cyòÈÁž<¹C P€ (@ P€ (@c0ød E–A P€ (@ P€ ( S€Á',•- ¦(@ P€ (@ P€à3Ÿx P€ (@ P€ (`ò‡Ÿ,t;¶m]è¼ÌXòœùTòƬ (@ P€ (@ TXŸ*ìгã Ê‘@æ_XÖóÿÈ(G6¦Ö-ã¦7”#éº]Pºî³x” (@ ”ˆ€˜Õ¤ïU"²Ðb 0øTlB@ P€9êx¬íëGwxxxd¿|¶êT9™ž`ê1¦ìÚ‹w[Vz‚“Mí5RNoÄŽ ¥H3+·BŽ£:«»WU•j°µµ…­ƒ;ê·ŽÇA¥ŠÀšg]Ðàx¨U\ú™÷Ñ̹zÙgc[•,,am#/Êpê…5±·¥r«£ýò›y¯eåÌoQÏ­OÑ*ñnìùÞ®7;©Õà×¼¦|{÷ÔÙÙ²ÛY©ö+Ø÷¯|P¤)qia¸ÜƒGšÝ¿1§iu ø!-ûÄìUæYÌ vÂÐíš\@aËÓö©^ÕíœàÕ 3F~|ñ⩲hõå8 '[G<³âTÚuhŽ?AšÔ6[{WÔ~jæîB¦èv¡Ë­›jðlÐ #æíÄÕ|tÙ‚:W¯ü€ñMá6f/ô„bužÇƒ (@ `ðɰS)@ P ÈVh=çwDÇÆ"VóŠÄ®ñ~üzÕÇød¶œ/…àSN…ÙP@¡P&c9ÈS mžCrj*’cNá‹^w°äù‰Øt×Ã罊Ê_½o#²Ã¡ª|?{”ã>Ãöó÷*“š¸ƒë`òá³ö“vãe÷ÂÚ¨³q:¾ô#\'n¹„ûH½‰£KŸAÄœ®è¹è¯¬ ’F±2ª%ü¯Ï:Œd£¨¶¼lŸ”¤Ü‹ÂÉÏÚãÚÜAxwo¢4šöæ:kÜRïâÀëþÙÿÖ‹™&Úvç~ö«†¼ŽîȺ”{i)Qøý«—á²o,Úö^ŽsF’2±ûm<ÓíCD88ò¿WF¹Y(@’ÏÊÿ*ÙYzq|*ŽÏ¥(@ ˆ[·š?‹ÛVbLÇ øJ3£Z Ä¢£‰P+/aqoôý:òGL(¯`i'/ôZ•öm_šrÂðÉ‘x³K¼}ú`u¤ Q{1opêûzÂË¿1B'®Áyš‰¡ºE4é=°`Ë Œ©oÏšhÚw>]9ÏFµG¯<ëuÀ+ß]Ι ‘³ †„ ®·'|ƒ:aÜêÓH7T—43l×än˜¸#»'H3Ãjbä–Ô\¤97Q»g`@û`Ôö–f—´Æ %Çq7§Þ|.7ŽkÝNgà\ÑE%$ŸX„ÁmêÂÛÃA]'â¿çt#ô÷ý¶O{-¼¤¾ù¢^ë¾Xø«Ô¿´C˜Ò(ý¾,æ ¸\©BoYØxãéñCÑúÁœ‹TºÉø Ï%,ZxB>í×ðá™PÌÒ• ]ªŒ§°bÖ.øÏ\Å/ÃÙZÊkQ ¾í_ÇWŸõCÔGs±51û*WTCÏéSáüß×0G8wy’ò6ðn? ½SpóÖ½ÜÅm‹ÎWX» ù‹Ð2ý*.G+‹V¢…-¼[ÅGÛ¿@ÏKsðÞÆ;xtèuØeÏfË™‘e çÁ¥kA…û÷\1fÇ!,èä€Â†‹Ö(æ¦(@c èºíΘå³,ã 0ød\O–F P€†¤æ+¿HˆÎ""ê6ô‰Ç²i«qN]Ïj„S›w!*{BŠòÒVl½ÞCžóz|‚ô,£åï@Óå'pëÆfŒr¿€å/ŽÃ¾€¹8x%‘á04u†LÞŽx9š¥¯nÍýÙ4+»¢ÿ38ràw Ó \K–f¶‰ÙmZ¯„ï_€­z ú6^ ®.ÝÞ'ÿGÃpû˜J P€e+ÖSQ¾ ¯l[^1k×~WZ1Øk P€0²@&NÎl/ù™O^m0[þ°Jh7j"Ú¹IÓA,дwWÔŒºŽ¥|ûB«¿7cûMHÉÄ?[·#æ™ÁxÖEGP@Æ£çbX;XZIϺ¸[¯µÃëS;ÃSš¾bQ=C¦„ÛÞõØ—$ÔW·Ü}k„ {Í,¡¨V¡ nÐã;¸ÁZúŸw÷n~‰ˆD2/lņóm0qZWxW±€M횟wžÊžUP]rù×;T ìŒMäWrAÈëãÑ>ögü|QŽÎeµÕPß•¾R€éßß°yÇID¥WƒwÓvhä,5º&oþ‹ÃJ+¸‘ãï4€­=<‚ûbþ•§±êÇEè,ÅÁÄbá7¼œŠ%ýûbÑÝ‘˜?Z¾U,+]ÿO¹\­Ù3ö-0ûl®‘ên"þ…œu³,PÃ>IwåüÒµ”i…o|Žé_âµ…§ò<‹J;ô¥¶¼Ü~ØT©¿>[à7c)FÖµÔW°žã¹åhžeß3OÉ}+JZ#¼{R¾ U>¯ªÙûcØo]ñýEè`#7ANÏ{íså<ÚkKÔpvÀÃ{ÉkÐNå6(@ ”_Î|*_cÇàSù/¶– @9È÷̧è˜Õ"ûAá {¸¹TÉéƒÂÒ –*¥ôˆeiBŠ[·¿Š-Û®@™ñ¶ì¸‡ƒ»Á!_Es²Â~~¹·Æ¨âhç ÷ª9EÃRºuÌ]•€x)X”uŽþº³Òíàâœ{ã•¥ÜQHÏ~‘ë×´Uº5G%§ŠA\ê^L ôÌ~¨º—$=g&YÊ!-ú©©ËÀƒªx]>=ZÖƒ¯§ô@wßÞøüVfîD|.yª)ðÜê¨QCÜ#–½Tq[õ‘”í—}ØPßõ_Ç–Sàsbz4kƒ3·àRqgóÈí)ÒZ~.P îÆ^Âñ ]­‹CšåÒìé‰E«©o¢Eî°P‹\®ÖÌ™{§0«Iî¬ ÇpD"îÈ×v‰™‰H¸g 'ÇÜüšäJAx}ÕtØ~þ–üõ(ß³·Ä³¸¤É8Úåh¶¥#Òÿu>¦Ë`yâäÜ~¤¥?DÂÉÙ¨þYô\zAú·X”úrËÑÌ$ºwsZÈ}+JÚߘßZþ2ù¼œ_Ö¢Ó`ç,§åm{VÚçj`òýHG\lì]]ƒ·Ýå;» (`òœùdòC”§ >åáà(@ ”´€ÎË¢R…BwEì¶m?¹»2D0*gºÃcÍÒ.ÇBúv=ç{ш¹Ÿû]‰ )€âœû«NûœÇ MÐèÒ‘ÑÂÙ .NýðÕ5ù¡êÒ:.‘_õZÓ_–ôá^*3·¥ù*0àö¿÷1jE"z®:‚KQR[0Ú3·¢$}õx®:ñw´¾îAÆd|‚·¿¸ö˜ea‹WßÞõ*¡S÷¨fð¶»Â–È| (`*œùd*#Q¸vä}GY¸s˜‹ (P"¶£gòÌ_ºUú F«BÎH± ìÁ N`ÅÂýˆ–žQ¤J¹€õ®ÅínƒÑͱ¥"ôÈ*¨Æ’ù{q+MÌ R!-:çcä[Ž fa/Í|QãÊé¿¢·I=†Òç LMÁƒj^¨ã猪Êœ]÷öÆç™¤¯æÏ•žtøËÿÉ鿤Ì[±G<{ G <“%«dC}WFüÃçn#Mb°²õ€4“ìaÚC¨ÓÿÄÇýz`ÊÎ8ýA7} /oÇ­šcâì0Üú`(ÞÚŽDñ-kªûˆøe9F½¶~'~5t]“•PïÕUx§Ê§˜¾.:7ФpE¯á]q}ÅT¬<§ù¦v¹ Æ`¬,2=å=æ©DÊ•X³#¾Á aoñ¤õ=VpñTi…·?è…Ëó¦cãíÇÿ¬ ý..Z…q=_ÇomæcvXîLIƒç1‘ (@`ð©DXY((@Š,ï™O>[UÈo9«Úƒz§âÀ/Ž0°‘tsP!˺˜ðõôŽžƒ.u<áÝ𬳙„ï—õ…®GF²TýÙ¬ñÚ7«Ñ3fzùÀó&š ˜½Q…>)j ×¤×à¾g0êyÖÇè­Idô8T} 3[œÁÄ`éÛäêIßΗÐãºØjæNçÚtÅÔ1*,é!¾A°)Æi€×NE ­I8}W'òámàé:ÝñiæKX6¹*e&#æÚÜL|ôx_õ+—Ó x¾°‡¿î‰øýè,=»ÈÎíÞ<¿™ÿÃî·ëÿV=«ºxeå4¸EhÏš’Êú%vMwÆÖ—õzu8Öì‚yÑaøïΙhi(@«³<Áªõܤjvðî0‘¡_`‹ô¬¶JÒãýŸ¸¾B˜VýšosÄ3+näÜrÊQÀ)ì}Loð?̘½?ëÛ$µÛþع¹åVs®‹Ð©{`=bNn…€¼1Ôœ¸A P€ @éèúÓ›®šuåÓwL>.Öò¶(SÞÏ¿–`b[äO¿”×"]Þo#ä}qL¼Ä¾ø¬"ï‹<â%öåm9]Þ·U«Õˤt. ÌR <<þþþÅî›('88¸Øå° C@\!!!Æ(ŠeP€ (PNŠòvâ¶<}˱cÇŠô¾¶¸ïƒíììÞÚ’*½Ä_*ÅK<òT|†ö¾ö¶œ.¦¸‹mù%öE>1%X“Óå}±ÇÄK,ù·Eº®—&³VšØ—óÉiòZÏ¿è:–'ìÉs; (@ P€ (@ P€0†€˜ýÃ… (@ P€ (@ ˜´€¡ÙL&Ýp6.ç67RP€ (@ P€ (@ PÀè¼íÎè¤, (@ P€ (@ P@`ðI–àš (@ P€ (@ PÀè|æ“ÑIY (@ P€¥! ¾©† (@ P€ €é 0ødúcÄR€ (O 888ßîR€ (@ ˜ªo»3Õ‘a»(@ P€ (@ P€ €0ødƒÈ.P€ (@ P€ (@S`ðÉTG†í¢(@ P€ (@ P€f Àà“ "»@ P€ (@ P€ LU€Á'S¶‹ (@ P€ (@ ˜ƒOf0ˆì(@ P€ (@ P€0UŸLudØ. P€ (@ P€ (` >™Á ²  (@ P€ (@ PÀT|2Õ‘a»(@ P€ (@ P€ €0ødƒÈ.P€ (@ P€ (@S°2Õ†±] (@ P@Ÿ@ff&¢££‘œœ µZ­/S€ (@…B;;;xyyÁÊŠá˜RØ v) ³ P€ Œ+ OhÑ¢¬­­[8K£(@ PÀlnß¾­ù–ŸŸŸÙöÑ;ÆÛîLqTØ& P€  ¤¤¤ €'ƒJL¤(@ P ¿€»»;Äû.¥+ÀàSéz³6 P€ Œ  R©P¹re#”Ä"(@ P€¨hâ}—Ò`ð©t½Y(@ P€ (@ P€¨P >U¨áfg)@ P€ (@ P€ @é ðã¥ëÍÚ(@ P€(E •ßœ½ð¸TÜ{¤„…°³¶ÂÓ~öäRŠ-aU (@ P â pæSÅ{öœ €é dœÀôÆA˜r$½äÛšù–„uÇü?2J¾.ÖPjD'#]©Bhí¸Ÿ®DŠ€ŠNy„ ÿÄã´Rk+¢(@ P€Y€Á§Š<úì;(@c ¨ã±¶¯ ]ÜáááïÚh:ËÆ£àÇ:ª‘rz#v\(£àUcLÙµﶬT•2n«Îšb›t6´T>ÌTãÏØT쾜øX½ JùZË8†7kûáÕC¥L}¬·¥{@}ï8õ®g[[Ôh6CŠÑoáV'“Ž”òx•.k£(@ ˜µƒOf=¼ì(@²°Bë9¿#:6Ñ—Á'¡ ødøÛØ–¤. 1)8ðÉ l9_>`šb[M±M y &_Kz€Ã7ÿÕÌzêàˆŽ5íñ|×<5VR%‰”£yŽçì¨ã°º{XU©[)ˆbëàŽúí‡ã£Ãq… ¦&ÿ±[ΕÍõ¬þ÷,¾Ú­üa'µÝÑ·%-þ‰zþ>¼òÆ7u„Û˜½Ð=¼´¯´¯'Dªõ4Æ{s€r7Ô {0¶a¼ÿçé  ·¾žŽyqC°?þ"÷ «t»£ñ5îîF­gáwNb3>/K¤(@ Q€Á'#b²( P€È+`aã…¶£¢ÅÃK¸qŸvóAÏÏ#r?¸+¯aE/ôXy;'wÃÄqØ=!@š5U#·¤f¦°ÂÿWctû øJ³©Z Ä¢£‰?CgDíżÁ!¨ïë /ÿƸgîe§ŠÛöš?‹ÛVbLGÝçç´Xû?CçI³»véikfÌ~,‚ºÞžð ê„q«O#Y4ES^>9²ov ‚·O¬¾q\j[7¼¿n†´© oou™ˆÿžËþ­ëœHtÖ¡2R›®À”Fè÷å­Ü1Ê*?"èôÎþëøô÷h¬9‹çîh^›ÎÅçéDeeñpu pgMž´ÜJh»ð’SS‘s _ôºƒ%ÏOÄ&}Qœœ“±oÉÛÒí}e|BÆmÄZ‡J²[ø7õ_\Ýò<ìÃrZ˜µ‘ˆÝoã™n"ÂÁyÞ*Ïaé‹“p¶ã·¸üo2nl퇛ÓãƒSÚá)QJŽÎ›‚ƒíçá­æU¥}%"®G±U{4°±„½MÞr³*6ÂO»½·=¾Àäÿ»$ÕZ´E}÷ ¶ï<‹åÿ˜:ýQN9»zóªðï-ÉZoº¡Â³Ó\ÄOŽ#¶ài¢…(¬pYr ÔH½vÛ7nÂÖí‡q%ᓵEôaão¸­§9õ¥èkÖyû–SæÙç·2ƒ¼½á(@óÈóÃ|»ÉžQ€ @é ¨ð0á"~^ú5Nº>vuëàùOáÜw?àrö§Dåå­Ø|ù) éÛ½¯Ã”&ÕÐså5ÄÆÞÄÚþ¶YMVI©=÷ðÒ¦³ˆˆº€ }â±ìÕø'SJÎ<‡å/ŽÃ¾€¹8x%‘á04u†LÞŽxùƒHÆ)¬ü" #~Ð:ÚjœçZô§tE˜®¶*/cåȱØã; ¯FáâÎQH[:ïLɪEz¦Ôò÷ éò¸uc3FyI¿‚3Nã« LÞsQ§±²í_˜öÒü!ÇòŸãqUw‡ª§M¾Žð®Wµ\«¢D&ªò6bÚ¾kw UZ Ki擼ÜÝ-oé][ØxãéñCÑúÁœ»u·sD§Onæê”W°¸­=:,ÿ['´Ã˜-·±ý%WiÖ”3ý}ˆ`êÙO1´…/œªW‡[ƒÞøàpBn05bf>×ÞNv°w©öcVâ”Ñ·ŸÕïˆ9›–ãÅV~:Ï—¯p Å;ŒE¿jRàÇJºõ­žñO­ȴœº²òªpÿž+Æì8„òŒ»òòNl»þ &¿ÕÎV°kò ÞéwÖÿŽ¢³y–"·Éª&oþ1Oåq':YŽÞeµÞ¦’üªü?{weµþü70n¨ìûª¡²hî;®åš»V.™¦–f73£ºÚ~ퟙÖõ–e–ezµEM+¯Kj¸âžŠ[.l‚ˆ ÃÌÿœ†aІá÷~>Ó¼ËyÏyÎ÷L2óÌyß¿tWði·û]øßý¼ {wb ÖM®‰dêÕl‰ø»=»ajP#„>Û ³>]˜ÞD¨­˜ïóVÅt O¶ÆP÷Õ8w¸5öO»‚Ÿ†ÛåÖ(Ç\›ˆŸ×ÝÀê_Ïa…ÛmìýWôšú 8øšá8æ>1»üû¾ï¯»'ðÍÄþò‚+¢W<YKö>Ìÿ´ Ö¬?‹ï”ó_þF¿‡¦f^ÏÚÌDqþûøæj_¼û¨ƒÑgþyê5„@ƒck•Œmn¸šs1¸à×Áµs·åÿAM‘¼ö¤Hu‚üßV.Yþ‡]µÁäÆ"O š÷;4 1A½1ó:¡šLš½š[V€[Þxþ‹—ѽ¹'²ö¿ƒ>½§cé»0åÌøÇ¦ ,?±=3qùÈ!\ÈKBëO³s}ŒÇŸú ¾ŠÄ›írÿ_ªÓ©:$}Œ­gsÐNDiíUœ»X }ëxè®ãðº-Èê8 íÜå÷³bVØï?áOè[/›Ö¥¢ù“Ÿˆ¤úÖtÔ÷JÅÉ ™ðj÷(údjcŒ}o">o÷ 6\{ãdª£ë¤pt-tþyýùE’Ow×bŒÏSøþÆè\Úãùˆ¯1Pθ+å’sûîÖ²C­|Oìj‹Dš¸ñ¶ÌSé÷k‘|ú4n6F¥|WY«q/ PbhÝ=¿Glblë… ÁõÿbÕêÝzº3ü[w…¿,§OÔÚ@û××ý¯oðЂÿaΣ.I£ÚAñ¹€“gEAƒä“æÚIü±û4RµµáÔħ®$iâ õ•óˆ³D3Ã>*GÖĦóÉhãî ›¬8œ»â‚†íE¢Cö×`ѦœE|ð@ z¼Nî¿sÞ-ñ×ù¿ðРGPO¼”ti³ê ÎË]­¦=1 ¥­¨÷Nü¶§’¡ƒ~|sKènFÔQZö Ïj$í[‡¨¾$^Z©‰Èj;uªÍ•=X+úx= …ÙØï^؉ íñx_/ØŠÄ‹F£ÖÛåäˆ1nÕA [‘6­e¶,3Í jÓñ6¿þgÑz³rÏˌÞ-ÇQ³m^âIì-hO¼pÄÿ‚ºŒ4¨ƒc G5h’÷cýöCð~¢#< Z–«¦ûÛ΢ã:µ©IÐt‚ajd'íÅ:Yç“ذ6œ¿‚¶žÞ°Õ$⯶rC½š…ÇM&‘t7¡ qu6ôm §['±SLKm3þÚŽ_wÆÀ¥o(ê–§Q—¸I ”‘€J|©4jÔ(ñgébb ¾D Öï—ǹXŽ@)ß&XNÀŒ„ *³@ ´3ÎÃ~À–~ø%{ vU¦U¨ôˆŒ>ãé;køU=×›"A~vÌ}ƒ‘‹7x¸|È6w~I’æÏ+««Üœ‡bþñ/Ñ/o¢K~ýâþMr)RŸî&’®ŠOa"¡ ]zbo:¢™³éØ‹mCÜ?K ú[Þ\LúH*÷¼êVGú5é Œ­wê«‹ wÈ3hô[á}f·ä=ŸŽã÷©õŠ&Sí{ãÙ'_ŘovâÝæu°êÇTôÿø1ý‡b³Õ‰¤¦§gÁ De[­ ™š|)¾ð.8 [o?x‰ÙI)"›%“ò|‚lkîùf’©5†`YJ–åÜFü¡ŸðÖ3]1ôæl|þ¡¢}1°mí:¨‘™9a&wÑ!㶸'Yº(ÈÙˆÙHéâþluíQ§4ïñÅe´¿‹{a½»t+ŽÇ¥!K$h³îºc²hÃ6ôUlØRó缉nï_G“§ÞÆGïGˆüJÝU¬zë_Ðfú`²›}áøEסN¶øö[&•D³w3k"¨ç`¸kâñçCØvÂþmº‰ÿÛÄÌ—sñp l%Ó‹ uj€º¿Ç•¶žp‰û Éî ÑIV™ûrÊ/«ªæ‹†õóOù{K¹¢MÃåÃûq,ö¦ˆD…¬[wá¥O°œŸ“+i)ØýK^2''9>·õÿ«jzÁ_Ÿ|ԎΨ›}MÌŽ÷¿2{5¿ xÅü ;¢qhê¹Ê·ÿ$˜4(ˆ!ÍL¼5MÕ+û!þ];±õ<2¢¿2EÛSÕô€¯[ª]à­Ú‡ä[:‘lËoY¿bº¿âÊTžðqËýxSÍ­>|möAL‚‚Wýp8~ ox%\@‚S´‘/b‘ô5^T5=Mø ±äD\wk¿¼l¬ÿCp:ƒ¤ìÜä“ù8[à6(P–¶¶¶?~<.\ˆØØXøùùé·å~.–%PðîÖ²âb4 (`¥¶ÁOáéF;1ç½_QcØH´S>›Ø8ÀÙI‡³‡Ž"]'n¬­ÉÿÄkVB2 #ïÅ‚¶ ^|îÔ¦ŸÂŠ—âJ¯‘èeöæ,f«+ý±ªC‡bDàïødV$.eÈi/ZdÄÃÉ„bn.%fÞìX²{SÄ'6qƒèms?C”ßcèbú»¡bÛ(‹˜²âßCû"|}R¡$Véa,£dW1ÃLYjÛ˜žòÓé%øúØ\¥’nÇaÊÿço—n¥†¸¡þÓpùe6mÿk³‡bì£ÊebE”¹uŠýf’46^p»‹8ñ\Yrâ.#ÞÆžnÊ[6óç+çy¶­ Ÿ6Oá…Nسí æ{)j¸CÜ âNâômeoNÿy.â’C9ë$wQ¡¶~6T: ÂVyÎØ8#?NÁ o"þ†8çúF<ï[Ð7‡&Oàe»pæðb´~ OÎ:(Ò$r©…G>9ˆ=³ÜñÕØplH)0‚˜=”v[-âÓÅ”EÄåû¼Ä%°¶u|Ѭû<>ì1´õ·ƒJÎhJrC ŸaùÜóTµë£c<Î'ÞBìùkðnè——VêÍ{30ÕùFÇ 6ó£Ì)HfœÝ¨«žè2PÜ_iè´öRú_p¢VÌî´ñnƒC‡Š2âñÄ<ÑÑ'7éf#f*EÅ‹I Ã\ìª:¡ËàAèüP5$ì^_¤@[ŒRµòl.^“õŠ“twn£º²Îˆ/”{Ó›j/GiÉ̳™þÂd†•ê S6k ¡ÛœÏDÂ…D¸ú% Ú6מ¾.¥B¥¼ÐWÀÜyJQ>S€L F˜4iäŒ'ù,·¹Xž@Ñ¿v–##¢(@k°ñÁ°§[àRŒ3ÞTÜ‘)oQ¹ ÿÔáùÛHycšԒ ¶0ù›%?=z÷ɓXn7+#†ÀMù@ Ô_–ϦbU‡àÅo£_Âlô ³U¼ë¡åãÿBd\1É'[ñëæ ¢_(|üZaÊÁV˜»ô5´ÊU4ââÚ(‹˜4iH8¯Ý-Ù¾ht³§wgÌz´>^lë‡ùÓŇDñ¶Çe8`×Ýüûãà•]ÈÔäfVvÅnDGŸž÷ÜÛб¼ÿ÷ÆÏ¨1| :æ'Sáâ¬Ãi1ã&­´ÉÔÆ#0æá(|òþ&ÄÉdjš¸çÓ{‹‘Ðw +Èö”"F1éÏ-Øxð2nfÉËÙH=¾ÿYs-:6Gõ¬ý˜Ý·+þ!¶?JVnûÐŒh²óvãZޏßÍÑÏ1wu5q©YÛ‚ÿoE:Ä]ü˜@s'q¶˜—ºR¯&= ™"Tß µr®âàÒ/°!ï§Ñr.îÅÖ㉸-êQ‹KýÝjàN†¸dPž¬²ƒ§—=· ZoÂĉßá’2k%ã4NÅ ¸Aþ¿&Js&ŸïÆžCŠGøæÿ¦EÖ¬¼+%k" ¡+’bâÜM1»ÉÛt"¸HÅâ²M[›;ÕèUõ¨žq7ô™>a—w3r4w³`kï„:¢jíí˸|µè5šÕܽáœ|FüÊ`^'µY¢îâFK6k:vmÖ]ñ ¨G߯hßÚw®âfƒÜ¸‹þ×|¼¦ê½#*PÕòFÃaèZÿ*¢þø "ì¢æ"Iu÷ b“r§|iR.!Aç÷RMŸËÒt‰¸|%w²’. J5àßÐ×þ:‚¿®ŠÄ£Þÿ¨FãV´ÿ{Ôž>pI¹ ¾\ã f^:dgox”ò%RP×(@!PWüˆÇäÉ“!Ÿ¹X¦ÿ¹´ÌqaT *§€ÊãÖ&b\±Ñ‹7íâgÛmڎ“ âëc§.obÝ‘7 ÎvÁ‡GŽy ÛÐ×±+¶`‡­gw¼&fJ¼V°«`­ZûÏÏ/\¨lI癊U\6äÕá_‹G~¥ÊJÑúôGtY°oÿ:¿nØç¼s Å£ÔS\0áW\y1ÙuÇÇGÏ4V‰×¹ØA>pÍà{6‘µïÅéëÃÕ¡7‚œ›boÂ6tñë‹ÝñÿÃkí>¹÷ÛøaøøVxkÜ%¼º¼EARFåŠÁ¯MÃʉƒáûEuôùü¾²„êmƒÅ ëW 󥢃×pÜu4ø~ž÷ÄíŠîiɹ¶_„?‹§N%᮪:j{†âÑgVaù‹ `›µqgOãBJnbÇlÕ¶ ðÂןâÒsc䘀lû xk%Þï\øbµêm{¢SÚØ,~B²SóâßZÚ÷¬ÏÚ#½f™ú!^ìsEïtWw⣧?ÆîKâ:©š.zt>ÿgaº§ ï6ÞxüÓ%ØÑqÆF´DdxdïŠÄn—G0-Øðß“‚S ¯eâÒùâ—ô¼Äoæ.º´“øß/ñh0D¼&ÄK¦º_¸íþ’„Giª”ÕØx (ø(vþ²gD"¹w‡zhÓIÖ$@IDATr»Öýˆ?ëÔF]w'¸ë§J‰Kû›ÂkÖˆD^M‡<à€ëy±(O*ûPtíx »¶®AŒ¼™N ¿öbæ–³RÂô³©Ø³£±a_Ë}ò!ãuyÜÔC_Øà˜ÜVÊ)Ç”g¹ßx1µ¯P™*ù©7(@ P |4Ç0§»/Ú¿øa‹0«N<•8[‘µÛ~Šç–â«(1ßVܵ;G¾÷š¸µAfömütúK<0H¿ï^ÿs÷Òj¼óÉ)ô}é)TÙwUµÑíÝ<úÇ{ˆ8"/¼*¯E‡[?À¼¤‰˜ÿOå¥nÑíóÑ''õ4ÎÜ® /&ž,z (`µü××j‡–£(`aê¦x}{^·°°*,3—ÕUX<ÖܰëX@>Œy«fyï§ÿ]\‹æŒŽ–°©9‚ÿ ë†cœÐþÅ¥X9عêÎâT*×~øâX¿ÐÊú° Ž=âp²®—õY•€î:NlÙŽ?¯WG`§žp—×Cp¡(@r`ò©ÜÉÙ (@ P€–"Ðû¡'!÷¼¨›ã­}7ðÖ=ŸÈ(@"P+}G˜¨Y儯=‡¢±‰CÜE P€å'Pe'ˆ—1[¢(@ P€ (@ P€UW€É§ª;öì9(@ P Ò ØØØàî]ýïØWÚ>0p P€ *F@¾àR¾/_o¶F P€ @Ô­[çÏŸGVVVÔÆ*(@ P€¨*W®\|Á¥|xϧòõfk (@ ”€âãã ®˜Ÿ¸*ƒ¶X(@ P€Ö! R©`ooù>‚Kù 0ùT¾Þl (@2P«Õ(ƒšX(@ P€ ÀƒàewZ˜õS€ (@ P€ (@*,ÀäS|v (@ P€ (@ =haÖO P€ (@ P€ ª°“OUxðÙu P€ (@ P€ (ð ˜|zЬŸ (@ P€ (@ Ta&Ÿªðà³ë (@ P€ (@ PàA 0ùô …Y?(@ P€ (@ P€¨ÂL>UáÁg×)@ P€ (@ P€ ÀƒP?èX?(@ P€(kcÇŽ•u•¬ (@*$дiÓ*ÔÛŠï*“O?Œ€ (@û »³x (@ P€U] **ªª”{ÿyÙ]¹“³A P€ (@ P€ (Pu˜|ª:cÍžR€ (@ P€ (@r`ò©ÜÉÙ (@ P€ (@ P€¨:L>U±fO)@ P€ (@ P€ @¹ ð†ãåNÎ)@ P€ (@ P€(+‹±q¸_¨ºnÛÚæFÅ pæSÅú³u P€ À½ hŽâ“½1ë@ö½ŸË3(@ P€ €• ÔóóE=?+ë•uu‡É'ëOö† @… \Z:AŽpppC`ÿ…8™SR8:¤úëNYRÅÒb2ºÂ‰Ämª•Ìã (@ P J8;:B­æÅ]–:ØL>YêÈ0. P€•P `ÜJDÍ}víÞÅÖõSj[R'Ò±uá[X}Ò’’O–“¥ÅSÒ˜ò8(@ P€(_´ô[8yæiÄPåK_êÖ˜|*5 R€ À= dïÅŒVaöÚEx®[(ü½¼Øv8æìº.¿Lë…)ë’ðëä@xyÕøշôMh¶`ö¨04òõ†hwLZ|i:qH_ß,Üù^é _¿AX|a·h£Þ[>£Ú7‚¯—/B{LÁw'2rÃ5uN¬&ÛЖQL¢~Ã%;.Œ C°¿7|ê7CŸ)Kpø¦ìXôñ™‰ßœ‘<§Y(ÂwfåVQbýfÆ@ßþü<}0Zúˆ1ðGP»!øè1Ûþp †~uIßÿC P€ ,QànV–>ñÒ(uëŠä/Á³ÄqbòÉG…1Q€°&ìh,ú2c¿?‚Ëq§°jP2"¦/Ɖw øx9›×F¿E瑘xK‡ÕrÎ`Ѹ‰øÍÿ]l;‡˜õã‘1o,ÞÞ–ž«"îw4ÿí­h1/.]ø ã}ÄŸ²ìCøz• Ó~;‰¸Ë‡°¨ãQLf6Ü̓4>Çëœé6¶×*›˜| þ¼jN`þÓ“°9ð}l;›€Øc«ðÔ­Œšö3’óòOfãÏ2c”×-ýS©ê73 kû\„oiˆ¹û/!1ö$¶~ŽÇ‚kjøá!÷Z†­q (@ X”@êÕÑâáP}âI L& ¸X–€Á»cË ŒÑP€ €µTC§ñSÐÉ£:`ëˆ{¢^Ü_¸læ~PšSk°êd{L™Þ¾5m`×`8&ôÉÆ¦õÑÐÏóÑe£Ù„÷1º±=lÕÕ VI§šè6n2Ú¸ˆëü«¹£Ë+ÿ@—„MØxJdWäbtbJh#÷¬üÿÞ_L¹§kN­ÃšóðÒëÀ»`S7£fŒƒGä lNU²O%ÄŸIÑ•ÒÕo~ lýE‚éÆü´nâ²j÷E'<ì*P«·Ä´Ÿ6àãîEå P€ (PÁwïæÎ—aÈË`òɲLJÑQ€¨ü*x¸ÕÌï‡ÊV [mÌäž MN@Ò­HL ñ—y‰GFþ7éiÐ_̦rB@€#ô9'¥VU]¸¸¼éPÙyÂËþ&R¯ç]þftN‰m(õæ=—XÞ¨~ÃÓµ)I¸fï Oƒ D¶âÒ@Om ’¯)ñ•¿a…F륫ßüØ¿„ÕëÃá·w&ú¶láï¬Æé¼+šâ&(@ P€¨0‹±qùmË{<<þ'2ïÜÉßÇËà­à-{|(@«PÊvI¥O")óäW¸9Åüã_¢ŸaY±.îu$—"õén"éj¦8’›€Ò¥Ç!ö¦#š9|ÇbxN±mäœ/›˜ô‘Šþ¸yÂõæŸH¸-zé˜ ‘‹7x¸æÅWlüEòªÖ?•ª~QÒ°ÿ†ç‹#°‚鋇àÕ”}ˆ7 c? Äo7ß$–â(@ P€'p16^߸üU»?OŸAPàC¨U³à Ί‹Œ-—F à]yiJ³ (@ P ,làì¤ÃÙCG‘®7×è Š¿ã“Y‘¸”!gi‘ 'ò.¡3Õ¾î.v,Y„½)âWó²¯`ÛÜÏå÷ú†˜NŸÛFYÅ”§:dF6Þ‹mA¼¸•6ýV|¸WzD/§¼¬\qñ›ˆÇ Tõž`´žsù~?q‚W]Ç ~®5p'ãtYñï¡}¾>Éè nR€ (@Š ¨£'cô‰''ÇŠ ‚­Þ—“O÷ÅÆ“(@ PÀ”@üê—ñø‡{ytFO\†sæ®­SNV¹ ÿÔáùÛHycšTèÔ!xñÛÅè—0ýBýàå]-ÿ"ãŠI>Ùú¡ß£9ˆè ¿V˜r°æ.} ­ ®ÄSZÌ}.®²ŠIiѶ&³ãg¢GCoø6yËí¦beĸ)3Š‹ßT וãʱ::.B”áB PÀ*Ž;†úõëÿí¾Ézš6mú·ë)ó Ä¥x3ZGö£ø¸‹ü'¾’-•=þ ▯ǰ°° jÍR€ (PÞ¿ïÞW¨Éz~>¸ß_µ‹ŠŠº§÷µ÷}°½½ýË"x™1“ߦʇüŠVLÙ/´mxL9./ëÊCnËrò®rŸr\ٖϹ—ˆëò¸©‡¾°Á1¹­”SŽ)Ïr¿ñbj_¡22Ã… (Pétú¿•·•=þÊ+ÏÈ)@ P€¨ Ý:¶« a2F3ÊL#3‡¹› (@ P€ (@ P€÷/À™O÷oÇ3)@ PÀªµÇ‡GOZB$÷Ceÿþzͳ(@ P€ ªg>U¡ÁfW)@ P€ (@ P€ @y 0ùTÞâl (@ P€ (@ T!^vW…›]¥(@ X“€ü¥. (@ P€–/Àä“å#¤(@ PÀH iÓ¦F{¸I P€ (`©¼ìÎRG†qQ€ (@ P€ (@+`òÉ ‘] (@ P€ (@ P€–*Àä“¥Ž ã¢(@ P€ (@ P€V Àä“ "»@ P€ (@ P€ ,U€É'KÆE P€ (@ P€ ¬@€É'+Dv (@ P€ (@ Xª“O–:2Œ‹ (@ P€ (@ X“OV0ˆì(@ P€ (@ P€°T&Ÿ,ud(@ P€ (@ P€°&Ÿ¬`Ù P€ (@ P€ (`©jK ŒqQ€ (@sÇŽ3wˆû)@ P€ @‰M›6-± ”“OegÉš(@ P€(G°°°rlMQ€ (`-QQQÖÒ•JÓ^vWi†ŠR€ (@ P€ (@Ê'ÀäSå3FL P€ (@ P€ *“O•f¨((@ P€ (@ P€¨|L>U¾1cÄ (@ P€ (@ P Ò0ùTi†ŠR€ (@ P€ (@Ê'ÀäSå3FL P j dïÅŒæMñzTvQÍQ|2 7f0q¬h鿵'kg8B›ÍÀ^ÙT9¶û·‚æÉ (@ P  ü¾{Œ7ÒÒ*aOªnÈL>UݱgÏ)@ ”¹ÀŸôEÇYÇ¡QjÖÆaq?7¸=öbµù;÷Åcðê¹ò÷)Çþ泺‰ÄmªýÍŠîñôÒ®é‡~ÀºS>‘v½eq P€ (Pá/ÇWx  ôêÒeI P€ @ñº7Aøº?0ýaø‹¯7tW·bÓŸµP ñ¿¤ç0ÞKîLÞÇQ¯Ûløñ+b@Ó±uá[XÓ¿?…”s2­˜¨xˆ (@ X‚€œù$gC™[ºulgî÷W€ßöW:›¤(`­>]º!ðÄ쾡]ÔáúöM8üð$<ßâ6lI{Ärç~?àˆÎÝ‚!¿ÉŽ‹Ä#Ãìï ŸúÍÐgʾ©/)ŠKìZ ÀÂ?à•¡ðõ„Åq†Ó¥t¸¹wz6îƒ9ÑéÐÉòÍB¾3+ïÜÇ0{í"<×-þ^^l;sv]ËC´ó_LíÕ~Þ~ê8 ³W~€þ­ò.¥“±,'–cJÏPøyù"(ì|z4ÕUyŠ´k³˜ö¥I؂٣ÂÐÈ×þ¡Ý1iñ!¤åuS'ŽíŠÐoxû¡ókñÓ´^˜². ¿N„—W=Œ[}Kߨ½yõÂS]ýÐï‹ËÈWË9=|Ð÷óKÐflGøÃúÕ%ƒžr• (@ P€e+À™OeëÉÚ(@ TiÛFÝÐÙy ~ߟ‰}4رi{ÎĨš»ñå†í¸>z8êþ¹{ta˜Õ¢º¸WÒ Ìz6‡}mß<ϬSXñÒ5Í;¾w©)î§4ÿmo¼ûÙ^Ì ªö ÞÖ+ëpûè<=aÚ~ú3^k]*ã+Ô²£±èË0¬øþ¾tÍÀÙC0húb<¶cšèŽbÞ„âhå8ø[W8¦îœ§G`Oö˜¢c˜9ãDÙ>ÿÅÁ ¢ìõÝøxìpÄkG-+÷ÇŒsXÔo"~kñ9¶ë縘4`,ÞÜù]/cÁ˜ç°¡ýbl>Ý^Ú$ÄÝ´‡¿ûÈ;Úÿqß ³Ëmçž½j"iÕhtüò{œyö5Û9gÖà§3ðÜ`ب¯Á7(*÷Z¦ûÁ½ (@ PÀ‚Z4 …ƒ}] Šˆ¡”V€É§ÒJ±(@ ”,P­édƒWwEv—Ø´Û=_þ5{£ÁG°=mÞ…”¶/ ½Èwhþ\‡5ç;‰Kõw Q}Œš1_u_Í©ƒ1Ú^ìÓe£Ù„÷1º±=ôô &è.~‡g?\zs×áýnιNJDX ÆOA'‘èBu´Øõÿ…Ë9@ðé_ñËå®x5¼+ÜåUm1õùGñù{E*æÔFlLì†W_é’[Ö½#^|¶>ûWѲú=F1kþ\ƒU'Ûcê÷=á[Sô¢ÁpLèó&­F†Ûnüx6 ¯þ,ɳ= sM"FãEsêÞ½¼=ƒÞï¿‹•G^Æ¿Z©ðçš5ˆëþú{ˆ8T-1í§ ÆÍp› (@ Xœ€“£Ôj5vìÙ.oúx Q::Ø£yãJñpyð²»òPf ªŒ@-´}¤nü± 'ölÆ.§žèÝÈ6½ÑÛ/ ›v^FÔ®³hÞ= ö"÷¡MIÂ5{oxL¼±—µyjS|-ïB1• '—t)øéýÙØ^n®ö0ûÇLå7}FG?*[5lµ9ú¼ŽöZ2R ·]ÓÝŽ&*Ó¦¦à†ƒ¼k‹ ó–Zîž&Ëæ6T8fmr’nEbJˆ·¸„ÎK<0ò¿©ÈHOCŽ40ŠCiÃøù¾¼ê>бC4X½" ™YG°zíuôÕN]1n†Û (@ PÀâêûùâbl\©O²õý}-®U5 o±«*ûM P€e!`ß±;Z\ŒÂwë÷¢Ö£½ÑXαµ Dïޞع;7D·®ú„‘›'\oÆ#ávÁ·W9ñ±H°qƒ‡kÁŸ(U‘DI-tý`'¶¾ë†e“ÞÀækç÷¡è¹¹%lœ]ᘖ„¤»ghÒnàvþÍ‘ öÛ8¹ÀA–½S°/ûf*n™(«”0lׯÕnÎCñõùD$&æ=’’ûõ Ô’qÜLÄ•LåLåY¥O¸öìþ¼j Ý˜pÞø¶ìZ_²bT×ÚJ#|¦(@ P€/àâä•xs•’z½Ô±:;9¡./Ñ+5Ø.XðÎþ7Äê)@ P j¨Ü» [£ãX½>Ýû4Gîï´©Ò§jn\…Î]Ñ­¾¸ùXÔ!Ã0²ñ^,øh âEH›.îùôáR\é5½Š›š£ªwOW4xæ?˜Óâxé¥Uˆ5q™ZqâêÇð˜×ïX´`R4âÀÔCøòË-H3q’:¤z»o/({-_,Ù†Ü[€›8Áh—:t(FþŽOfEâR†ÌXi‘ '4P‡Äà€X8w;e",û:ÎOFŽœt8{è(Òuâ†åÝ}{Ù?…§íÄœ÷~Ea#ÑN™ –uÿÚá듌"æ&(@ P€°z~>bÖS|©g=ÉDÕCœõd9("aòÉ¢†ƒÁP€°ÛzèÚÅéênèÝFÞÈ)wQ?Ü=êÜDµNÝrgCÉݶ0ù›%?=z÷ɓXn7+#†À­Èl§¼Š Ÿl¼0øßŸ¢ÿé70鳓0˜ÄdXÊôzõVxí«™h´u<š‹iÜ¡}?BZ;qï)Sk´Åô¥3ÑpÛQÖûσæÉqh›Y3]¿á^u^üv1ú%ÌF¿P?xy×CËÇÿ…È8‘õªÞáß-@—S3е¸,/  žù6Ù*ôŸú"<‰ ï`LX“ ÝýzÙø`ØÓ-p)Æoš—jÒpþ,.^»'9Þq (@ 帲-Ÿå>ù‹ñº†ŸqD›I‹ðuq³Î2ªšÕP€ (@ P ´L>•VŠå(@ PÀÊê Ëë±ÿ +ë–awÔMñúö8¼n¸ë (@ P€(gN¾/gp6G P€ (@ P€ ª’“OUi´ÙW P€ (@ P€ (Pμ쮜ÁÙ(@ P€e# ©† (@ P€ €å 0ùdùcÄ)@ P€0hÚ´©ÑnR€ (@ Xª/»³Ô‘a\ (@ P€ (@ PÀ ˜|²‚Ad(@ P€ (@ P€ €¥ 0ùd©#ø(@ P€ (@ P€ €0ùdƒÈ.P€ (@ P€ (@K`òÉRG†qQ€ (@ P€ (@+`òÉ ‘] (@ P€ (@ P€–*Àä“¥Ž ã¢(@ P€ (@ P€V Àä“ "»@ P€ (@ P€ ,U€É'KÆE P€ (@ P€ ¬@€É'+Dv (@ P€ (@ Xª“O–:2Œ‹ (@ P€ (@ X“OV0ˆì(@ P€ (@ P€°T&Ÿ,ud(@ P€ (@ P€°&Ÿ¬`Ù P€ (@ P€ (`©L>YêÈ0. P€ (@ P€ (`L>YÁ ²  (@ P€ (@ PÀRÔ–ã¢(@Ê-pýúuÄÄÄ 33³T©U«‚ƒƒáääTªò,D P€ (@ T&Ÿ*Ç81J P€•N@&žZ·n ggçRÅžššŠèèhtèСTåYˆ (@ P€¨¼ì®rŒ£¤(Pé䌧Ò&ždçdÙÒÎ’*-FÖÎp„6›½Ù¦Ï(é¸é³¸— (@ P€¸&ŸîE‹e)@ PÀî`×ëMàÚâMì»káÜS:¤úëN™É†ÝS]²pY×Wšî·Íû=//&]2–qƒ“›'¼¼¼àåÛ­úLÄü]ÉÐjãðí°úhûÎÜ1èBÖÑѹ^w mã“{Ž—;œáâ!Ηuø?o¯$‰z½ÐgÑeh ÎEÎi|ÜÅ #¸e°÷..EÎÁ³½["Ð[ÆáÐÎ#ðÆÊcHÓŽÓ¹Ù+ØvSÙ)åàì¼GÐ`r$ô/ÛâúcÐ"DмÞ5‡ð~XYõ©PcÜ (@ P€D€É§ÂÊJ)@ Pà ¤mÁ²_Zóg,ûÝ01ðÀZ,Êӱuá[X}²¬’Oe]_iºz¿mÞïy†1©Ñnæ~Ä'&"þÌ,ì“‚…c^ÃÚ>ùÎDÔønVÆå¥´‰øiöWÈÿ VˆG¢8'ñâ·xÜ1ÿØ›»}ùGŒõP6P̺‰k^@ßç7ÃmÒ2ì¿x‰ñ1Ø<»âfÂãÇs“JúªÃîÚJ¼öÁ.¤S#`¦?©I+S¯wuK¼U}*68¤(@ P€e&ÀäS™Q²" P€xð:\Yÿ-v4žˆˆñ ±å» H1øœžqb9¦ô …Ÿ—/‚žÁ§G3QÝ ·PÒqÃø5q[0{lW„xÃÛ/ߨŽÛ¢@v\$>†`oøÔo†>S–à°2Ã%{/f´z ³×.ÂsÝBá/f×¶Ž9»®A'fºü2­¦¬K¯“Ŭ™z·:7y¦Im C#_oø‡vǤŇrgÒÜg}ùýП? wþ€Wz„Â×oÇj‹ïCq}4×‡ì øyú`´”³qüÔn>úà 1hî¼âÚÊï„é;tœ0­ïœFL\ª7}o÷;‹ˆy;ôã”±g>>9ÚoMi¦«¸·½Ù‡ñù¬¨7ý+|0´ \ª‹Ómìàö<>û÷@ÄÏŸƒ_ò“FµÑ;üe¸¬z³÷ÈWMÉ‹qrÏ(þõ^r­,A P€ ,C€É'ËFA P€¥ȹ€—ý‰.c£ù°Ñh»Ö(3]²¢1gÜ?q´Ã"¼‡ãk'"sÃ/ˆW®¥*é¸aûšX0æ9lðy ›OÇ#öô,ŸÒvbÿü§'asàûØv6±ÇVá©[5íg$+I°ìh,ú2c¿?‚Ëq§°jP2"¦/Ɖw øx9›×F¿EçÅÌ›‹X:¬Ž¸ë ›ˆßüßŶsqˆY?óÆâímysfîµ>Ã~ÈuÍQÌ{+ZÌß‹K~ÂxÏSÅ÷¡¸>Ât²¶ÏEø–†˜»ÿcObëçáx,¸vA$*Ó硸¶Ï‚Z Ö´¸“ƒMó¾Á>÷ÎèÔ@ü~ŠÊ=þŽÀ_fcéé³X6k |§Í@'ƒì£A ÷ºªÛ¨øPôééã7OuÂú ö`ÇaeFÛhzóð"|.d”Ôš‰þÈSŠ{½—T%S€ (@ 0~ÿdA¡1 P€ @aìã+ðßäx¦§lœûâéžaùcÄÝtDŽåÔFlLì†)¯t{5 º{G¼øl·üY/%7lIsògüx6 S§÷„oMlí<àe‡œSë°æ|'¼ôú#ðÓilê†`ÔŒqðˆ\Íù³^ª¡Óø)èä!¦ÆØ:¢ÅÀž¨÷.Ë M,šSk°êd{LÑ·e»Ã1¡O66­F–¾ü½ÕW¤ ]6šMx£ÛÃV-`bŠïƒ¦T},ÜŠ­º±?­Û‡¸¬ÚðmÑ »–œô¹÷¶4Ø÷N[øxù a‡Qøø|GD¬ž‰®v¹ñØøÂ;cocáèQˆ¸1ïŽ (’(*¹²¥Ô›w/(ý=¥ºböñ‚AÓÞHÅM8ÁÅT2ËÖ Îö·qý†&¯B-4[„¼0áËq¸Ð½¨”VÅ«¶Øþ÷z/¨ÃÜšR·ù>™;“û)@ P€ @Y ˆ¯ ¹P€ *ƒ@&¢–­DLü ŒþA°îî-¤{|‡/ˆ–©)¸áàïÚIZîžpÌûšE[ÂqCmJ®9xó–á^@¿ß¾ð~[q‰Ÿ§6É×Ä+{Q^ÌÀñp«™¢ÊV [mŽ>A–¿Ó`E›œ€¤[‘˜â©Ê~Q^Ý/MÜ[põ)Uä?«œàE¥¤>”tŽù5ç¯Ø¿„ÕëýñYÄLôs¡O¾Ž™Ó‡!(/)”_Ðh¥Ä¶\lÎ÷HÚ‡ “ýÍ$•j Ù ¯ ý—S¡™û"Z”úz;õÊŽwyØ8:‹®§"%UŒ³“Q\šT\K« 'G£·UÕ‚ñ|D86ö Ç¿bˆJ¥[&ÚU¡ø×{ûûf¢n£>å7Å P€ (ð€8óé³z P€(ÝHq£qOüsãAìÛ·OÿØýžµ]‹o·ß‚“ Ò’dðsgÙ7Sq+ï²»’ŽFiãì Ç›‰¸’i¸WÌt¿´æz3 · ® ˉE‚<\ þ¤É1äW£Ò' ÎuºzÀÍy(¾>/nˆ-oŠ-IɈýz”Ö½Ô—ß”ÁŠáù%õ¡¤ã"V¤rŸ}èL_‰Ã,@›Ã¯cì'GļÃ¥èy%·ex~éÖUuÜáQÛ®®uôq–KÙø…!Ìç$6F^.’HLÿc#þÐu@×bf™ÑR-Hü"_¸–„Žs*y£¨Ò-%½ÞKW KQ€ (@Ë(x§lñ0 P€ € ñKc?/Ãö ÝÒ ¹Ÿ®û„+6.û 7‚û ·ûv,Z°)"롹/–lƒrÛkuHñÇ U‡Äà€X8w;ïŠ#Ù×qî|2T!Ã0²ñ^,øh âÅ~mú)¬øp)®ô‰^¦.Ç2¬T®Û8ÀÙI‡³‡Ž"]'/ÍÒA:#Ç'³"q)CfʴȈ?†“ …S7ÆUé·MÔg²œÁNu }(鸩>ä\>€ßO\A†Y]Ç ~®5p'ã “l¦Î+±-ƒ¸+|UÝ“Þè‹Ës&àÍ5"U^©Í@Ü‹ðBø¯ð›ö:8Ïl’QWCÃç"0­æb¼÷}‚ÝÒ,%¿Þ o´_šY† (@ T¤“O©Ï¶)@ P t9çñÃw‡ÐfÄ`øúË%î«3|‚v}‹ÕÉ­1}éL4Ü6Íýüиÿ+#ä³ñº,'o¬ <Ëãʶ¼±‚²-÷ɇܖïu•mYF>ä¶²®W¶ëètºqœ (@«8vìêׯÿ·û&ëiÚ´i©ëÙ³gZ·n ggçR“ššŠèèhtèСTåYˆ (@ P€å!p¯ïƒc²··Yì“òå·‹ò!ÕDþL®á¶áºr\~Û$ו‡Ü–åäs¹O9®lËg¹O>äb¼.›zè “ÛJ9å˜ò,÷/¦ö*#0\(@ P€e.¬O&efÝ8ÉLKµjÕ‚<‡ (@ P€ (`]L>Y×x²7 ,FÀÉɉ³˜,f4(@ P€ *N@¹Ì­â"`Ë (@ P€ (@ P€V+Àä“Õ-;F P€ (@ P€ *^€É§ŠF@ P€ (@ P€ ¬V€É'«ZvŒ (@ P€ (@ T¼“O?Œ€ (@ P€ (@ X­“OV;´ì(@ P€ (@ P€¨x&Ÿ*~ (@ P€ (@ P€°Z&Ÿ¬vhÙ1 P€ (@ P€ (PñL>Uü0 P€ (@ P€ (`µL>Yíвc (@ P€ (@ P â˜|ªø1` (@ P€ (@ PÀj˜|²Ú¡eÇ(@ P€ (@ P€ @Å 0ùTñcÀ(@ P€ (@ P€ €Õ ¨­¶gì(@ T¨Àõë׃ÌÌÌRÅQ«V-ÃÉÉ©TåYˆ (@ P€¨L>UŽqb” *€L<µnÝÎÎÎ¥Š=55ÑÑÑèСC©Ê³(@ P€ (P9xÙ]å'FI P Ò ÈO¥M<ÉÎɲ¥%UZŒ¬ám6{³MŸQÒqÓgq/(@ P€ (p/L>Ý‹ËR€ €ÜÁ®×›ÀµÅ›Øw×±ÊtH?ôÖ2“µ3Ûçû=/¯B]2–qƒ“›'¼¼¼àåÛ­úLÄü]ÉÐjãðí°úhûÎÜ1h?ëè‡è\¯;†¶ñÉ=ÇËÎŽpñçË:üŸÀ·W’D½^è³è2´ç"ç4>îâ…‘?Ü2Ø{—"çàÙÞ-è-ãðGhçxcå1¤é ÇéÜìl»©ì”ÇrpvÞ#h09ú—fqý1h¢GE^ÓšCx?¬¬úT¨1nP€ (@r`ò©ÜÉÙ (@ ü-´-Xö«BkþŒe¿& þV­<¹@:¶.| «OÞkòé~Ï3l\v3÷#>1ñgv`aŸ,óÖÞðÁÈw&¢Æw³°2./…¤MÄO³¿BÎøO°ò@<Å9‰¿ÅãŽøÇ†ØÜíË?b¬‡Ê°bÖµH\óú>¿n“–aÿÅ+HŒÁæÙÝ7{8ž›TÒ×Pv×Vâµv!½˜3ýI5HZ™zM«[âí¨²èS±Áñ (@ P€(&ŸÊ…™P€ @Ùèpeý·ØÑx""Æ7Ä–ï6 Åà3|Ɖå˜Ò3~^¾ {ŸÍDuƒ¼CIÇóc̾€Ÿ§Fë@9óÄAí†à£?r]Ùq‘ø`d‚ý½áS¿úLY‚ÃÊì—»1±^;Ì<¢)¨jßhú vd‰]Ù{1£Õ,Üù^é _¿AX«…&n fíŠÐoxû¡óÛq[×$ˆý£ÂÐÈ×þ¡Ý1iñ¡‚Ù7J fê,6Nй~ˆÙ:¿Lë…)ë’ðëä@Ñÿz·Zô½}(æÎ+®-¥fžmì|ÐqÂp´¾s1q9¨Þô¼Ýï,"æíÐûdì™OŽöÀ[SZ †™:îiwöa|>k#êMÿ  m—êâl;ø†=Ïþ=ñóçà—ü¤Qmô.«^Åì=r´J^Œû“{Fñ¯é’ke P€ (`ùL>Yþ1B P€Pr.àÇe¢Ë˜Áh>l4Úî_†5Ê,˜¬hÌ÷Oí°/ÇáøÚ‰ÈÜð â•ë¬J:®´!ž³¶ÏEø–†˜»ÿcObëçáx,¸¶ÈÀü§'asàûØv6±ÇVá©[5íg$$Á ª*ºª9ŠùooE‹ù{qéÂOïy Æ<‡ >oaóéxÄžÞåSÚÁ.ç ›ˆßüßŶsqˆY?óÆâím&æÙ˜¨³Ø8‹ëÜ1àãåo^ý³‡.bé°:æM”ªLŸwÿfZÜI‰Á¦yß`Ÿ{gtj ~#Eå€ÿ Gà/³±ôôY,›µ¾Óf “A†Q‰ç>žµq»Š>=ý`ü©NXtÂì8¬Ì»íC/`~¸V„ÏÅŒ’4ÑyJq¯é’ªäq P€ (PIŒß[U’°&(@ TEìã+ðßäx¦§lœûâéžaùcÄvD^èÔFlLì†)¯t{5 º{G¼øl·ü1%7ô´õÂC7öà§uû—U¾-:áaW•hc֜^ÞbªMÝŒš1‘+°9FŒaM&ÖuÙh6á}Œnl[µôÔÏøñl¦Nï ßš*ØÚy"ÀË9§Ö`ÕÉö˜¢ßo»Ã1¡O66­†œDUh1®3¦ø8ï§æL ÅabãÞÛÒ`ß;máã僆Fáãó±z&ºÚåVnã7 …£G!âÆh¼;& H¢ÈDb—RoÞ½ ô÷”êŠÙÇå«'wÑÞHÅM8ÁÅT2ËÖ Îö·qý†2«MÌXÓØ"ä…ùwX†ðˆÃ…îE¥ÔYЮéþ÷š.¨ÃÜZÉ}2w&÷S€ (@ò_#r¡(@ TLD-[‰˜øüƒ>`ÝÝ[H÷ø^þ-SSpÃÁÞµ fÁÔr÷„cÞ×,ÚŽ Ø¿„ÕëýñYÄLôs¡O¾Ž™Ó‡¡~J®Ù{óVAi[q‰Ÿ§6É×Ä«:ûó×tFS¢TNp„¥VÖéP¸Ny®69I·"1%ÄS•Ê´9P÷K+|ÓlyÌTÅÄ©o³˜ãpT,x6g”—*(Yx­Ä¶\l Ÿ ¿GÒ>l˜ìo&©TÍ^xí¿œ ÍÜÑ¢Ô×ÛÉ{/Õ+o8Þ½æE`ãè,ºžŠ”T1–NFqiRq-­6œÞ:U ÆóáØØ/ üŠ!*ed•n™hW9„â_ÓíK웉ºú”ßW(@ P€ @ pæSâ³i P€(½€îF¤¸Ñ¸'þ¹ñ öíÛ§ìþÏڮŷÛoÁÆÉiIH2ø)´ì›©¸•wÙ]IÇ G¢‚}èL_‰Ã,@›Ã¯cì'G ¿Âæz3 · J9ñ±H°qƒ‡«ü“jUr &Ó +9 2—a¸æ'lœ]áx3W2 Kˆš\=àæ<_Ÿ7Ñ–7Ò–¤dÄ~=5 Õoª³„8mJ8.²YúäXA/e¦M”y@¹!=¯ä¶rϼ—ÿªê¸Ã£¶\]ëä'ñîå|semüÂæs#/ëgÓ–Kÿc#þÐu@×b¶šÑR-Hü"_¸–„Žs*y£¨Ò-%½¦KW KQ€ (@Ë`òÉòLjR€ €˜ë“øó2lÑ-½àáá‘ûð銱O¸bã²ßp#¸z»oÇ¢{"2"škÑøbÉ6(¿‡§)þ¸!rÎåøýÄdˆzÔu¼àçZw2îÀ6dF6Þ‹mAü]1;)ýV|¸WzD/y©–Úõ}ã±mý¤ŠTvÊ>|úÅäª ›Ñ¯«CbpÀN,œ»‰¢Nd_ǹóÉP…ňÀßñɬH\ÊÙ+-2âádBátO‘ Åu q–t6pvÒáì¡£H×ÉËËt0gR(Aeâ¼Û2ÕŠÚ§nŽIoôÅå9ðæš?‘*¯oÔf îEx!üWøM{œg6É`«¡ás˜Vs1Þû>¡èÌ4“ý)ù5mx3}“Up'(@ P€¨$L>U’b˜ ª´@ÎyüðÝ!´1>…þr‰{î  ]ßburkL_: ·M@s??4î?š'Ç¡µ2Q¥FÛâëR¢1¦#½=àÕ°7þ£yÓÚ¢šm#Lþf ÆÏD†Þðmò$–ÛMÅʈ!p“9 Û<7k¼7>…Æþ Ðzh²G=ŽJ mä¯VoŽðï Ë©èÚÀ^mðÌ·1ÈV‡àÅo£_Âlô õƒ—w=´|ü_ˆŒ+9ù„ã,¡*ôŸú"<‰ ï`LX“ ­9“üŽˆçéJŠÅðü _·×ÐEØøyo\ý|4Z×ó„§O0zÍø~Ó×áÇ©çßC¬H¨êxvÞ+p-:kªHY¹£4¯iñKˆ\(@ P€ €5˜úúÎT¿L•3·OÙ/Ÿ•uY§²mü¬|ŒÏÆë²¬¼é‚ò,+Ûò¦ ʶÜ'r[¾ÅW¶eùÛʺr\Ù®£Óé"Äq. ¬RàØ±c¨_¿þß§iÓ¦¥®gÏž=hݺ5œKuNjj*¢££Ñ¡C‡R•g! P€ (@ ”‡À½¾6ŽÉÞÞþe±ONÈ—ß"ʇ¼Iƒü ]ÃmÃuå¸ü&J®+¹-ËÉÉçrŸr\Ù–ÏrŸ|ÈÅx]7õÐ68&·•rÊ1åYî7^Lí+TF&`¸P€ Ê\ 88XŸLÊÌ4º™‘™–jÕªy P€ (@ PÀº˜|²®ñdo(@ XŒ€““g1YÌh0 P€ (@ Tœ€r™[ÅEÀ–)@ P€ (@ P€ ¬V€É'«ZvŒ (@ P€ (@ T¼“O?Œ€ (@ P€ (@ X­“OV;´ì(@ P€ (@ P€¨x&Ÿ*~ (@ P€ (@ P€°Z&Ÿ¬vhÙ1 P€ (@ P€ (PñL>Uü0 P€ (@ P€ (`µL>Yíвc (@ P€ (@ P â˜|ªø1` (@ P€ (@ PÀj˜|²Ú¡eÇ(@ P€ (@ P€ @Å 0ùTñcÀ(@ P€ (@ P€ €Õ 0ùdµCËŽQ€ (@ P€ (@Š`ò©âÇ€P€ (@ P€ (@«`òÉj‡–£(@ P€ (@ P€/ÀäSÅ# (@ P€ (@ P€V+Àä“Õ-;F P€ (@ P€ *^€É§ŠF@ P€ (@ P€ ¬V€É'«ZvŒ (@ P€ (@ T¼“O?Œ€ (@ P€ (@ X­“OV;´ì(@ P€ (@ P€¨x&Ÿ*~ (@ P€ (@ P€°Z&Ÿ¬vhÙ1 P€ (@ P€ (PñL>Uü0 P€ (@ P€ (`µL>Yíвc (@ P€ (@ P â˜|ªø1` (@ P€ (@ PÀj˜|²Ú¡eÇ(@ P \4GñÉ€Þ˜u »\šc# (@ P€¨lL>U¶c¼ ªš€.K‡¸ÁÉÍ^^^y? øü´ån¡Cú¡°î”A¢IÝ á¿Dâ6ÕÊ=6H P€ (@Ê  ® A2F P€¨êj´›¹&û£b¿5IÇÖ…oaMÿþÂdSUU²ÿ (@ P€¥¨Ø÷𥋑¥(@ P  ¤¦¦böìÙxë­·L>ä1Yæþ®þ: MBÇcm’N_MvÌBômгdê·5 [0{TùzÃ?´;&->„´Ü¢Ðĉcc»"4ÀÞ~AèüÆvܾ»ëµÃÌ#šü°²÷½æ¡¯`ÇÝdü2­¦¬K¯“Å ¬z·ú½3š…"|gVn q‘ø`d‚ý½áS¿úLY‚Ã7ó•e[=†Ùká¹n¡ð3¹ÛÇœ]×W"¿]®P€ (@ PÀZ˜|²–‘d?(@ X˜€³³3ÆŒƒêÕ«‰Lî“Çd™û_Tpë7½ãÍékpåÎ)|6u!tS>Å+Ík9g°hÜDüæÿ.¶‹CÌúñȘ7ooK4'°`ÌsØàó6ŸŽGìéX>¥ìŠ FåŽ/GxóÚè·è</bé°:…ÏõÎz6¾mg{lžºQÓ~F²’]ÊŽÆ¢/S1öû#¸w «%#búbœ(Èw®“[ (@ P€¨ä¼ì®’ ç(`ÉÞÞÞxê©§°lÙ2äääèCµµµÕï“ÇJ¿h°ï¶ð™©Ê=Å&ϯۅw[;áÑ÷``‰;—k½Š_ÿ ™îÒœZƒU'Ûcê÷=á[Sœ×`8&ôù“ÖG#Ãm7~<†W–Çd•ž™§»rýþÍ©uXs¾Â×=!5c¾ê¾›Sc´½¬»:Ÿ‚N2Êêh1°'ê-þ —O3'Ô—m@IDAT•/\¸ OäB P€ (ðêׯÿje•†fÞæá:(@ Pàþ1lØ0üøãúJäúÿ³wðQUyÇŸIB %$!iIPŠ`¡Šˆ(6ÄUžʢ²ëª¯WÛ»”uQ䵬¸º*¨(*ЍØ@P!*E@„4ˆJ ”$3ï=“\2&É„LB&ùÝýŒ÷ÞsÎ=å{†eæÏ¹wLZå¶²Ÿùäh>@Ÿ¨¾]¯Kþ;VŠÿfsfgjÛÞ…š¯‰vcÎB…Ø£ÂíÛ´£E¼ÚX ¤*Ü\ö’¥ KÊiê(]oh\;µqnWöëñè&øäh¡Ö±îˆ—»BGh˜B­~…æ|·Á"ß.¤"€ €‡Á§à˜'z‰µ@=”›kÝîfmæ8›3ë-Ý?=[ç]ÓCŸ=4Eß xXý¬»áBbZ+6úbMûñ9𺟮`å*Eî^¯­æÑP¥ž¢‡òˆÊÞ¦gdq—2k¯Ê G…X¿È³û'eî³JD­Ò*ÌHSfH¬ZÇ”Üéî(Ê $u!€ € PkJ> ×Ú.Ò1@º 0`À™W@·Â-úÏíÿ£u#§kÆ“3ôP繚ðЧ2Ï÷K¹X—wúLO=ºP›ó¬UGr*/#U«3 ¬¼ tQ‡%šñħÊ2·ÚåïÔ†Ù* KPb» -ž¿\9V*û2=ýì"™X’{ i¡è(—Ö¿J¹.§ 쌢ì°äQÝ}©¦?¶HV½ÎÜ5zåï/hë°ÑEÄ©X‘ € €@= øTÏ&œá"€Á)PüÌ'ë×áâܯÿ̯ZÿüDMN­é÷ôWÓÐxzüoê±à6ýÏBë×ãÂ’uëK³4"sŠF¤$(.¾£N¼ä!-L·žìݰ—&½<]ƒÖÜ­Áã×á$]ýÒZå‡&ëúGoWücÕ½}gõ½xªòÇܤþöê(GK7ñVµY0ZÝâ“tí¼œÒ« B»êæg낌ÉÚ%^펿TsšLÔ«SG*–ØSp¾õè5 € €@•üý(ì«\YivºÙÛǦ£ö¹÷Þ€™½÷±)Z|­Ù›|ûÜÜ2hŸ›4;ß|E°ÏMó2çö±oŸ7s¹\S­|6@ N ¤¦¦*Ï 2õú–¹: Π@@ê”@U?GDDÜfìµ^æ÷ÍË<ä!ßëÜ3ÏÎ7Ë÷ͱý2禜Y~oÒì|ûÜì‹–ü[>ŽM¾¯—»°Gž9·ËÙyöÞ¤{o¾ÒJ•±ƒ=¥9A@@@@ Ÿ¡H € € € €>>ùd!@@@@ Ÿ¡H € € € €>>ùd!@@@@ Ÿ¡H € € € €>>ùd!@@@@ Ÿ¡H € € € €>>ùd!@@@@ Ÿ¡H € € € €>>ùd!@@@@ Ÿ¡H € € € €>>ùd!@@@@ Ÿ¡H € € € €>>ùd!@@@@ Ÿ¡H € € € €>>ùd!@@@@ a¨„:@ðعs§Ö®]«ýû÷{gù<WRR’¢¢¢|擈 € €Á)@ð)8ç^#€µ^Àžúöí«èèh¿úš““£+V¨_¿~~•§ € €‡·ÝÇ<ÑK@ èÌŠ'Ofp¦¬¿«¤ª#©îIKU{S5Ú@Á*=uþÙzty~Õš-Ë'PõW­wõóê²æ¤~j0j@¨eŸjÙ„Ð@2\Ùzad¬¢bÛ(..®ø• óŸÙ,g—T²K¹ß¿®wÖM0§*×åÈÂzjÒ» uÏI ޲‚ .«îúËl¾:,«£Î2@ € P§>Õééep €@]Ó)“¿UFV–²Ü¯4½{S»¿ÌrõÉŒ{5wõÑŸªrm]›×ªŽ§:,«£ÎªŽ“ë@@à8vŸ×ƒÓ‹^#€ÔFsËQŸs4eîtÐMíâ;ª÷ÈGõéú¥zzü ¥´S|·Áúã~‘ûFºƒ膎§hòʂãÉ_vz¥Ü¡Ï¸ÓΩô÷îÕ%ƒz¨s;kÕU§StùS_k§3[ïÞ>LÞÙ¦÷nîd­Äê¨kæîu×W¹HSÆ P×vñjŸr†nœõ½ö¸7%Y«¸*}­{ŒçkÆ’×uÇеK¸P³6}]¹q{Þšå®ï\Myk¦®?=Eí­ÕdN¾L±CE]-cÜžãð’û°Rõ—¾8?}¡=@IíãÕ6±§†O˜­v7VÞ|,ÃÒ=¾azpÎãsjWµ‹k§”¡ôòÏyE M¥»¬‚tkžÇ VJ‡xÅ'tÓi÷|ª}V™rÇR‘{þ&½}×EêÛ©­õžj¯n§ŒÔc_½¯Ê­×»o½½ÊsŠ €T§Á§êÔ¥n@šÈÿN³æjâ‚ÕJ[¿@ã>­±Ã'+ûª7´rËf-{¨£>¹÷I-*Ž=øß±µì4D×NÿP«6ghõ«#´õ©4ç·ÿäMêÕT#fn´Vbý¦F5“ ÑÌknЂöhñ†t­?^yÿ§ûç–4éhut×ZÏTšvß'ê=m©6ozSãÛZWeÜù+4ó¹ûïJmI_£×.ÌÖÔ»fégwL®ŒqoªÄMŽåÖ_¡‚Ÿ5íÊõQ§‡µx}¦ÒR_ÓØ½S5æö·•]^°ËTQ–¥ÉËÿ^Ï¿æÐíÖ{"}Ë÷šÙ•îºzŠ–4™ålåÕi_fõyúU×ëý¶÷ê£uJ[÷¹æL8EMüK9.‡>}B“uÑßnVVÚj}òÌ$›ÔT•2òç=hƒ= € PŸj™&@%P e÷Ÿ¬¶ö3ŸÚžª‡VØ·¼5Ô€+®Ó‰‘¡r4MÒð!äê>J7 n­†ÖÿÚ=L=¤iËŽJOŠ»ž[ý[g+}k¡ÏA¬™§×VŸª w¥vCÔ¤óeºvx¾>œ¿¢hÕ•Ï«Š+¼Ö•¯ž×>¬+ºG(4¬Â溪Œ»ŽŸ ­J¡‘ê}ÁYê˜þ«¶­2ãö=¬òë·¯)XóŽæm¨?Ý9Dñ¤æÉs÷5j½ð}”SQôÉ®Å×¾±N¿æfÔÒúqß­4èŽ[4(óC}°¦dÅ›¯«üI+Xý¶ÞX?@ÝóìPh“6ê×D…~¥l—ÐöÝtÜ®oôæ;Ë”~¨©Úõ¨bªŒQyï#ÆF@@ ÐÖ§16@‚EÀ<ói™Þ¿¹}éç<™ø“#B±1Vä¢x µEŽÈ(Eº4Vvh˜B­G“;ËŠ=¹ÊrX·×}a=×éÑ9ŸëçŒ=Êw9uè`Œ®-³x¦¶í]¨ ÉñšhwÆY¨°{*|0º3»¼kíAD©C‡H©¨öªŒÛÑB­cÛ½,2²úêŽ=Ur܇+ñ<(¯~rÎíÛ´#"^m¬!Ú[¨u›\çve›`¡µ ìˆ­¬ùò,èh®–-­ÀZñæ°Dq»•³³Œ7?u×åîs‹Ò}6YŽ%Â*TŽKhÒŸ4w~{ýkêdóø.¥\z§&ß5J‰™z‹·òÞGvö € €@M °ò©&µi @ Z¥¢2å5¢‡dñX¼t({›r|Ä$ò>~Pã§ïЈg–h]ºõ ó-sum¼ý×§ÃòŒC…Ä´VlôÅz~£ýPtk¿-[iÏ_¨’0éÛÑ]ëkŒ¾Òʽg^Y×–?nÏÊ?.«~Ï«B¬_0ŒÙ¡Ì}%’…iÊ ‰Uëc]Ñ|ié®ßµ[Û~߸)WnºÒvG*&º u×£ÈÝYÚZR½;§â±UP¶‹C)#u׬…úáËé:é‡;5î©•rVhtx˜*ï=XRŠ#@@ æìOÏ5×"-!€ p¬Â”Ø.C‹ç/WŽ€Êß¾LO?»H±Ã=,Ü›«ýMÛªK‡…n×Ê9Ïkavq”*¤…¢£\Zÿý*åZ+¢ \ K¹X—wúLO=ºP›óL9§ò2Rµ:ÓëV¯ª\{¸wÕwPî¸ÜlXò(î¾TÓ[¤ ëyLÎÜ5zåï/hë°ÑeE+š/–î.ºêóÙ3µt»µ4.«?ñ/}•p®ÎI¶~mÅcK¹@uX¢O|ª,ó ©üÚ°1[ŽŠÆR]á–åúìç­Ê³Þ.aÍâ”`­æ;w@¡•¨×ï÷`}!@”Á§@IR €@ x=ó).Aç?³¹ÂÛÙŽèXh²®ôvÅ0VÝÛwVß‹§*ÌMêßàˆ’j>üϺ¿ïšÐÃúõ±nÖ/êm¥‡6)ºõÍÑRçM¼UmŒV·ø$];/G®°dÝúÒ,Èœ¢) г~yïÄKÒÂt¯àSU®=²›O)wÜn-´«n~q¶.Ș¬¡]âÕîøK5§ÉD½:u¤bÍj¶ŠæË—¥éch‚FœY¨©#RÔ6¡&|×GO¼ðõ1wâm¦^³5ì¥I/O× 5wkpçxÅu8IW¿´Vù¥èê2ÿëÚþ•¦^Õ_â[+®ËÙúgÁÕšzûÉjP™zý}–Ù 2@@À ø{ƒ‚¯re¥Ùéfo›^ÛçÞ{;V´¾h|ö±)Z|­Ù›tûÜ<¯Ê>7iv¾ùê`Ÿ›2æeÎíc;ß>oær¹¦Zùl €@HMMUbbb•ÇfêéÑ£‡ßõ|óÍ7êÛ·¯¢££ýº&''G+V¬P¿~ýü*O!ÊÈ_ª»ûŽWþŒUzr¨b¹“‰ €”TöspÉ•EG·YG{­—ù×Hó20O-õ<÷<¶óÍ2zsl¿Ì¹)gžU`Òì|ûÜì‹–Þ[>ŽM¾¯—»°Gž9·ËÙyöÞ¤{o¾ÒJ•16@.””ä&íßïõPœ2Z —¹† @ ¸ÜŸ›Y#u!€ €TV€àSeÅ( à—@TT«˜ü’¢ € €u[€àSÝž_F‡ €@ýhpªþ¾juý;£F@j™€yf € € € €Õ"@ð©ZX©@@@@À|â}€ € € € PmŸª–Š@@@@>ñ@@@@¨6‚OÕFKÅ € € € €Ÿx € € € €T›Á§j£¥b@@@@‚O¼@@@@ªM€àSµÑR1 € € € €Á'Þ € € € €Õ&@ð©Úh©@@@@ @ªC`çÎZ»v­öïßïWõáááJJJRTT”_å)„ € €Á!@ð)8æ‰^"€A'`O}ûöUtt´_}ÏÉÉÑŠ+Ô¯_?¿ÊS@@‚C€Ûî‚cžè% tfÅ“¿'38SÖßURµ£`•ž:ÿl=º<ßw—ò—êîž)š´äïüêL=–mWç¸*S7•Ñ¢, € PmŸª–Š@*àÊÖ #cÛFqqqŠk{œzžyû$Cm¨•…õÔ¤wêž“X¹”ûýëzgM¨Šªõ_»Îê3üMû"[Ί®­jÛÖïO§v,^wþá4¥´·æ'.^»Ÿ¦ÑÓ¾Už?—S@@ Î |ª³SËÀ@º(¦S&«Œ¬,enúV/]vPÏ¿Só¶»jÁ`sõÉŒ{5wõQŸÜ#(_Æ/ŸkÆðíšqÕ_ôVNEã DÛU!tiçâ»tÎÈéÚzúßôÁêteenÒŠ7'ëºÓ»*¼*Us- € €@Ð | ú)d €@ýp4ŒQ¯ÑéÄC¿jCf¡tðÝÐñM^Y²*Ù=ê•r‡>7w½™[°úœ«)oÍÔõ§§¨½µzªÓÉ—éñ/vXë†<6kÒ¿/n«¡36¯8ÊÕ;ã;(úø¿êëâ»çò—Þ­žIÒ'¹Å·Õ}ž¡wo¦ ïlÓ{7w²VýtÔ5s÷UêÓgéÚAå´éѼ}Ò¤­ú_{™úX§µ[ÖëŸÃ4âÙ-%«  7júж:gæ÷š”mç§/Ô#£(©}¼Ú&öÔð ³õÃîb ½L‡ ~Ò³÷½¬F·¼¤çn¬ÍB%G#Å$Ÿ¡3{FÊa©R[ù›ôö]©o§¶–m{u;e¤û²È·ÜzmÌâ}Aæ"M3@]ÛÅ«}ʺqÖ÷ÚSjò½.à@@ ŸÂH% €5-P˜—®/Ÿ}E+âkPg??#…f>—£qÿ]©-ékôÚ…Ùšz×,ý\¯²‚&1:팭]òµvšÀľ/ôþÒ¶êÞä3-üÑ,ÔÆ/¾Òž~CtrãâQ;buþ“s4©WS˜¹QYY¿é…QÍŠ2V@jÁn]ý†G›¥Ÿ<Û,®¦dçÔíkõá?^Ô²V§i`×.úø~úù?ÿÕ/VœÍl…¿ÌÓ›¿ôÓ˜‘½uÁÑ´]ð³¦]y£>êô°¯ÏTZêk»wªÆÜþ¶²í€Œ?^V_œK´xcW]4êx5r÷Îë?UlëЧOhÒ¢.zâÛÍÊJ[­Ož™¤s“šZA/?Æ`w¥ðͼæ-hÿ€oH×Úùã•÷qºoq®]‚= € €@5 |ª&XªE¨-»ÿdµk£6mOÐõßž¡çß}X›øÛV ?A[7”B#Õû‚³Ô1ýWm)èÕ¢„A§+á‡%úv¿”÷õ}qÜÝvf¡´FÖʨ¯¾üM}† Pqx©‚ÆÃ4ðš[ušg›½Ú´«°Ç×V]úÑ“ûkêÜÉÜÄ¡Ö\­³·¾®WWšÛú ôÓ¼yJ?cŒÎkmÖ•µ•ÝvÁšw4oã@ýéÎ!Š·"F!Í“5æîkÔzá+úèðm~þxYÁ§œíÚiàÚÄúþXQÕ¶BÛwÓq»¾Ñ›ï,Sú¡¦j×{ NˆqÈ¿z‹l ÖÌÓk«OÕ„»ÎR»Æ!jÒù2];<_Î_Qé € €ðý)1@•S  €°Ÿ‰”©å Ôþ¬ýŠhiö]Ææ²—ðç;Z¨u¬½\ÉZä¦Pg¡µ–©ô–O|£áÃS““ÏÖ ¬Oô¡ŒZÞöt jçýW¨ÃŒò w¹kô«M»í2÷tÊU—+úƒ×µè‹¹z7ÿlÝzæÞ*ßvˆõ«1»3”¹¯¤Ç…iÊ ‰U똒±ùÓ÷výÕ¿í½;ƒÏ_¬z[E¤ŒÔ]³ê‡/§ë¤îÔ¸§VÊéç QHLkÅF_¬ç7fY·E¿¶e+íù ‹ù/ € €@µ ”|º¬¶&¨@jhÜGï® O<¨·¶Y”°%¶ËÐâùË•c ò·/ÓÓÏ.’Gl¥שCúê×7gêÓfÖª§.Ö´›ô×ÙýÖëåKÔè´ÓÕÍJ*µ…´Pt”Kë¿_¥\—S%ARåªpš4VWv]¢Ç|OFÖ)ö"®£h;,y”Fw_ªé-RÆAkQî½ò÷´uØh +w9•4è­›î®Ì§®ÖÄ/WÖ~3v—ìøUë3÷«ªmnY®Ï~Þª<ë9YaÍâ”ÓHò(´cK¹X—wúLO=ºP›óLDÒ©¼ŒT­Î,÷á[>K € €@e>UVŒò €µDÀ¡¨sîÑÉ‹5yÊgÊ IÖõÞ®øƪ{ûÎê{ñTå¹Iý˹+¯ì8ÔbÀ:þ·_äv®’ÜÏ3o¦A眤Ì_r5`HOQ­£¥Î›x«Ú,­nñIºv^Né_Ñ+»1ÿsBÚjÔ•½µym´.¹¬GIަíЮºùÅÙº c²†v‰W»ã/Õœ&õêÔ‘ŠõguX©^‡(þ’gôþ¬ ”÷ïkÕ/±µÚĵU·A×hæŠ=rU±-×ö¯4õªþêßZq]ÎÖ? ®ÖÔÛOVƒÊÔ–¬[_š¥™S4"%Aqñuâ%ia:Á§RSÉ  € P þ~¼ôU®¬4;ÝìícÓuûÜ{oÀÌÞûØ”5ÿ¶lïM¾}n¾ Øç&ͼ̹ù>`Ÿ›2æeÎíc;ß>oær¹¦Zùl €@HMMUbbb•ÇfêéÑ£‡ßõ|óÍ7êÛ·¯¢££ýº&''G+V¬P¿~ýü*_? 9•>{¤¾u®–¼{£ÚÛkÖO F €Ôˆ@e?{w*""â6+m¯õ2ÿêe^æAE¿"Srî™gç›åÚæØ~™sSÎ,37iv¾}nöEK¼­Ç&ß×Ë]Ø#ÏœÛåì<{oÒ½7_i¥Ê˜  €HJJr“öï·~2Î-<<\æ¶²¦½£¿ÍX§³þö¢<• E € €@­ øT«¦ƒÎ €uG **ŠULšÎ‚T=~Ö¹úß_"uÒ3õüyQ¥–ªêA@@ :>U‡*u"€ H°ºóÓtÝÈ:© @@`Ñ~ AÓ  € € € P>ÕÇYgÌ € € € €@ |ª!hšA@@@ê£Á§ú8ëŒ@@@¨!‚O5M3 € € € €@} øTg1#€ € € €5$@𩆠i@@@¨Ÿêã¬3f@@@@ †>Õ4Í € € € €õQ€àS}œuÆŒ € € €ÔÁ§‚¦@@@@ > „ÕÇA3f@nM›6÷è= €SÄÄÄcÚ~}kœàS}›qÆ‹ €@àCc™H† €@ ðX5 n5Çmw5oN‹ € € € €@½ øTo¦š"€ € € €5/Àmw5oN‹ €@½عs§Ö®]«ýû÷û5Þððp%%%)**ʯòB@@à øóD/@ 0§¾}û*::Ú¯¾çäähÅŠêׯŸ_å)„ € €Á!ÀmwÁ1Oô:³âÉßÀ“œ)ëï*©@`Z2I)=ïÖÒ|«¶‚Uzêü³õèrsRCÛ±h³œ¡¹ö,ÓÔËú¨c\œ:œöˆVÔ E9Ý" @@ °ò©L"C@ê…€+[/\Ü]w|ªÆaë'3ÂÕ*i°ÆÜó7M:³­ªôZXOMzwa€]Êýþ -© “Ywµ´yd3E)ôEN¥½ò žÈþƒ>Øøu.د†>º\Ví¤#€ €Õ)ðÍŠލ¾{RE4kvD: µS€•Oµs^è €€O02ù[ede)sÓ·z鲃z~üš·Ýå³ô±MÌÕ'3îÕÜÕµa QE})TÚ¦tEö ä&¡jѬjÁ¼c Oë € PÒ2¶ÖƒQÖ!|ª;sÉH@z%àh£^£/Ò‰‡~Õ†ÌB÷Ø 2iʘêÚ.^íSÎг¾×žâ¸TÞÏs4á¬%ĵS·WëéUÖêk•{Ë_ª»{¦hÒ’C’9îs¾f,y]w MQ»„ 5+Í©òê.H·Ú7X)âŸÐM§Ýó–Þ¼}˜&¼³MïÝÜIqquÍܽŕտ¹šòÖL]zŠÚ[·¾u:ù2=þŹ»ïîÓ0=8çq9µ«ÚYcH:A/ÿœWTÙÁtCÇS4yeÁá6ò—Ý£^)wèóƒÙz·¼¾8·iÁ]çêú—Ó”ñü%VÛ]tË‚/|ä§/Ô#£(©}¼Ú&öÔð ³õÃîb`wÏÑ”¹Ó5n@7µ‹ï¨Þ#Õ§ë—êéñƒ”Ò>NñÝëÿùE–²”÷©&ÐIÿßæÃ}æ@ðW`On®ÌЍ²^þÖC¹š øT3δ‚ `¼t}ùì+Z?Xƒ:[7Ýþ¢™×Ü íÐâ éZ;¼òþ1N÷-Ε­Ðã×üU«úÍÔw[Òõã[7hÿûï*ÃYF§¬ç1M»ïõž¶T›7½©ñqÊ®»àgM¿êz½ßö^}´.Cië>ל gkÔ“s4©WS˜¹QYY¿é…Q, Ï_¡™ÏåhÜWjKú½va¶¦Þ5K?Ûñ¤üïõükݾ`µÒ·|¯™ýW鮫§hùÁ2Æ`';ZéüòúÒZ#¦¼¯g¯h«ö׿­-Yëõô0ëž;oƒ6k4íÊõQ§‡µx}¦ÒR_ÓØ½S5æö·•m/<ËÿN³æj¢ÕÇ´õ 4îàÓ;|²²¯zC+·lÖ²‡:ê“{ŸÔ"3 k¡vݺé¸VávOÙ#€ €ÔQ*="£Žš0,@Z+P e÷Ÿ¬¶“­ØÈƒŠ9ë=ÿî-ØÄ:ÿiž^[}ª&þ÷,µkl-iê|™®þˆnœ¿By1_ꃬÓõç;©•y–Q«þºõºÓõ¯‡Ê¨+_=¯}XWtYUnݱ_ëõôç·M»¦¾6ê`õGE‹±L‚Ÿ[ ?A[7´Ê7Tï ÎRÇY¿j‹UÏñî*j¬Ó¯¹Y'µ4u·Ò ;nÑ ÿ{J¬¹_'%ûÙDeŠyüøŽæm¨Iï Q|#«¢FÉs÷5ú¿3^ÑG9éŠSyC ¸â:j'iøNz,l”nÜÚÊ‘Ú=L=-;¬¨_‰ºýÍ÷ÍEl € €€_Ç'uUófMý*K¡Ú%@ð©vͽA(WÀ<ói™Þ¿¹¶ÌºP§¿¼_-‹žŒíÌÎÔ¶½ 5!9^í:œ… ±G…9Ûµ«E[Å75¡¤¢-¼UE–µþ×¥"Ý'SºÜº·oÓŽñjSÕ<ŽjëŽ^¹;è S¨ÕÿÃ1,GsµliB8E›£IÅEìVÎÎ2–o¹ìåHö•Ü{˜qF”g¨uû_çve›`’ >9"c"SE[hh¨‘QŠ,fwÉz¸¹³Œ.Û×±G@¼Z´ˆùlñÍw+%??çDD4W÷®½«âü”õ±ût…&@ðW D¯z@Wç?£{_ÚäЄĴVlôÅz~c–u›[ñk[¶Òž¿PáQ-ÕbÏ6m;PRþîí-'â(‰S©Üº£c¹;K[÷—Ô]täp¯*òlÓ»6¹vkÛï%¸rÓ•¶;R1Ñæ¯ò…8¬@ÕáH•u§aö6å_åûbÚ÷ìOHlÅìÎPæ¾’f¤)3$V­cJ>Nx^sÄH@@ŽR !®Ò2³ü<™fâÛek\h’O‹®™ú@¨NÆ}4ñ¾áÚðăzk›Ka)ëòNŸé©Gjsž‰º8•—‘ªÕ™ K®³[}ª™Ó¿ÑvëJ;VèÙÙ‹åõð2{[nÝ)è¢K4ã‰O•ež¿”¿S6f«0¤…¢£\Zÿý*庬–”mÊl¨¼ ×A}>{¦–n·~=/«?ñ/}•p®ÎI¶1‡%(±]†Ï_®+•¿}™ž~v‘ljЗ°äQÝ}©¦?¶HÖ8¹kôÊß_ÐÖa£5,Ê#RWÞ<ó}§ÿ½øMš¿Í3•c@@à¨HkEºõ/\;wï>"¯¬„ÈÈŠhVÁ37˺˜ô€ | 8)"€ÔŒ€CQçÜ£;’kò”Ï”š¬[_š¥™S4"%AqÖ¯­xÉCZ˜nE›¬»^˜¬.‹¯U¯„u?ï*¸ôõ-ºc¯âSwÃ^šôòt Zs·wŽW\‡“tõKk•ïh©ó&Þª6 F«[|’®—SôËu·æ»Dh‚FœY¨©#RÔ6¡&|×GO¼ðõ1wâYc¿þÑÛÿÁXuoßY}/žªü17©¿=¾@ô%´«n~q¶.Ș¬¡]âÕîøK5§ÉD½:u¤b"ö¤‚=Êܸ^¿í¨è‰é¾9HE@ú#Ð.®µÒ³¶Éåôïó!µgÕS­zƒøûqÑW¹²Òìt³·Í ísï½3{ïcSÖ<µÔÞ›|ûÜ<¯Ê>7iæeÎÍGmûÜ”1/snÛùöy3—Ë5ÕÊgCê¤@jjª«<6SO=ü®ç›o¾Qß¾}í×5999Z±b…úõëçWùzU(©îî;^ù3VéÉAvD©^ 1ØM›6ä}}DÅ$ € €@­سw¯~^»Þï~EGEª[§²?ÿVösDe?{w4""â6+Í,º7¿cl^æa ÖröRçžyv¾YÎoŽí—97åLΤÙùö¹ÙÝ`ø86ù¾^îÂyæÜ.gçÙ{“î½ùJ+UÆ`Ø@¸@RR’;˜´ÉsŠÊk$<<\æ¶²\U[;UvÅä € €µX -s«ß½sYKWxÖ“ß\5VàSQÓ P¿¢¢¢XÅT¿¦œÑ"€ €Õ"À/ÖU kVJð©F¹i @£hpªþ¾jõQ\È% € €{ûKǾ'ô@@@@ Î |ªsSÊ€@@@@Ú#@ð©öÌ=A@@@êœÏ|ªsSÊ€@¨æg’Ù@@j¿Á§Ú?Gô@¼½R8E@¨­ÜvW[g†~!€ € € €u@€àS˜D†€ € € €ÔV‚Oµufè € € € P>ÕId € € € €@m øT[g†~!€ € € €u@€àS˜D†€ € € €ÔV‚Oµufè € € € P>ÕId € € € €@m øT[g†~!€ € € €u@€àS˜D†€ € € €ÔV‚Oµufè € € € P>ÕId € € € €@m«­£_ €µO 55µöuŠ!€ € €@­ øT«§‡Î!€µG Gµ§3ô@@‚F€Ûî‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 € € € 4Ÿ‚fªè( € € € |Ÿ‚oÎè1 P«rrrŽI\{–iêe}Ô1.NN{D+ò«©ùKuwÏMZr¨š¨ ÚcÝ~Ý#@@Š>U$D> €@¹³fÍRfff¹e|fº²õÂÈXEŶQœ@Š‹o¯n'Ÿ§›ÿù•~wú¼Â#Ñ©´WÔÙÐüéZ³`¢z5ðÈæ@@jÁ§Z3tN½{÷jöìÙÚ¸qãQ L§LþVYYÊÊX«ÅO ЦǮÑCŸäUPW¡Ò6¥+²ï%7 U“ˆf «à ²@@@àØ|:6î´ŠÔ)C‡éßÿþ·RSS~\Ž&j;à2Û-W[¶ì‘˪© s‘¦Œ ®íâÕ>å Ý8ë{í)ܦw«ë_NSÆó—¨}\ÝòáAå§/Ô#£(©}¼Ú&öÔð ³õÃnS‹µ™[×úœ¯K^×CSÔ.áBÍJsú®¿ø’¢ ½ÿëTú{÷ê’A=Ô¹µb«Ó)ºü©¯µÓ\sðÝÐñM^Ypø¢üe÷¨WÊúÜܱçîùšòÖL]zŠÕï8u:ù2=þÅ÷XÍEéÖxÇ VJ‡xÅ'tÓi÷|ª}&æ?ÎÒµƒ|_gа!€ € P[>ÕÖ™¡_ €@ ê7ÞÐW_}ut=wæ)ãËÿè½ô>:wP¬…¿hæ57hAû´xCºÖί¼ŒÓ}Ÿ5ш)ïëÙ+ÚªýõokKÖz==tƒ¦]y£>êô°¯ÏTZêk»wªÆÜþ¶²í`RÁ*M»ïõž¶T›7½©ñq|׿8·œþ‡¨e§!ºvú‡Zµ9C«_¡­O= 9›*¼O°¨Îüšù\ŽÆýw¥¶¤¯Ñkfkê]³ô³‰Wü¬éW]¯÷ÛÞ«Öe(mÝçš3á51W:·é½»uõ×ýu–~*‰sÕÏ@@¨…Ü¥P '….!€õG @Ëî?Ym';ä<´_uѨ‡Ÿ×C­XÌ<½¶úTMüïYjר!u¾L×D7Î_¡Cg,ET°æÍÛ8P“Þ¢øFVV£d¹ûý߯補‹tE„•æÊWÏkÖÝ#dÕ¦‚ŸÊ«ÿ 5,ÕBÉIxòS|Ú´÷Ùêßz®Ò·JmKÊ”}Ô@ÇOÐÀÖ¦ö†ê}ÁYê8ëWm±.OZ÷¶ÞX?@~ÛŒ×ÔÐFLäÉý õ0 ¼æVVêºîëN(ãoòM›6™JØ@@ ˜˜€Zêoe|d­¿ Œ@àèBCC5jÔ(õèÑ£˜g>-Óû7·WˆeÙµî==8î]²ÿC½•’©m{jBr¼&Ú5: 6b¼×9·oÓŽˆxµ · J¡qíÔÆ¹]Ù;¬Ò&øäˆR‡‘îÀ“)åÌö¿þõ:³õÅŒ{õèœÏõsÆ廜:t0F×Ú««,>pye8Z¨u¬;²ä.à S¨5&+ö$÷Z”ÃáêÌu­JWtó‡Ãå­> yjpŒ € p,>K}ÚFêˆ@Æ 5vìXuêÔ© #j Ènçë*kuÓ¹Ÿ~«ÜA­}±¦ýøœF¸ï=ó¬Ú½èpBˆõ‹y1»Ræ>+ØiÖ5I…iÊ ‰U똒;ÌEYîü˜òêw9â?y?¨ñÓwhâëK4¯wk…~©;{ß\\.D!+d"IÅÛ¡ìmÊqFÚ§î½g<3B¢c¹{½¶î·R}ür_Y×yÖÁ1 € €µQ äymì}B¨õÍš5Óu×]WÅÀ“f¡ön\ —¤«Ýñ)ŠJ¹X—wúLO=ºP›óÌZ'§ò2Rµ:óÈ…%ÒèîK5ý±EÊ8h•Ì]£Wþþ‚¶­aQ'ͰJÔo_V¸7Wû›¶U—1Vài»VÎy^ ³‹×a…%(±]†Ï_®+•¿}™ž~v‘L<ÌŸ-,å]Ôa‰f<ñ©²¬1(§6lÌv¯ŠòçzÊ € € P[>ÕÖ™¡_ €@Üpà Š?ÊÞ?óÉúå·¸6mÕíœ)J:CsþÚG Â’uëK³4"sŠF¤$(.¾£N¼ä!-L?2ø¤Ð®ºùÅÙº c²†v‰·‚W—jN“‰zuêHÅúŽ=I•©¿xt͇ÿY÷÷ýAz´U\·s4eû(Ý8´IÑ­|¡ÉºþÑÛÿÁXuoßY}/žªü17©¿UL>±öÒ¤—§kК»5¸s¼â:œ¤«_Z[ôÈ'Ÿˆ € €@p”õ‘Ü»÷¾Ê••f§›½}lê³Ï½÷vÌì½MÙÐâkÍÞäÛçæ–AûܤÙùæc¾}nʘ—9·í|û¼™Ëåšjå³!€uR 55•çÿÔÉ™eP € €Á q›ÕϽÖËü+ªy™5˜çHxž{Ûùf‰½9¶_æÜ”3këMšoŸ›½I3/³y›|_/waB‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B‘:@@@@| |òÉB" € € € €@ >B±œ:ò7ü¤Á¬Öw…V!çn=ó¯¯5m³«œ+ª?Ëu`§ž›ý™NúëêûTªxúØ÷éð¨k‰Ñáþê 0G>ò‰Üà TÔƒ € € €@P„E/kS']õÚ³Va£C‡îBÔsÄizqPNòÕçºéýçìÝ’¡¯Æëì6ŽÃiÕàRæ·kô¯Üxýçá.êè,TÃÆa ­þ†}¶p„—‘Ï‹Ž2ñˆ¶Ž²žê¾,XúYÝÔ € € P7>Õ<†¨÷ùƒôJEÁ¦rë.ЗŸ­Ö‚ÚXÁ§š ýdî8 ZªKC‡Â¬ÿ»­& j²­ªˆK?«2F®E@@¨OÇ2òP'÷g¥éo¯ÿ¢³ Ô¸e¬Æªöz(sëÕ”tè²þêÿ÷úŸU•÷ÓGêùZ¨Î¸|ˆ¦ö.=…»~×ÌwÖê_öi·+LNé©W/ŒUÃÙúç¼µzó×<ís4RÒ Çéž :è„p‹Ô´ñø:EœÓF¿}ú«>Û–¯°¨–ºjTOÝÜÉ¥OÞY¡ÉK÷k[á·êóm˜Î{¢šÏ_iõé =Ø9D¶¦ë‘××éýLÓÿ–}zs-[T¨ÛîLQ×6ýåÁµŠ»qîH(Z­U°iµ†ÿ»Pß{‚úÉ´ý‹Zÿ!A[¬Õ»ÛšêŽ¿œ¬!éëôÀÇYúéwk¼aMÔoð zlHS-›»ôHƒ{ŠŠúSp´cíÜP‡×“Y«Õ>zÓG[–wáîßõ¯¹kôêú< o¦!g¯ûFª¹ÓŒe­šo­õ‹6éËNµLì¨GÆè—Ö ëöjOƒf>¢·>©™¸Ýר᙭ôëg¿iiŽSÍãâ4áÒîº$Î;¸h­>ûq­ÿ&åö3O¾û£þñÝNmÍw¨YT¤Æ\ÒG·t*ý^ª“Ø € € €@Pð 5ÓT¸KOÿûg­Né«…·Æ¨E^ŽþõÒre¹¼Zi¤a£újsÆZ5ø,ý³·wpÂ*îÌÕìÐ'‰½ôÚC­ÔÊyPYûÃn¥?óâJ-éÔ[sÇÅ*¶ Wó^_®?¾ÙPo]§ÓRáN½øe´þuÝz²i¡V.\¦«ßúMgNꪡõ·+‹uwHo}tQ´ÂLÐd~q÷ wëÙ—­þ'÷ÑÂ[Lÿwèé–kEaûâ~ì¬g6ÍžßX“FÖý­­q9Ê?£ÑHÔ©m)Ë:fÞìÕ_×ú20ÏÆ²7k¬ÏUa¬Ýì{ ­Ooç^½øoË8¡—Þ|8VQ;Óõ—}¯Çcɺ#ÑrÜ¥9ËZë¹[ÎԴнšýÜWºeƽòd}rUe¯øA—¾³AKzöÒ™f -¿×V´Òì[†ê_é›÷¿Õ_úEÿœ¬^‡#afpEÇV¤œ~ÞïZ¯×4ÕSw¬á…ÊÌØ­=Qü±6Êl € € €µCÀþz^;z4½pê‡w?WoëÝ=ÍëîÏõ”õñ¬mZ¼;FãÏŒQŒŒhÐ¥•Ròó”±Ï~€|¨úõKT¯¦Ö CéÔ3Ó©»­÷D–_2æÊ˜”×OgT3uØŸ£÷VíTfA˜âZ09y¥@IDAT*©YI;!€ € € p¬X"qT3àû™Oùëjwx¸ÚxD›Â›7R‹R«^ükй÷ r¬ºZ7(]Þ™{@9K§‡F„+ÖuHÛM¤±UÞÑ@1&ðdoÖê£+ËsQ‘å¹7mîôj³qeûoµÝ¶eƒR·¼-ût¦}»]¿ì*P¾œ:TÐPcŒÇxvÅ}\c5 ˜ú·ÌÖÿ<ôî³[·úvBœæìŽ0µlVâj9:Â(¢x>!!îµ;í ž#TQMKÊ;5R«ÆÚ•ç5Xë6Àʘ”×Oµé¤Ù77ÑK‹×jìGùêÚ§‹î<;^Úb € € €À± ø@ÿ& q`Ÿ~7ËfÌmXÖ–¿?_{]Ѩ¢d÷M Ã+,q8×ÔÕbÿ>Yl:\—É iÞXÑû÷hëA뤉I1w‡YÏpr4T¬YqS¼Yq’Jo!M­@Ù½ú½Àº´¸ÿVÿ÷î¿IJz\èÑéCVg—«t„̳íýkÖêöOéºëOÓó ÔØ¹C?ºêpßÊ5ðX½Û iÖH-›Äéáûzk¨w°ÆºÑlžc9Üé²\ھׄøŠP®û•¹¿º{Ì‹¹´²&åöÓª¯‘õl©[ÇÆéæ½;5ëß+ô§EM5ÿÜöš&Ù@@@8f%Ë4ŽYêNáq­uF³ízéÓYÏ›Vá¾]zù«ßµÏ×­B‘VðhSÚn+8etìÕ3ÅeCãã4Ë-Ž^Udȱúh+4.^Ån×3f+ýé“KûwíÑ/»=¢köØýÚ;õÍW›ôÝ^ëúƒúòãMZÕZCâJG+kR^? svê묃ÚoÍGhãFŠ·Vj7eÌËœÛÇv¾}ÞÌårMµòÙ|\³Jg¼ÛXÏý¹›ºÛ³ã£\½O2¿8åå_6Dt6oW6j@jjªkO‡è  € €õH ""â6k¸{­—¹¯É}o“µ7Ùñ<÷<6ËL¾YÉbŽí—97åÌM-&ÍηÏÍÞ¤™—Ù¼M¾¯—»°Gž9·ËÙyöÞ¤{o¾ÒJ•!œPŠ£¾ŸjÓ¦úuŸÓý5·Õ½ðé65HŒÖq¼Süzs¸Ü>ý*J!@@@ê…€YýÆ@±€KÛÖ­×C/í¶”í²~ì­¡º$wÖ?FÄ*#@@@@à(>Zݽ$L§?U ¯»#¬¶‘…Fëžÿ9³Úª§b@@@‚U€›©‚uæè7 € € € Ÿ‚`’è" € € € ¬Ÿ‚uæè7 € € € Ÿ‚`’è" € € € ¬Ÿ‚uæè7 € € € Ÿ‚`’è" € € € ¬Ÿ‚uæè7 € € € Ÿ‚`’è" € € € ¬Ÿ‚uæè7 € € € Ÿ‚`’è" € € € ¬aÁÚñ@ö;555ÕQ Pë6mÚTëúD‡@@@ ~ÔÇà“ËšZÏ—ëÇl3J@@@8žqs\¯¶ºzÛ=©f2íãz7¹õêÌ`@@@.ïx…}\£ð£·u1øDɉ§ € € €ÔJ:ר+Á§:¬• è € € €T·@‰ukðÉž€: ¬îw.õ#€ € € €@Ð u$Ø‚O6vнKè0 € € €  ‹Sð)P«œ¼')PõàýC € € € PÇ<ãÞ1‰ª Õ³ÞªÔSí׆U{ Uk “bz4S56®F@@@Z,¨ø„7 T}ÕBV[W>ÙxU´]O­ž„ª’ë@@@@ (·°ëªuµ5øT¨Ê`W¦lUúĵ € € € P?*{¨L٠ѬMÁ§ªû{½¿å‚fé( € € €…€¿1 Ë•5èª^_V½G•^ÛŸùäÏ üõ·œ?mR@@¨”À|§™·VëÓw«Ðé¬ÔµuµphHˆº´k¡ÿ¹8ITnm„ËåÒöíÛµcÇ:t¨®Uj\ 6TË–-#‡ÃQ©kç®]»”››«ÂÂÂJ][W ‡††ªyóæŠŒŒ¬´§&þÆ(ü-çG“Ç®Hm>ÈÊý©(òòg**SQ~•ffß¾}Ú³göîÝ«‚‚åçç«Aƒ S³fÍ¡¦M›V© .F@@àøÛܵZgž5iªFÖw6©Ðúž´6m·ŒÍß.O©‰ <åää¨k×®îïV• ¶Tª± (l‚Gæûçúõëݽ­T¯wîÜ)óÖ\׸qãê¶Tª?Ǻ°ñë—=§GkÃ?ÞiZ“ª\{dOêFJMì„÷—~ûÜÞ{Õ•nÊ•—ç]Ï1=?Á§ÊâTTÞW¾I++½ZÀÍŠ'ó0²ääd÷¾2x^gêaC@@º'`þÙþ¢ÂÞËÂ×··º÷`Dx øz×Ûÿ×àYÎû*ëY¦¢|ϲþÔç]¾Êç5}ÛñçyMþ ¬,\ïtsn¿<ëõ•æ™ï÷±¹ÍÎ¼Ì &﨨ùµ€ùóç~蛹'ù‚ .p?-ß³s]·nÝ´råJw]æ¶<6@@¨CîèS ¾Õ!3î©cÊp|øŠAØifï¹yŸÛye¥Ûù•Ù²® Û­Éà“˜¿ÿOkÊ–UÞÎóœ¯t“æ½y§yŸ{—¯ðܬV2Ïx2+˜<7xzì±Ç”——w8ù»ï¾Óš5kô׿þõˆ”¹ÞÔcn¿3A*6@@¨;UþâQi ‡"ÛµÒè~­tR\cE„:•»kŸ–§fè??äj—yÌ’£‰.½²»FÇäéåWk^ŽKŽðHÝ<¶‹†6?¨æý¬Ù› +\vQé®y]Pó6^¨-§ù_éŽä1:øÜz=}FÃÚÒ«Ú×çV}óúbº\ãÅ ]•&ñ~‹{Ÿ› ½Ó̹wš]ÎWzEy&ß{+«ïrU>¯³TÞàËËóÄ0å¼ËzžûÊ÷¼ÞïcóS”ö¯Úy_dV<™À“¹r¿Ì±I3y¾6ó (SŸ©— @@ꎀy®‘ýE¤&öM;´××tаö䨳Ok¶”³E„Î:½›þ6¤…šZ´ž_’Œ´ËÑP‡vÔ™-\Ú´üWýÛ <™Uµ÷÷˜®|rjûÒgõ§óNT‡èæÖAE(¦c]ôÄ7*YF`tØ*/P¨Ì¯_Õìÿ.Õ¶ÂÊ_]¯°ÿ(ÙC;â •áf—õÞ—W®¼<ïzjä¼¶ŸÊò•gÒ<Ó=m@Ï4_å=óíküÞïÙ³§Ì‡‹oذÁ]Ïå—_î^å)sl6;Ï}âõ€2õ²!€ € €@Ý¨ÑøŠ#\ç j¥¶Öj§u߬ÑMÖª¦{_ýI7¿’¦ŸóCB‚Îñ¾1Å¡ø^‰º¡kCÈÌдoöê@ ñרM©1¹”óÑm<üIeù¤>Û¼K{sךStóÐ$…—*ËI¥¥iÝo ú«ÖfäWúò:tAE±_q Ï4ïëmÏ2vš½//Ï.Scûê>•äk€eÁ”U‡wyÏsÏcÓ–ç¹9ö>÷Õ¿Ò̳žLPÉ×6yòdM›6­T~nn®»hy?Õhê3õ²!€ € €@ÝðüRÝ£rD´Po\Êß­÷¾ßw8ˆtð÷l½÷k®Þ–z.JƒØ6š8ÈZu`·^ú`«¬EO5¶Õ¤M©A¤jÆÏ«Ñm¯kÎíC”ØÜz”Š£‘b»Ÿ¥³{GY>Nmyû/Ñ·“ÚD6U³ÖÇë¿!ëîDËÖºE.étM~cš®<¹ƒ¢›7WëîèoŸm?ü…³ íCM¾ô$uh¡ˆ¨¶:qÒÇ2ßô 2>ÔC#{)>2BÑNѸ.×îc†PJ$ 'y›Ö*#º»NK‰Tں͇߇rß67_ß®û^½9G/¾ð¢þóέ˱ޛf«(¿¨”õÍ~R翨÷~Ê=l.×n¥¾ó¢ÞõL³Ë»½çìšcïsÏžù›g_ã]Ÿnöžuy¦{—W‡wÙ£:¯îà“¿òÄÔWŠg]že=ý©Ç¯¾¨Q£F~•5§7ÞxÃ]vàÀe^cê3õ²!€ € €@Ý(ºíÎa}¡©þ—£Y#µ´¾í9s(ãg{.eì8$WjÙºÏÚ»¿@Y—3OW×.eþ˜¡w™tÏëªùø-}r¦-ÖÇë“õ‡Ë{È÷·ºÅv9K7=»Dvì±Q*ëïwé…æfDkË_¦iOïÐuó×+g×½3j›¿íŸúÑ|+øQOþáJÍO˜¬/3vkgú ÍÔ_M ×jÚåWi~ÇGµtë.¥|£ö=v™îü¨ŽÝýb†6¬Û¡ø¤ãs\WµÞ¶V÷z|]wfkÍz‡z7FW»L§·Ù®o}§l;èYQ¾ñwD¨sRå¬ß ]ÅU»výª »Ú¨ëqÍÝïoS¬–låÅ$|å•Õmï²e•³Ó‹eìÓc³¯Îà“¿,«œ/Pﲞe¼ólQïtûÜóZ»l¥÷fSƒ *¼nçÎúßÿý_÷ÃÄû÷ï¯áÇ—y©àS™ïÞ›·L‡j½zsîX§_ò•œ`­°kÜAÝöhÝ/;‹‚î¦ÃÔ6ùxµ² nÝòy‚âó¶hóÎâÀž*Ê/꓎Éj¿o½Öÿn®sjÇÆÚûÿík¬­Wužç9ö±O|ÃæØ`lƒ1Ä6`ŠT jZ”ÒTE -iJ1‘æG£öGU¤ü+ù‘6 R/ê/ªD¢$©\µäB[P$Ôô!„›ÁÁ6ØÆÀñ _}Üõ®uÞ½ß==¿Ûºí½Ÿ)}{ŒñŽ1æ7¿gm,­Á:k_{Sy嶇‡qæ ¢þ¯cïf={ë\Æ®·fÛ¥;o;¶Îõ£m÷ÿÂFoq |Ƕ:dKOm©Ñ_¨;}zøß®ÞvÛmåÁ,o~ó›Ë»ßýîräHýï«wo¯ýÎ?›ˆp÷Þx€ @€Àf,¾n|3{×»žyôT99{/~ôÒãåÚ=4íh¹öÄE§ûä#OÏ¿L|ÞûÜ©ò‡ÿõ«åß}íéòÜE/.ïÿ‰KËñzÓ ÆÛd“qôŠåòçN–ï Vkù~ùÜ¿ù¹òÖ×][^|ÙìËȯx{ùw>Sv>¨uäEåê—î~3Ô‘óŽ•óÎ<;ÿdÙ™Ü_N^~]¹¦‚œ¹ÿÞrÿ£Ÿ*¿ðr}¹ù%³ëÅå <ñèv_ÖY•öLùÞ7¾Uzüöò™ßüDùÄ'~»|öÛO”¿y{|ñø±rüxüÅøc—‹Ž*Oí|ÑØPþ, ®+¯yõsåÏ¿ù½òÌ™“åÏ¿}ª\ÓË;>ɶosöà9Ej:ؾo‡_Ç71|ªöÓp³fŒÖªÑ©×¾bk²þ¯‹5õO^:uj°ï[ßúÖ¼æío{ïàIEÚáÓ R @€ p¨h`¡7Û¸ÎÌ_øáìNÇ^TÞñ/ž¿×}/¸ê%寚ýÝÏ>Q¾xwŸž)ßøTùãÏßWþäÔ‘ò²7¼¼üÝ«ôOí¶sÞaΖ_Ñ£¯øÉò“/ÿJù/·}³´¾øä‰ßûå½ÿödyçǾPî}øÑòØC¿Wþéuù6úÈìý]ûÐGO\U®xøÞr_õ'ó޾äêò’·–ßþþcóïúÕ÷ý>öÄ“å¡ßúû[øµO½&õÔwÊ7¼ñïÜZÞõ®wͯðîŸ*¯;úírûÎ?]žx2¨?=ãðô…åÇv¦žCyŸõ¼òÒ›o,Þ}G¹ç¾Ù§ÎÎÜPnºvø_'¹{KVÿSÒÒÂ~þÏkžŒœb×9g[ëuÜÕÛªóžiU7¶6ûzýü_Moá–’­L-}ɱlúÎÕšt¿Ø­œò“–&Õ?<ûÑëÃþðüËǯ¾úêÊ2ßOû² @€ s‡€¾óiþ®EïD6}y²üþý ÜóìÑró_}MùèÏ¿¶|è=·”¾ïºrËì{îýÓ{˧5œÒ9¼>r²|ìÿ>^NŸwqù™¿~Uy©r›>«î»_Ó§co*ÿü—ßQîùÕ[Ë?ùõ?.÷=¹xاNÞ1’PeûéêKkñ ¶˜K,”…–9û¶Ùëç»ÆqO}_üý>µÀ´ôÕ9g›p­ÉÖæ¬«}Å“Öe—]VxàÁž|àEט¥ý´/ € @8wl{¾òØÝß-¿ô;ß)ðÙä㲋Ë-×\XŽ>ò£ò™ÏÝ^~é³ÌÿêÚóé>WîþÂwËïϾ½ù¢W\S~î¦c[ùÒæm³Ù}îÙ?C|ïÇËç>ñ³åñ__yÃ싯.¾äEåÚ7½·|ä?R.}ǡþòÊ/¼ú²réµo-¿òÃ[Ë?û©‹Æ1¹à/•þçÿXþÆW±¼éêËÊ%'^Wnýµ¯•ÓçßR~ñ“¿QÞyϯ”¿yýåÒË®,7ÿí–O}ç>ÍþÚÜßøayÉM¯*—ìùTØ‘rÅßX.¿ïåýø#—”ë¯{®üéïþVùØÇ>YþèW•·¼íeçë·†ò»/â|¯WÝ|eyô¡ãåÕ7^YökБGª|Ï($×~Î-2_×eN~½²¾Îe<¶.{Vö÷ü*ôìÖªÒ”wMZëÖô{‘š|ÿ®È:¯ÙeÆúR$ÕJ÷¥¼|åRÓgîRw^úE?úÑ~mf—^ú'u×_}9qâDç<}ä#é¬QBƒ§»ï¾»Üxã½u$!@€ ÃEàoýòÿ(W¾ìºÃuè-öä÷î)ø¯Þ6én_þò—Ë[Þò–I=/”âÏþóå oxäǽóÎ;Ë 7Ü0©géâ3÷—ÿõÉÿYÎüµ÷”·\£·ñÕÊWåšç<öµO—Û¾}}ù{?ýúr©&k\«²™}¸äÏŽ£€©/ŒÖ·ÕkÒ˜Vº>8#-õÔ¤kp¤^YåœÏx&ïùŽrÎ+çØÖš­t¯ô§j®Ÿ[ aÖ¹t°¡—¹USkuì3J÷r5Ùôõ"hà$M¾~£ë½x+¯—½ìeå®»î*—_~yÑ·ÖÐÐI=úËyÚçšk®im@€ f~·r˜ŸaSg‡Í¦Èè}‡^ö¡¼îÙÇî,ÿïË——ÿÄÛ×>xò=Ö`=<ÒVz4_šWøQÓwÞõik_±—÷ꊥ×5®M;¦&ë{ýuŸzo6KêðõpªÖêxhOå ¥e}?åœ÷0*{å/µ¡à’rûí·—×¾öµƒ_(Þº‰þ³ú½W« € @8¼öí{2Ø‚é ñÌå‹ÿíwË—¾°¼ä–·–·½òƒxJɳùšGhåŒÂqŸUnìÊû©§Ž»´±ûO®Ûöðiè€âa‘j HÖ¹´uzÝãœ{û¬jWZúô“þ¹Ü׿þõróÍ7w~ªu}âIƒ'Ùë®ãc¸-Fh€ @8ìô†äÌìÿt>ÒõçÑû.y~ žòMÜ”mÔ Ï½ÄVämçÑ«Ë_yÏ?Ü{ðŒ†ò®=z¢¼ñg~¾¼Ññì*<ã8}óˆÌ¹Åÿ“cëšzÇûjÿ¸r_ÏÓº¹AfΚmóGÕœ—M?óÖsɾþƒ§ï}Ò?»ûÒ—¾4êKÈu}Ç“êÕ§~þÃ9= € @8Î;ïh9ý´þì+ ˆ‰ØL]\pAyä‘G¦¶óõb"6S—Þ“>õÔSSÛÎùz1éúz ¬çj·ž~æ¥{yvak]¶¥e~ßýýüä“àÔŸrr\çÊ0ËX¾/ÕjÕyizuý—MþôÿÂÍšº–Gú¾¦Ç{lþ)¨ï~÷»ó/!¿âŠ+æÿãמ~úéùõÐCÍOgΜ™÷èŸÛ± @€ s—À-¯:x9ñDçŸ^¹å•W–ý¾[Êñcz‹6~iØ¢Á€þ5‰þO~V™èAƒ' ¦ãÄóá‡.>úèü+aà9$Ì> v饗Îÿ¸ØTžÉoöÞÿÍâÇfWýWîôkFÙ—®Ø_R.Ý+kŽe5T’®e]¾tÅZòë˺¬–óöçbõC5õji{j4°³Zu-M{Y—µßÒ3/_C"×Û·îXÿ5²&_º®ôkðä¡”¬jtÕþųáÓoÌt @€ @X+Ùðéý³ Ÿ]9\ª} p¤Y÷IVƒ%ÇöU/_Ö9ùŽgîŽ/MËy[kódäkÝù.ÝûgÝ_ƒšM®| )÷©î¸eó™·.›/„ü̹gÊù¨… @€ @cÔ3Ï%rV‘5ÚÓ³Š>=ë|÷9c}1µKÕlzøÔ:T‚èò³Ï5¶ÎùÅr¬|^ÒU£•ºûd=%œñ€ @€ ¬™€fù‰%Ï%<«ÐíZó å½ä»&5ùYç\­gMúY¿1?†Oc¦Úlíg¬{´^L÷9§:÷ÉgA€ @€ uðÜAÖóˆÔä;§û¶rÖ3'-—s©“硇μý!+h®1@ÅõåºZw¯u½èš>² @€ @€À&䧞깄綺¿ý´y.é^ö‡¬ê]ãÞÚåëúÑñ&‡Oõ!ò!Òw5Û–®œóöӺǓDÅ®÷‹­œóι @€ @€ÖMÀ³ Ï$dýaÏ&d¥{¹'­r­Ø=ΧmåºòY»6_ùí .Áô_¿K_çUÜZùÈ÷‹æ¿—yå¤ûÅní‡@€ @€V!†É¹„çšOèªsõ=O]šWúÖ„ÝÄ'ŸZ0º¶Ó;g«=íwÙ|ñ\¯Z]uNy @€ @X'}ð¥žCäÃ9Ý3õV<¤)¯å}ÑócëµU_Ý[×LŽW>ù@cÕªiizë¶©ùË’´¼Z¹zÚ¨øoŠ… @€ @k&ðôl?Í4³Ð¬B—ã¡9†óêÑRœ+cû¶Y'¿¥·´VŸêÆÔÖ½óx•áSsÃ%DÞ6·È‡“ß(¹¦~!2v¯öv½íSyC|@€ @€ °Fš;xa«ísÆáXV5Z™wŸ­sª³&?—÷±ÍÜVýmç“Ø7“­W_^¹>`ÎÛjoùzAt/ßWÓEß[Vy @€ @ØŠ4ŸŸzRì¼çi»Îãš:/]Ëvíþ´n»›Ù ç!ÌÐ-†êZùÔä;N[ëÎéYéûZ²ç=¬|×IS½ljµ®Ø5¼yYÅÇg×…¡»Vù®½t_ß3ëRËùº´\cͺ¬rZ©µâyQÔõÕ¸ÖÖ{;ÆB€ @€6E`hàἭΑ~ÆÖkëAŽk•÷¥œ–cY××Öƒ¢¬‘fÝ$ÇêOÿÔ,Ö'žôU?Êùr¬Z×Ëê>Ö|ÏÔ­i-÷Ê÷Þò]g_qú§¾¨ZôÛ·uãÚåç—ºi›±Ø€ÔºcÙÚ×y­ ¸†6®Ó‹¡¡‘tÝGz¾P³p®_ŒÓ³K{h¥[iFÙ¦&_—zÒ¦¯\ægáN­|å´Ü#ßš{+—˺­rég-> @€ @à Ð{v/û¶Öm¥û’æ:Y¿ïïÒ”w¯}Ùô½u{Û·U­†J¶Y“÷q½ïã{È*çÚ™»ç¬Ëj9¶?ÏþpMj­º:¿ÑXƒ•m-Èaˆc[£å§–5Ò³¯¼^D oêœbåt†ôgá\Ó ­œj´ô‹#>¶îUNK{x)ç!—îíØ÷ñyTï¼îå³Hw,ßg°&«Õe[¹y? @€ @‡€ßsÛêì·¬´.]½zŸíY¿ï–Íœ‡@Ö2–ïz[íUžêÞìó\ãx¶ÍÎù»rªÕ²]D»qê-?µÖ>Þoív“Ã'=”‡%yð.Ý5­|’ïû굦I÷uα­tû¶úEÐðHVKýÊY×/’òZÞw-ê<\ª­öQ_ëžÊéR¬–µôk͹´ò[Ëû¶rÒ†ò]}è€ @€ ©ôÞ¸oÍ».­|_ºGíë=¾µ–Õû몭/å<'Èœûd¥g|ç3—÷q½5dzÖù^¶ÒÓÏú³©ãÚ¡rºò]zÕ>=ÜäðiÊiü€†dœ¾öT¬+kõ"zð3swjÜëÕiùSJòµ.ýBxYs¬½µ¼Ï"Zľ¯s¶ªñàIûyÈä½½§Ïî=e]+_õZV¾—ë§ï<€ @€ p èý´Wúµæ\¾ÿNÍïýݧØyûîÍØsiy©6cšZšj—­{3/èÒ=TãåúVÜ—sý¾Øm Ÿ 51ÀVN@²Ï¾¬/ÕxYÓ^öõ"i#«å\ÆþåÊ”4‡Ô§z³ÒÞZ¾‡j•s,«zß˃$kÒ¥©Ï~žg&ïôËW–lúsñìë »ü¬·ŸµÖ°€ @€ Mðû鮽3ßå«×9Ùôkizßmݾûe¥¥ž±þ5”–5½ŸwÿÉsiU£Ú¬WÞ÷u­÷²>+Ù©IßyYëgÝgsœÖ}©Ù÷^Ž7b=PÙÈæMõPcua¤^ûº4]zu]µ6“vtÕy$½k©F¿P²Riù>г¦ûúSOyë©É×òymS«}ÅZªÕ²]D»?»ôÝ <@€ @€ÀÁ  ÷Ô­eÝÖ5Š­µü.Mý¹Æ6u½_×¥œukг&kÝ~öùYomÖº3g°f«œ–b/çRS®Ž]_Û±uußRñ¶‡O]‡Ì‡öÀÄšcõJSœÖº­sz1ÝkMV—rZÊË÷Ê{+§¥œ?Õ”ŸŒò>ê“o«zÕå¾Ê)¶õýfÒÎùœS—|_Òê\j­œò,@€ @€Àa$ ÷Ú^ök«|jò;§XïõœËX¾kRÏ¡Q]£ØšëòPÖl]›}>‹¬/åµ×Ö¹´òµ\»ˆvcéû¾ÆŸZ‡ÍÇ:D÷ʽ§Õý|&ÕÚ—õ•5~¥y¹ÇC% €ôË"&úѲ&«zY-ßCû¦¦³XscŸAº|?£ëÏR;C§Ô¬·ljò[«ÞK5-­Õ‹@€ @X•€ß‡ç>-­+ïÚÚº^º¯Zó{rç3–ŸzÆÒ5#°fkM±5Yœrå=œs­ïÝegÛîœÏ5ÖÒÊ×RͦÖR{> Ú7¯‡©ËW>5ík]~®ºN¹¬µï:çõâii¨£ÖŸDR.åu]éûìêQ¯–ê=œr,ë‘­öñ}üŒÞ[5ö[÷tNû*ïK±—4­:g}‘ÝýÙÒ[Ún @€ @`ûò=½ïÞÒ”K]¾cÛì¯óŽ»¬Þ›;—¾FÊY—µnM±|_õ^ÒµrùZ²¹Ÿ÷ôY7o8Ûc_Öµ©Ùw]gÝÖõ¶]ºó£íº†O£o8P¨«‡$~X뎵•ýÌÉ—®K¾^@-dS“¯Á‘¬rêÉ_ í#ÝvæÎóÖT_ûê·ž¾4-Ù¼º´Ôk_q.ï--}×´4ç°€ @€ ý  ÷ÚõJ-ý¬“î\úªqlÛ§å gÈWÞ5öÓê~u^šæ Î¥ŸÚ¬dçÜöeµTW_óDèŽmU`Ö~ŸÂCQ\û®4çägmêöeu©N/¾÷qÞ/¸Où 'k*i¹GV{)o_5Òd}/åó|®™ÉsݵŽU›šõ´µ_Ç~¾ZW¬•ù…ÂO@€ @€ÀþðûíÒ\ï|jÚ×{÷ÙYÛNŸêr9v¿ré;Ξ­ùÛ>éÁ§A PP²7uï+k]/`Öë…ö€Éƒ¿È6¹^{¸væÎCþ‹vêÕR­ü¼·tiÚWV—ê\3swzä+çKçó²&«e»Œ?߀€ @€ CBÀïëuÜ)¾j}å£Zó^²ždNš®Ì;¶fëZÝǾr9lRlM5Zî·îšÔ­¹Þ±l®.=kºüz¯®ºµèÛ>é¡r€âÃ×z»NÖ9ÙzI«óºŸ^\ €¼×C%ÿH÷>«z½·|Õ:Îý]ç¾YÙ¼NõÒò²¦éy>×9§8W¯s×~½W'† @€ l›€Þc÷­:ïØïͳ7µôUã÷óòkYÕi¹^5òëØ½™«}íãO<ÉwÞ¾÷ð=¤[KßZZçe[Kµ¹êع.ÝùµØm ŸêÃêáêaˆµ|pÕ8¶o«=½‡j\']K/ªF²ª•¦•¨Ô½ŸBªW­–÷S¬_ÕziнWZùÎÏÜùr­¬÷’¯åÜ"ZÄÖ[Öui½×–y|@€ @€À¶ Ôïåuÿ–æs9×eÝŸùÚwìZZIzëR^Ks÷JóŒA6k»ÖV}òµ²×zZùyÍ›BS¬¼–ëÒÏܼ(~8ÒæÝý>éA[ÑÖÓº6ádoêé{/k²z‘=,R^!kŠå{o÷)ö/…RÊÉWN¾{fîÜwN±}ï­Z]ÞK5Öl¥i)ÖJk‘Ùû³/··r7Z¦g·€ @€ 0ž€ßkïØ²´z´Ÿ÷¬­ê·õzn-­t-i®QlßµŽeí;§Bö[W–g òë^×vYõxe4Åc×”Ú±{öÖmsø¤‡Ëa‡c[Ô¾¬VÖ/”ç׸N/š‡:êÓÞG9勵èÖdÝë}ô !]—UÚÏý²º”—îÚôgò|9g›{[ËZùÒµêüBÝÍ»Æz+Î\ú¾Gjø€ @€ Mð{ô¡½ëºŒÓ÷>Ò¬×V5ÎÛviš¸¦e•÷ù€]ÃåëÜVç}?”ö«5åôBzX¤šVì³ÈêrýÌïiÍ¿²ÒôÉ'ÝÓgsÿLÚ09—ZÖYï³Êi©OËv-~ÖZg-> @€ @à ¨ßß×±ÎnÍÖÏã¸ËºWy_c4ïïO>åþšd,ßZúyÏ”ÏǶٓšt-i^éOÕúê[Únbø4å0“ƒ’V¬ýTcˆu½òZY³P?õ‹¡A‘–j<<’¯A“öMÍgð§–féù?¯ËX5®Ó>õåœzëœ4-ëµ?OžÍ×¾zrÕqæjJmÝK @€ @ؽ»êZǶÚ'}ÇÖdíg.uç­µlöN"©Öûåš[dܪ‘–«USçûâÌmÜßïáS> Ài@bÛÊ iÊ×/ˆöÔ ©á‘H®ñÐi–Ú¹¯j´Ô£^_>—÷P5×ÈjeìšÔí§M?÷‘®em=ÿg+ßҞ߉@€ @Ø~¯ž'ii]y×ÖVõµ¦¸KKÝuµ–ƒ¤Vó¾wWêºjΦvŒÏ°#Ìœ>­•ËÞ­ùû5|FÒo=¸ó -{³§O÷ ¯˜´§}éŠU'?÷rìšYzžw¯mÖ¹?­óê÷J͵ʥŸµ-ßšl«/óéO©Í>|@€ @€ÀTù¾~¨·«6õô½_jöeíg]êÎ[³U½Dž+HsÞvHóª÷>òµ¼‡íB§»ß=-ëûŒ©mõ¯¬­køÐµO—îƒgÞ~m³6sö•—ŸWÝã\«Ö¹´­º)ZW­õÚêÞõJ;m_m#† @€ f9<Éç°n«\ú®•f½¶îqó]zÖ¹vŒV×ôõvÝ;õÚW¬•ûf\ûŠëåÞ±z]×ï×'Ÿ:4Kèa5d±u­!8'ÝÃç\ë\­g¯kó>ú“jr_ÇCšïéúÜ׽ιVÖ+k¤9®óŽk[×OÍ×õÄ€ @€ M¨ß·×÷›šw}m½oêé+Ÿ±|ÇÎY³žqKËO;Õ{Ôqö×9ÅZ¾_íÏ“góÎY;v¿‡Oç¡Iú†Ó§)§•ý e¡¹×uuÎy÷ËêC6/õe­ë­)ïz×Öšó>‹÷ȺVNy¯ºÇzm³®ÎÕñ”Úº—€ @€ 0…€ß÷Žéé«Í\ú¹oêòëØµ™K_yÇîMÛÊ¥ÖW[ïµuN±–÷¶?ã‡÷©ó™³f+þº†Cûôå3×òSÇií;ïX6ýÌѳ¦öó{ž¼¯mÖZë³ÊyÕ½µÞ[ÇB€ @€ÎUõ¥+®uñ°6ÆvÕt}_“÷Ï>ùgM—ž5òµºöYd?ëýÜçç§íËÕûdßh¿?ùä‡ðÀŬX~Z?T­g÷“uoí+»|/Õ×~ý))Õø9ì;–µ¯œVÆömëü¼¡ú‘µUjÏÞuŽ€ @€ p ø}~ëŒ}9ÕgÞ¾mw켬}çl[9×f®Ö2žºWÖÛO+_«uEfñÓùÔ¶ê÷ 0–9Hß~}9Ý+ó-?µ¬·.k¿ÎOÍyŸìÒ|Ϭ“¦UkŽ37/<û#ó­š:Ÿ½­ú:O @€ @8¨††%u~(ÖsfýÚfru>ãôÝ—šýÌY³›s]ZùZÞË6µÚW\¯ì›’«k{ã¡!Fos#Ù·__Î[e}[Õ¤Ÿ±uYûuÞº­óŽeí¯’s¯­÷´•îeÍVzú­Ø½]¶îïªC‡ @€ T}C‘Ö™ëúŒíÛf¿5ÙôëšÌÙWMÝ7%×ê÷}ë}]ë|û¾¶u>ûÒÏúÔÇö×=ÍxÝƒŠ¡ý¦æ]o«‡H?cëµuM­;nåë\«Fš–j]_ÛyÁ@Þ=®µmé-­¯Þ¹.Û·_W: @€ @`}ƒŽ®ýúzZ¹–¦½S·_[Ÿ¡Ö[˽R¯ó޳¦îuìZÇ}V9­¾žEÅÞkisÔíå]7h×=|³ßÔš¬·oëtÜeU×ÊYË}¬ÉÚŸÒßUë{ضöÎÞVµelÞo™~z @€ @ë&°®G½OÆ]¾ŸÅù.›u}5ÊeÞþ”~Õº¯¶¹OÖõùî©­÷®õŒÇÔd}§¾p¼óp= ÈaŠã.«­ZÐ\ï½êçó(Öl•s¿ë¼uÛV­4×»?ë­ÉvéC9åë{HcA€ @€2¾÷²]¹.]Ïéœm—ÖÒÝc;¥Æ=µÕZµ^Ç‹ªÝ:LJÂö 3–}€1{.Sã[/}Ÿ×Z—mÕÕµËÖ¨Ï{Õ~½gϾ:çÞ±¶Þklu€ @€ ý&àÁË2ç¨{3îò}ŸVÞšm]+Ý9ÛekÔç=l½WæÒ¯ëê8ûí¯«Æû ÚM )Æì9¦F‡¯ë2¶o›k­¶]{ºÎùŒ­õYå¼Ük+½ËwO]³Šž½C~žk¨–< @€ @`c†Cûwí±Šž½ömó<Öº¬k•w´:¶&«åÚÚ.²‹Ÿ­œ5×Õ±õÚŽ©SSïÛobø0vÏeë²Ï¾m>hjökëzëŠí×¶®u¾ÕãÚÌõùu.ûûüOÛäðbÌÞcjtèº.ã.ßë¼mî—ZW½kl]—û¤ŸuéwõYŸR;Ôã<€ @€ p®èˆtåZzK£ÔíÛÖy3u¾Ëº.û]Û¥¹§U—=­:k-›ûµòÒÆÔtõvê]Ά ‰±{/[—}]~×5¶ÊÙ·mÕwÕíÉ~ïßêuÎv]5Þ @€ @8ÌÆ F–©Éž.ßÜZù–æzYçm»´VOÖÖ~+Î=ÒÏ{§^ûcëê¾ÞxÌ€£wƒžäؽÇÖéVumÆé·j}T×ÙÖµ©×=Y›ué»'k­ÕuuÜUg @€ @€@?®J­žKÖÙ·UMú}=YÛêÉüÐ>­Z÷´l×ýêÚ±uu_oÜ5üèmšœ²ÿØÚV]j]~»«¦K_W¯÷ÉûX“íÒ]3”w]Ë®ÒÛÚ € @€ °*U†C½]ù.ÝÏ’ù1¾ûdÇÔgÍÔ^×wíá¼íØ:ÕO©õþ£ì¦SöŸR«‡«ëûâ:—p27Æïêz¦Ü§ËÏótÕÔú2=õÄ€ @€ ý$°Ì djO]?56Ÿìã»Ï¶«Gù̵bïÑeëþ®ºeöîÛkOn[ƒŠ±÷[ç‡¨ë§ÆÞG6{ÓÊuía½o/×Ô÷H€ @€ åt _j½Žu·–æSd.ýº¯Î¹¿®[&νZ~ß½³~l]öLòëÁȤæ ÅSï³j}Ý?ç£ Õås/ûuô–æú1ù¬Mh߬Ň @€ DËD†úZù–V3©k¦Æ¹ßÔÞº>÷jù›®oݳWÛæ bê½ÖQ_ï1·` õ å[{¦V÷g.ý±uÙƒ@€ @8— Œ´Œ­3«º~jì}Ò.³GÝ“ûµüM×·î9¨m{ 1õ~SëõÀ­žZ«ã®¾`ÝWÇ}û´jëý§ÄëÞoʽ©… @€ ì©Ã•¡3ví×Òk­Ž[÷jÕÔZkŸ–ÖÚ?µ©=Sëó^“üó&U¯§xêÐdj½NÙÕÓÒÇj«>ý˜µu–UïK? @€ @à…H`è}x_¾•ki-®­º±šökÕ¶î“ÚÔž©õy¯Éþ~ ;¦Þwj½AtõMÑ»j}´}µ}¹±{d> @€ @mc‡+}u}¹ú®­Ú–¦¾©z}¯:îÚ¯®s<µÞ}KÛýøä“;vãzÙezÜßÕ».Ý÷iÙ®{´jÑ @€ @XÀ”Ë”ZŸª«g]ºïÓg»îµîž¾ýFåösø¤.3”Y¦'aôõwåºôeŸ!σ@€ @€Àz ,3˜é;Aß~]¹.]÷éËõùeú—éñýV²}C••6žÐ¼ì–íË£õíÑ—Ó«æóø€ @€ ì?¡Í*ù¡Þ1O¿ìËö9Ó`ÍÐepƒ5¬rŽUzóøCû åµ×˜ßsJ­{°€ @€ ,O`ÊfLíPÍP~쓬²Ï*½cÏ×[wÐ «œg•ÞÒØ½ÆÖiÿ)µõy¦Äۺϔ3Q @€ @˜B`[“)÷[;¶n UöZ¥wÌÙF×ÔAÅ*çZ¥·ÜÔ}§ÖwÝ{]ûtí@€ @8ìÖ5h™ºÏÔú±œWÙw•Þ±ç›Tw«žmÕþ1 W¹Ç*½cÎF  @€ @{ ¬2˜Y¥wï)º£Uï±j÷ÉVȆȺθ®}ÆâÞöý†ÎuÐÎ3t^ò€ @€ pî8hÃ’mŸg]÷[×>ù;L‰uu]û¬ó9ˆÞß®IDATgZçó± @€ @`¿ÄÁ̺δ®}6úÚ¶¡Ç¦Î»©}7úâ±9 @€ @žÀ¦D›Úwí@ëÐeÓçÞôþk!Ù€ @€ A`ÓC¡Mï¿vˆçÂe[ϰ­û¬ýEfC@€ @€6B`[ƒ mÝg#εÊ~=Ï~Ýw#¿l @€ @€Àýüì×}w|]ι<49ˆÏvÏ´®ß%ö @€ &q¸sÏ´òkúB†¼Ðžwå_6€ @€ @`#ÎÉAS‹ØR`ÐúÍ@ƒ @€ @`]^0ƒ¦0/-*{5íåA@€ @€À^/èáÒ^Ϭ<ŸÉT†S‰Q@€ @8\.­ðz18YÞ­ð^-€ @€ ` ´¨l @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @ üî«0 BŒIEND®B`‚flask-3.1.1/docs/_static/shortcut-icon.png000066400000000000000000000076731501065614000205140ustar00rootroot00000000000000‰PNG  IHDRÀÀeœ5gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<÷PLTE8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾8¨¾ÿÿÿeªRûtRNS $tÂÞÊ_;’ßýá&V¯îô>%uËùõ@ <”àþW±ïGúIûPüakXwÌ‚=•Y²ðª·Å ÑÜç'xÍ4 ?—öBZ³Rdа[ )zΘâµñë-({eo÷Ô‘ó¦ÐºÝE, 6SŽj|›¤~Ãh„éÏåš!+]؃øÇmÄò´èŸCUÛ‰1s"¹Œž¥æ*yl¾b׫M™ã:Q®g8}Ó¸9“iL©í#….½¼ÈnD£ê¡T2¿q^0ˆÆÕäÙfÁÒ‡K¨`œ3 A¬J–O\À»cNrÖ7pÚ/ì†F¢§É5€ ÇÐbKGDü<£tIMEæ 'TÎ=G WIDATxÚí\k\TeîÌâ Œ  ‚bŽâpó¶b h\"3@PAÑ‘@P. ©¥$H ¸²hà-ñ–«äKÍDÁPÍÊÊÖK®Z®–ZëºçËžÁ!óœsæœ÷öwžO|~óï´Àøxz¡½Ó‚þ`Ä @aë=‰ê#VíQ£[ŸYhŒ1R´wþ2Ö§3ö4Æ¥6ÞÑáEã‹§YzG‡ñ¾F&€µwtxÉϨpðŽþÆ#€›wt˜h,‚8yG‡‰“ŒC(&½ÌÉ;:¸„€Ð°WzqôŽá¯@{gòkܽ£ƒÛÂx{§¯“`ˆwZàM.–0Ì;-ˆ %À`ïè@&šÂ;:DFá@{gªáÞÑÁ"·PÄL›î*}:²Ã*€öÎÀ±‚xG›8Œ´Þ™9K@ö4âgcò9BzG|vÐÌfßi |ÅE?k7áéÓHÀ%àDQøSÓpdIâð§æá,’€9&óE°À “wq›a ‰G@*®º¤ùˆ" >—€Œ…¢pÃv/š(†u&6ò,QÁ›ø¢¹˜T1àkÒ,CÀ|Y1˜ú‹ `)¾À›jÙ«“Þ²ÅX—»·¥Ÿøö²åX§Î¼Â…£ïÓgJ6¾Ók…cŽìU¹+û:(jATÞ»Ùï¼Ô«òg+ŠÎ^aýÂÕ5%9p š5†Ð/*ö ÛÞ0 ¦(Y[J¼;ãY P–­[O ˜ËЭú®#Þ/G$À 0Á½ Àw(wúïy"¼Që7lÄ]¡xŸ+ýøMÃü˜ôBK×–P›·`/±/²çÂ^e3fQâLJ€!ÅÚZ±z2öYÃ!&r›ÿ-Êú2Ó­« Ÿ~d4v0’-ýðÊmÛQÖ/HÏßñçn‰«´òìûû²kØïÜ¥AYߪ­JÝö‹° È~‰…õwþ»/Š~öž½íF=ð{¨ª3úꤪ- ”õ—dÁ(t§`Ÿ<ƒ}ïCtª²_†:´bDIJ;–c]Ý}ŸÈL3”w|}Œî†cÃøDo¿ió’0g}Íá#GõýϧØ<86ÉD¹ê³ã¨C«ÆýÄýÏì$þ~·ìy§Úº8”õåý¼s:lpNÄUâmE*̧=‹¢™¨T #$«{' N¥;~9· IKžŠFÑ÷{áózªSœÆÕijÅìL«í\9!¢! e}ϩլºkø7Rú4þ¢åÛ]ƒÍ!ãµ¼³Ž,gŠãSŸ’sXЄŒ×Î}Y¦dGŸFþLÌNÐ_Ÿ2Ì™ª¼º‘SCdþ!d€L‡ÁçéTA¿©÷kÇZÒ”éüzÏAZ?ÐãB!7úd<„V¥H¹ƒOíñ"þ}Á¬FÍøŠ×TŽ2ÿa̤Ÿ=|¯ ö”êÒ´8Ò÷ÓAöõ7ßòk¨Ê”§¯˜³ø2¿Š£*ud?ÂôéTeŸ5Ï4Õàïl‰Ó7ùþ‡£¼ØSÔîˆìa\;ú²+?žÀ“~î?Ò ÓצfkøNn¶>g÷àÀo,?ú%¯4}í Hã³õǯ a¤Îåé$—9Ó?zu_T{ª¦Þ—*›HLã_ûŠ}ûÓƒô—ŸMRR®øscí>ôú.)aLóÔÔ]nŽ-·Xpý2kúEGnø2ém® `•$.©¼Î2óMþ)Í Aÿ#Ëg;qY ‰Gp³˜ýÈ:3él«msÜ"r«f`祟3w…2—®Yã…v%;—a$L¤ˆè„~xõm?&ý›w,˜æû9ˆ„‰Lƒ;¢_¸àŒAßë*uVVˆN†èŸ­.¬ôBÐ7ÉüEOæ¿â ù^ÕCßÉâÎM&ýèÛÕú³·ŸkH<[ä × MLú¡»Š;ŠÀUˆìDw˜œÜVßµcÒwÖ§“Yþ¢©$Ldµ²=}ÇežLúV§vv^¯žB T åÓ[sPçx1éûÞ8ªæXI Ìð¯g½WååºíLú}7±MÖš8ÎòuåtåÁ{û€¨A ÙçþnïdP`7³™þý—Mô½Ê©t1à7ü'À¹”êÁâ8&ý‚kÆs¡OÃeþŠ€×ü*f• ·J8Ò§ßH@AFœIÿáùÍÜéRÀ”$OßË‹>G ä”[èωˆåKŸÆ,s\ýÐôe¶¿Ûv 9qƒ±ÖȺ}šjðjõÎcd€,oq™ WÀK ,eÓ. uƒ½äžûL‰ù}öMúNáúÓCÌY&hR„£¯…Mæë6ð5Ï¢»Þ‡°à>ÁÈ¢ø=;€¸ aP>ãB8FwtæÝsÝØª+<”/XUòñ5v3€æxžBQ¾¬Â‡þ·[üÞ&(†ªäêÎ/°@Á¤ß¾}”úÝŒîO ÆÉ½ù-ZAµ|˜5þîõŽœÐtxclÛ0'ßúì#ÂËLÔ–dêÓ0¥–Yo²æY¥¨ù·Ð·G›áôàóoÖg£Îg?F=¶Û|÷`0›'À{¸PP>²W—fkÖ®Wúôöó=D tn‘8 h¨sƒ‡ÞZœyêPCú–-é ‡J÷¸¿ˆühDOt.ïÎ;a«Ã-±È¾~ÅŠzû¢D7=‹îÑp¾è³ð”÷À¹â Þh[ÆŒe)sB=Ú6Ø:ñKÜ7@€v!L.#Ë?y«Aè‡pH>%t" àYwQ„»”%Þ,©ÿ¯…I:[> LÁ ý›[œ}RÛüWap•HøAvì­VžÁu‡u©`®¥Ê]:ÅS¼B ÈòúçýâLu¯M^˵IhFŸ}WäV9À•ã|u©:öȶnXk_Ñaó. ²%Ñf~Q$zûòÀ;?ì6l5¨ìk³nâýíÛ<†¨Iw§òÌ›UÉ—z<É&>û^p}OÖšzŽ9:>øtݨ£^ÖîJ&ǧf´q`u>8ÕûïÍò°É:˜mD€s\X†È±>z6XU¢½cäùºïÓíH¬X–*¢²ËÝ•0wÌÂ>¿¬qô?¸jU’ÿô •Ågü~ïÎëã²–z{!²+¿›M5Û=›²MB3ä]ƒ· $H A‚ $HøÄÿ®ú¸„¨žˆ%tEXtdate:create2022-11-22T23:39:03+00:00Ën^ý%tEXtdate:modify2022-11-22T23:39:03+00:00º3æAWzTXtRaw profile type iptcxœãò qV((ÊOËÌIåR# .c #K“ D€4Ãd#³T ËØÔÈÄÌÄÄË€H J.êtòB5•IEND®B`‚flask-3.1.1/docs/api.rst000066400000000000000000000523101501065614000150460ustar00rootroot00000000000000API === .. module:: flask This part of the documentation covers all the interfaces of Flask. For parts where Flask depends on external libraries, we document the most important right here and provide links to the canonical documentation. Application Object ------------------ .. autoclass:: Flask :members: :inherited-members: Blueprint Objects ----------------- .. autoclass:: Blueprint :members: :inherited-members: Incoming Request Data --------------------- .. autoclass:: Request :members: :inherited-members: :exclude-members: json_module .. attribute:: request To access incoming request data, you can use the global `request` object. Flask parses incoming request data for you and gives you access to it through that global object. Internally Flask makes sure that you always get the correct data for the active thread if you are in a multithreaded environment. This is a proxy. See :ref:`notes-on-proxies` for more information. The request object is an instance of a :class:`~flask.Request`. Response Objects ---------------- .. autoclass:: flask.Response :members: :inherited-members: :exclude-members: json_module Sessions -------- If you have set :attr:`Flask.secret_key` (or configured it from :data:`SECRET_KEY`) you can use sessions in Flask applications. A session makes it possible to remember information from one request to another. The way Flask does this is by using a signed cookie. The user can look at the session contents, but can't modify it unless they know the secret key, so make sure to set that to something complex and unguessable. To access the current session you can use the :class:`session` object: .. class:: session The session object works pretty much like an ordinary dict, with the difference that it keeps track of modifications. This is a proxy. See :ref:`notes-on-proxies` for more information. The following attributes are interesting: .. attribute:: new ``True`` if the session is new, ``False`` otherwise. .. attribute:: modified ``True`` if the session object detected a modification. Be advised that modifications on mutable structures are not picked up automatically, in that situation you have to explicitly set the attribute to ``True`` yourself. Here an example:: # this change is not picked up because a mutable object (here # a list) is changed. session['objects'].append(42) # so mark it as modified yourself session.modified = True .. attribute:: permanent If set to ``True`` the session lives for :attr:`~flask.Flask.permanent_session_lifetime` seconds. The default is 31 days. If set to ``False`` (which is the default) the session will be deleted when the user closes the browser. Session Interface ----------------- .. versionadded:: 0.8 The session interface provides a simple way to replace the session implementation that Flask is using. .. currentmodule:: flask.sessions .. autoclass:: SessionInterface :members: .. autoclass:: SecureCookieSessionInterface :members: .. autoclass:: SecureCookieSession :members: .. autoclass:: NullSession :members: .. autoclass:: SessionMixin :members: .. admonition:: Notice The :data:`PERMANENT_SESSION_LIFETIME` config can be an integer or ``timedelta``. The :attr:`~flask.Flask.permanent_session_lifetime` attribute is always a ``timedelta``. Test Client ----------- .. currentmodule:: flask.testing .. autoclass:: FlaskClient :members: Test CLI Runner --------------- .. currentmodule:: flask.testing .. autoclass:: FlaskCliRunner :members: Application Globals ------------------- .. currentmodule:: flask To share data that is valid for one request only from one function to another, a global variable is not good enough because it would break in threaded environments. Flask provides you with a special object that ensures it is only valid for the active request and that will return different values for each request. In a nutshell: it does the right thing, like it does for :class:`request` and :class:`session`. .. data:: g A namespace object that can store data during an :doc:`application context `. This is an instance of :attr:`Flask.app_ctx_globals_class`, which defaults to :class:`ctx._AppCtxGlobals`. This is a good place to store resources during a request. For example, a ``before_request`` function could load a user object from a session id, then set ``g.user`` to be used in the view function. This is a proxy. See :ref:`notes-on-proxies` for more information. .. versionchanged:: 0.10 Bound to the application context instead of the request context. .. autoclass:: flask.ctx._AppCtxGlobals :members: Useful Functions and Classes ---------------------------- .. data:: current_app A proxy to the application handling the current request. This is useful to access the application without needing to import it, or if it can't be imported, such as when using the application factory pattern or in blueprints and extensions. This is only available when an :doc:`application context ` is pushed. This happens automatically during requests and CLI commands. It can be controlled manually with :meth:`~flask.Flask.app_context`. This is a proxy. See :ref:`notes-on-proxies` for more information. .. autofunction:: has_request_context .. autofunction:: copy_current_request_context .. autofunction:: has_app_context .. autofunction:: url_for .. autofunction:: abort .. autofunction:: redirect .. autofunction:: make_response .. autofunction:: after_this_request .. autofunction:: send_file .. autofunction:: send_from_directory Message Flashing ---------------- .. autofunction:: flash .. autofunction:: get_flashed_messages JSON Support ------------ .. module:: flask.json Flask uses Python's built-in :mod:`json` module for handling JSON by default. The JSON implementation can be changed by assigning a different provider to :attr:`flask.Flask.json_provider_class` or :attr:`flask.Flask.json`. The functions provided by ``flask.json`` will use methods on ``app.json`` if an app context is active. Jinja's ``|tojson`` filter is configured to use the app's JSON provider. The filter marks the output with ``|safe``. Use it to render data inside HTML `` .. autofunction:: jsonify .. autofunction:: dumps .. autofunction:: dump .. autofunction:: loads .. autofunction:: load .. autoclass:: flask.json.provider.JSONProvider :members: :member-order: bysource .. autoclass:: flask.json.provider.DefaultJSONProvider :members: :member-order: bysource .. automodule:: flask.json.tag Template Rendering ------------------ .. currentmodule:: flask .. autofunction:: render_template .. autofunction:: render_template_string .. autofunction:: stream_template .. autofunction:: stream_template_string .. autofunction:: get_template_attribute Configuration ------------- .. autoclass:: Config :members: Stream Helpers -------------- .. autofunction:: stream_with_context Useful Internals ---------------- .. autoclass:: flask.ctx.RequestContext :members: .. data:: flask.globals.request_ctx The current :class:`~flask.ctx.RequestContext`. If a request context is not active, accessing attributes on this proxy will raise a ``RuntimeError``. This is an internal object that is essential to how Flask handles requests. Accessing this should not be needed in most cases. Most likely you want :data:`request` and :data:`session` instead. .. autoclass:: flask.ctx.AppContext :members: .. data:: flask.globals.app_ctx The current :class:`~flask.ctx.AppContext`. If an app context is not active, accessing attributes on this proxy will raise a ``RuntimeError``. This is an internal object that is essential to how Flask handles requests. Accessing this should not be needed in most cases. Most likely you want :data:`current_app` and :data:`g` instead. .. autoclass:: flask.blueprints.BlueprintSetupState :members: .. _core-signals-list: Signals ------- Signals are provided by the `Blinker`_ library. See :doc:`signals` for an introduction. .. _blinker: https://blinker.readthedocs.io/ .. data:: template_rendered This signal is sent when a template was successfully rendered. The signal is invoked with the instance of the template as `template` and the context as dictionary (named `context`). Example subscriber:: def log_template_renders(sender, template, context, **extra): sender.logger.debug('Rendering template "%s" with context %s', template.name or 'string template', context) from flask import template_rendered template_rendered.connect(log_template_renders, app) .. data:: flask.before_render_template :noindex: This signal is sent before template rendering process. The signal is invoked with the instance of the template as `template` and the context as dictionary (named `context`). Example subscriber:: def log_template_renders(sender, template, context, **extra): sender.logger.debug('Rendering template "%s" with context %s', template.name or 'string template', context) from flask import before_render_template before_render_template.connect(log_template_renders, app) .. data:: request_started This signal is sent when the request context is set up, before any request processing happens. Because the request context is already bound, the subscriber can access the request with the standard global proxies such as :class:`~flask.request`. Example subscriber:: def log_request(sender, **extra): sender.logger.debug('Request context is set up') from flask import request_started request_started.connect(log_request, app) .. data:: request_finished This signal is sent right before the response is sent to the client. It is passed the response to be sent named `response`. Example subscriber:: def log_response(sender, response, **extra): sender.logger.debug('Request context is about to close down. ' 'Response: %s', response) from flask import request_finished request_finished.connect(log_response, app) .. data:: got_request_exception This signal is sent when an unhandled exception happens during request processing, including when debugging. The exception is passed to the subscriber as ``exception``. This signal is not sent for :exc:`~werkzeug.exceptions.HTTPException`, or other exceptions that have error handlers registered, unless the exception was raised from an error handler. This example shows how to do some extra logging if a theoretical ``SecurityException`` was raised: .. code-block:: python from flask import got_request_exception def log_security_exception(sender, exception, **extra): if not isinstance(exception, SecurityException): return security_logger.exception( f"SecurityException at {request.url!r}", exc_info=exception, ) got_request_exception.connect(log_security_exception, app) .. data:: request_tearing_down This signal is sent when the request is tearing down. This is always called, even if an exception is caused. Currently functions listening to this signal are called after the regular teardown handlers, but this is not something you can rely on. Example subscriber:: def close_db_connection(sender, **extra): session.close() from flask import request_tearing_down request_tearing_down.connect(close_db_connection, app) As of Flask 0.9, this will also be passed an `exc` keyword argument that has a reference to the exception that caused the teardown if there was one. .. data:: appcontext_tearing_down This signal is sent when the app context is tearing down. This is always called, even if an exception is caused. Currently functions listening to this signal are called after the regular teardown handlers, but this is not something you can rely on. Example subscriber:: def close_db_connection(sender, **extra): session.close() from flask import appcontext_tearing_down appcontext_tearing_down.connect(close_db_connection, app) This will also be passed an `exc` keyword argument that has a reference to the exception that caused the teardown if there was one. .. data:: appcontext_pushed This signal is sent when an application context is pushed. The sender is the application. This is usually useful for unittests in order to temporarily hook in information. For instance it can be used to set a resource early onto the `g` object. Example usage:: from contextlib import contextmanager from flask import appcontext_pushed @contextmanager def user_set(app, user): def handler(sender, **kwargs): g.user = user with appcontext_pushed.connected_to(handler, app): yield And in the testcode:: def test_user_me(self): with user_set(app, 'john'): c = app.test_client() resp = c.get('/users/me') assert resp.data == 'username=john' .. versionadded:: 0.10 .. data:: appcontext_popped This signal is sent when an application context is popped. The sender is the application. This usually falls in line with the :data:`appcontext_tearing_down` signal. .. versionadded:: 0.10 .. data:: message_flashed This signal is sent when the application is flashing a message. The messages is sent as `message` keyword argument and the category as `category`. Example subscriber:: recorded = [] def record(sender, message, category, **extra): recorded.append((message, category)) from flask import message_flashed message_flashed.connect(record, app) .. versionadded:: 0.10 Class-Based Views ----------------- .. versionadded:: 0.7 .. currentmodule:: None .. autoclass:: flask.views.View :members: .. autoclass:: flask.views.MethodView :members: .. _url-route-registrations: URL Route Registrations ----------------------- Generally there are three ways to define rules for the routing system: 1. You can use the :meth:`flask.Flask.route` decorator. 2. You can use the :meth:`flask.Flask.add_url_rule` function. 3. You can directly access the underlying Werkzeug routing system which is exposed as :attr:`flask.Flask.url_map`. Variable parts in the route can be specified with angular brackets (``/user/``). By default a variable part in the URL accepts any string without a slash however a different converter can be specified as well by using ````. Variable parts are passed to the view function as keyword arguments. The following converters are available: =========== =============================================== `string` accepts any text without a slash (the default) `int` accepts integers `float` like `int` but for floating point values `path` like the default but also accepts slashes `any` matches one of the items provided `uuid` accepts UUID strings =========== =============================================== Custom converters can be defined using :attr:`flask.Flask.url_map`. Here are some examples:: @app.route('/') def index(): pass @app.route('/') def show_user(username): pass @app.route('/post/') def show_post(post_id): pass An important detail to keep in mind is how Flask deals with trailing slashes. The idea is to keep each URL unique so the following rules apply: 1. If a rule ends with a slash and is requested without a slash by the user, the user is automatically redirected to the same page with a trailing slash attached. 2. If a rule does not end with a trailing slash and the user requests the page with a trailing slash, a 404 not found is raised. This is consistent with how web servers deal with static files. This also makes it possible to use relative link targets safely. You can also define multiple rules for the same function. They have to be unique however. Defaults can also be specified. Here for example is a definition for a URL that accepts an optional page:: @app.route('/users/', defaults={'page': 1}) @app.route('/users/page/') def show_users(page): pass This specifies that ``/users/`` will be the URL for page one and ``/users/page/N`` will be the URL for page ``N``. If a URL contains a default value, it will be redirected to its simpler form with a 301 redirect. In the above example, ``/users/page/1`` will be redirected to ``/users/``. If your route handles ``GET`` and ``POST`` requests, make sure the default route only handles ``GET``, as redirects can't preserve form data. :: @app.route('/region/', defaults={'id': 1}) @app.route('/region/', methods=['GET', 'POST']) def region(id): pass Here are the parameters that :meth:`~flask.Flask.route` and :meth:`~flask.Flask.add_url_rule` accept. The only difference is that with the route parameter the view function is defined with the decorator instead of the `view_func` parameter. =============== ========================================================== `rule` the URL rule as string `endpoint` the endpoint for the registered URL rule. Flask itself assumes that the name of the view function is the name of the endpoint if not explicitly stated. `view_func` the function to call when serving a request to the provided endpoint. If this is not provided one can specify the function later by storing it in the :attr:`~flask.Flask.view_functions` dictionary with the endpoint as key. `defaults` A dictionary with defaults for this rule. See the example above for how defaults work. `subdomain` specifies the rule for the subdomain in case subdomain matching is in use. If not specified the default subdomain is assumed. `**options` the options to be forwarded to the underlying :class:`~werkzeug.routing.Rule` object. A change to Werkzeug is handling of method options. methods is a list of methods this rule should be limited to (``GET``, ``POST`` etc.). By default a rule just listens for ``GET`` (and implicitly ``HEAD``). Starting with Flask 0.6, ``OPTIONS`` is implicitly added and handled by the standard request handling. They have to be specified as keyword arguments. =============== ========================================================== View Function Options --------------------- For internal usage the view functions can have some attributes attached to customize behavior the view function would normally not have control over. The following attributes can be provided optionally to either override some defaults to :meth:`~flask.Flask.add_url_rule` or general behavior: - `__name__`: The name of a function is by default used as endpoint. If endpoint is provided explicitly this value is used. Additionally this will be prefixed with the name of the blueprint by default which cannot be customized from the function itself. - `methods`: If methods are not provided when the URL rule is added, Flask will look on the view function object itself if a `methods` attribute exists. If it does, it will pull the information for the methods from there. - `provide_automatic_options`: if this attribute is set Flask will either force enable or disable the automatic implementation of the HTTP ``OPTIONS`` response. This can be useful when working with decorators that want to customize the ``OPTIONS`` response on a per-view basis. - `required_methods`: if this attribute is set, Flask will always add these methods when registering a URL rule even if the methods were explicitly overridden in the ``route()`` call. Full example:: def index(): if request.method == 'OPTIONS': # custom options handling here ... return 'Hello World!' index.provide_automatic_options = False index.methods = ['GET', 'OPTIONS'] app.add_url_rule('/', index) .. versionadded:: 0.8 The `provide_automatic_options` functionality was added. Command Line Interface ---------------------- .. currentmodule:: flask.cli .. autoclass:: FlaskGroup :members: .. autoclass:: AppGroup :members: .. autoclass:: ScriptInfo :members: .. autofunction:: load_dotenv .. autofunction:: with_appcontext .. autofunction:: pass_script_info Marks a function so that an instance of :class:`ScriptInfo` is passed as first argument to the click callback. .. autodata:: run_command .. autodata:: shell_command flask-3.1.1/docs/appcontext.rst000066400000000000000000000120071501065614000164610ustar00rootroot00000000000000.. currentmodule:: flask The Application Context ======================= The application context keeps track of the application-level data during a request, CLI command, or other activity. Rather than passing the application around to each function, the :data:`current_app` and :data:`g` proxies are accessed instead. This is similar to :doc:`/reqcontext`, which keeps track of request-level data during a request. A corresponding application context is pushed when a request context is pushed. Purpose of the Context ---------------------- The :class:`Flask` application object has attributes, such as :attr:`~Flask.config`, that are useful to access within views and :doc:`CLI commands `. However, importing the ``app`` instance within the modules in your project is prone to circular import issues. When using the :doc:`app factory pattern ` or writing reusable :doc:`blueprints ` or :doc:`extensions ` there won't be an ``app`` instance to import at all. Flask solves this issue with the *application context*. Rather than referring to an ``app`` directly, you use the :data:`current_app` proxy, which points to the application handling the current activity. Flask automatically *pushes* an application context when handling a request. View functions, error handlers, and other functions that run during a request will have access to :data:`current_app`. Flask will also automatically push an app context when running CLI commands registered with :attr:`Flask.cli` using ``@app.cli.command()``. Lifetime of the Context ----------------------- The application context is created and destroyed as necessary. When a Flask application begins handling a request, it pushes an application context and a :doc:`request context `. When the request ends it pops the request context then the application context. Typically, an application context will have the same lifetime as a request. See :doc:`/reqcontext` for more information about how the contexts work and the full life cycle of a request. Manually Push a Context ----------------------- If you try to access :data:`current_app`, or anything that uses it, outside an application context, you'll get this error message: .. code-block:: pytb RuntimeError: Working outside of application context. This typically means that you attempted to use functionality that needed to interface with the current application object in some way. To solve this, set up an application context with app.app_context(). If you see that error while configuring your application, such as when initializing an extension, you can push a context manually since you have direct access to the ``app``. Use :meth:`~Flask.app_context` in a ``with`` block, and everything that runs in the block will have access to :data:`current_app`. :: def create_app(): app = Flask(__name__) with app.app_context(): init_db() return app If you see that error somewhere else in your code not related to configuring the application, it most likely indicates that you should move that code into a view function or CLI command. Storing Data ------------ The application context is a good place to store common data during a request or CLI command. Flask provides the :data:`g object ` for this purpose. It is a simple namespace object that has the same lifetime as an application context. .. note:: The ``g`` name stands for "global", but that is referring to the data being global *within a context*. The data on ``g`` is lost after the context ends, and it is not an appropriate place to store data between requests. Use the :data:`session` or a database to store data across requests. A common use for :data:`g` is to manage resources during a request. 1. ``get_X()`` creates resource ``X`` if it does not exist, caching it as ``g.X``. 2. ``teardown_X()`` closes or otherwise deallocates the resource if it exists. It is registered as a :meth:`~Flask.teardown_appcontext` handler. For example, you can manage a database connection using this pattern:: from flask import g def get_db(): if 'db' not in g: g.db = connect_to_database() return g.db @app.teardown_appcontext def teardown_db(exception): db = g.pop('db', None) if db is not None: db.close() During a request, every call to ``get_db()`` will return the same connection, and it will be closed automatically at the end of the request. You can use :class:`~werkzeug.local.LocalProxy` to make a new context local from ``get_db()``:: from werkzeug.local import LocalProxy db = LocalProxy(get_db) Accessing ``db`` will call ``get_db`` internally, in the same way that :data:`current_app` works. Events and Signals ------------------ The application will call functions registered with :meth:`~Flask.teardown_appcontext` when the application context is popped. The following signals are sent: :data:`appcontext_pushed`, :data:`appcontext_tearing_down`, and :data:`appcontext_popped`. flask-3.1.1/docs/async-await.rst000066400000000000000000000114061501065614000165160ustar00rootroot00000000000000.. _async_await: Using ``async`` and ``await`` ============================= .. versionadded:: 2.0 Routes, error handlers, before request, after request, and teardown functions can all be coroutine functions if Flask is installed with the ``async`` extra (``pip install flask[async]``). This allows views to be defined with ``async def`` and use ``await``. .. code-block:: python @app.route("/get-data") async def get_data(): data = await async_db_query(...) return jsonify(data) Pluggable class-based views also support handlers that are implemented as coroutines. This applies to the :meth:`~flask.views.View.dispatch_request` method in views that inherit from the :class:`flask.views.View` class, as well as all the HTTP method handlers in views that inherit from the :class:`flask.views.MethodView` class. .. admonition:: Using ``async`` with greenlet When using gevent or eventlet to serve an application or patch the runtime, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is required. Performance ----------- Async functions require an event loop to run. Flask, as a WSGI application, uses one worker to handle one request/response cycle. When a request comes in to an async view, Flask will start an event loop in a thread, run the view function there, then return the result. Each request still ties up one worker, even for async views. The upside is that you can run async code within a view, for example to make multiple concurrent database queries, HTTP requests to an external API, etc. However, the number of requests your application can handle at one time will remain the same. **Async is not inherently faster than sync code.** Async is beneficial when performing concurrent IO-bound tasks, but will probably not improve CPU-bound tasks. Traditional Flask views will still be appropriate for most use cases, but Flask's async support enables writing and using code that wasn't possible natively before. Background tasks ---------------- Async functions will run in an event loop until they complete, at which stage the event loop will stop. This means any additional spawned tasks that haven't completed when the async function completes will be cancelled. Therefore you cannot spawn background tasks, for example via ``asyncio.create_task``. If you wish to use background tasks it is best to use a task queue to trigger background work, rather than spawn tasks in a view function. With that in mind you can spawn asyncio tasks by serving Flask with an ASGI server and utilising the asgiref WsgiToAsgi adapter as described in :doc:`deploying/asgi`. This works as the adapter creates an event loop that runs continually. When to use Quart instead ------------------------- Flask's async support is less performant than async-first frameworks due to the way it is implemented. If you have a mainly async codebase it would make sense to consider `Quart`_. Quart is a reimplementation of Flask based on the `ASGI`_ standard instead of WSGI. This allows it to handle many concurrent requests, long running requests, and websockets without requiring multiple worker processes or threads. It has also already been possible to run Flask with Gevent or Eventlet to get many of the benefits of async request handling. These libraries patch low-level Python functions to accomplish this, whereas ``async``/ ``await`` and ASGI use standard, modern Python capabilities. Deciding whether you should use Flask, Quart, or something else is ultimately up to understanding the specific needs of your project. .. _Quart: https://github.com/pallets/quart .. _ASGI: https://asgi.readthedocs.io/en/latest/ Extensions ---------- Flask extensions predating Flask's async support do not expect async views. If they provide decorators to add functionality to views, those will probably not work with async views because they will not await the function or be awaitable. Other functions they provide will not be awaitable either and will probably be blocking if called within an async view. Extension authors can support async functions by utilising the :meth:`flask.Flask.ensure_sync` method. For example, if the extension provides a view function decorator add ``ensure_sync`` before calling the decorated function, .. code-block:: python def extension(func): @wraps(func) def wrapper(*args, **kwargs): ... # Extension logic return current_app.ensure_sync(func)(*args, **kwargs) return wrapper Check the changelog of the extension you want to use to see if they've implemented async support, or make a feature request or PR to them. Other event loops ----------------- At the moment Flask only supports :mod:`asyncio`. It's possible to override :meth:`flask.Flask.ensure_sync` to change how async functions are wrapped to use a different library. flask-3.1.1/docs/blueprints.rst000066400000000000000000000304171501065614000164700ustar00rootroot00000000000000Modular Applications with Blueprints ==================================== .. currentmodule:: flask .. versionadded:: 0.7 Flask uses a concept of *blueprints* for making application components and supporting common patterns within an application or across applications. Blueprints can greatly simplify how large applications work and provide a central means for Flask extensions to register operations on applications. A :class:`Blueprint` object works similarly to a :class:`Flask` application object, but it is not actually an application. Rather it is a *blueprint* of how to construct or extend an application. Why Blueprints? --------------- Blueprints in Flask are intended for these cases: * Factor an application into a set of blueprints. This is ideal for larger applications; a project could instantiate an application object, initialize several extensions, and register a collection of blueprints. * Register a blueprint on an application at a URL prefix and/or subdomain. Parameters in the URL prefix/subdomain become common view arguments (with defaults) across all view functions in the blueprint. * Register a blueprint multiple times on an application with different URL rules. * Provide template filters, static files, templates, and other utilities through blueprints. A blueprint does not have to implement applications or view functions. * Register a blueprint on an application for any of these cases when initializing a Flask extension. A blueprint in Flask is not a pluggable app because it is not actually an application -- it's a set of operations which can be registered on an application, even multiple times. Why not have multiple application objects? You can do that (see :doc:`/patterns/appdispatch`), but your applications will have separate configs and will be managed at the WSGI layer. Blueprints instead provide separation at the Flask level, share application config, and can change an application object as necessary with being registered. The downside is that you cannot unregister a blueprint once an application was created without having to destroy the whole application object. The Concept of Blueprints ------------------------- The basic concept of blueprints is that they record operations to execute when registered on an application. Flask associates view functions with blueprints when dispatching requests and generating URLs from one endpoint to another. My First Blueprint ------------------ This is what a very basic blueprint looks like. In this case we want to implement a blueprint that does simple rendering of static templates:: from flask import Blueprint, render_template, abort from jinja2 import TemplateNotFound simple_page = Blueprint('simple_page', __name__, template_folder='templates') @simple_page.route('/', defaults={'page': 'index'}) @simple_page.route('/') def show(page): try: return render_template(f'pages/{page}.html') except TemplateNotFound: abort(404) When you bind a function with the help of the ``@simple_page.route`` decorator, the blueprint will record the intention of registering the function ``show`` on the application when it's later registered. Additionally it will prefix the endpoint of the function with the name of the blueprint which was given to the :class:`Blueprint` constructor (in this case also ``simple_page``). The blueprint's name does not modify the URL, only the endpoint. Registering Blueprints ---------------------- So how do you register that blueprint? Like this:: from flask import Flask from yourapplication.simple_page import simple_page app = Flask(__name__) app.register_blueprint(simple_page) If you check the rules registered on the application, you will find these:: >>> app.url_map Map([' (HEAD, OPTIONS, GET) -> static>, ' (HEAD, OPTIONS, GET) -> simple_page.show>, simple_page.show>]) The first one is obviously from the application itself for the static files. The other two are for the `show` function of the ``simple_page`` blueprint. As you can see, they are also prefixed with the name of the blueprint and separated by a dot (``.``). Blueprints however can also be mounted at different locations:: app.register_blueprint(simple_page, url_prefix='/pages') And sure enough, these are the generated rules:: >>> app.url_map Map([' (HEAD, OPTIONS, GET) -> static>, ' (HEAD, OPTIONS, GET) -> simple_page.show>, simple_page.show>]) On top of that you can register blueprints multiple times though not every blueprint might respond properly to that. In fact it depends on how the blueprint is implemented if it can be mounted more than once. Nesting Blueprints ------------------ It is possible to register a blueprint on another blueprint. .. code-block:: python parent = Blueprint('parent', __name__, url_prefix='/parent') child = Blueprint('child', __name__, url_prefix='/child') parent.register_blueprint(child) app.register_blueprint(parent) The child blueprint will gain the parent's name as a prefix to its name, and child URLs will be prefixed with the parent's URL prefix. .. code-block:: python url_for('parent.child.create') /parent/child/create In addition a child blueprint's will gain their parent's subdomain, with their subdomain as prefix if present i.e. .. code-block:: python parent = Blueprint('parent', __name__, subdomain='parent') child = Blueprint('child', __name__, subdomain='child') parent.register_blueprint(child) app.register_blueprint(parent) url_for('parent.child.create', _external=True) "child.parent.domain.tld" Blueprint-specific before request functions, etc. registered with the parent will trigger for the child. If a child does not have an error handler that can handle a given exception, the parent's will be tried. Blueprint Resources ------------------- Blueprints can provide resources as well. Sometimes you might want to introduce a blueprint only for the resources it provides. Blueprint Resource Folder ````````````````````````` Like for regular applications, blueprints are considered to be contained in a folder. While multiple blueprints can originate from the same folder, it does not have to be the case and it's usually not recommended. The folder is inferred from the second argument to :class:`Blueprint` which is usually `__name__`. This argument specifies what logical Python module or package corresponds to the blueprint. If it points to an actual Python package that package (which is a folder on the filesystem) is the resource folder. If it's a module, the package the module is contained in will be the resource folder. You can access the :attr:`Blueprint.root_path` property to see what the resource folder is:: >>> simple_page.root_path '/Users/username/TestProject/yourapplication' To quickly open sources from this folder you can use the :meth:`~Blueprint.open_resource` function:: with simple_page.open_resource('static/style.css') as f: code = f.read() Static Files ```````````` A blueprint can expose a folder with static files by providing the path to the folder on the filesystem with the ``static_folder`` argument. It is either an absolute path or relative to the blueprint's location:: admin = Blueprint('admin', __name__, static_folder='static') By default the rightmost part of the path is where it is exposed on the web. This can be changed with the ``static_url_path`` argument. Because the folder is called ``static`` here it will be available at the ``url_prefix`` of the blueprint + ``/static``. If the blueprint has the prefix ``/admin``, the static URL will be ``/admin/static``. The endpoint is named ``blueprint_name.static``. You can generate URLs to it with :func:`url_for` like you would with the static folder of the application:: url_for('admin.static', filename='style.css') However, if the blueprint does not have a ``url_prefix``, it is not possible to access the blueprint's static folder. This is because the URL would be ``/static`` in this case, and the application's ``/static`` route takes precedence. Unlike template folders, blueprint static folders are not searched if the file does not exist in the application static folder. Templates ````````` If you want the blueprint to expose templates you can do that by providing the `template_folder` parameter to the :class:`Blueprint` constructor:: admin = Blueprint('admin', __name__, template_folder='templates') For static files, the path can be absolute or relative to the blueprint resource folder. The template folder is added to the search path of templates but with a lower priority than the actual application's template folder. That way you can easily override templates that a blueprint provides in the actual application. This also means that if you don't want a blueprint template to be accidentally overridden, make sure that no other blueprint or actual application template has the same relative path. When multiple blueprints provide the same relative template path the first blueprint registered takes precedence over the others. So if you have a blueprint in the folder ``yourapplication/admin`` and you want to render the template ``'admin/index.html'`` and you have provided ``templates`` as a `template_folder` you will have to create a file like this: :file:`yourapplication/admin/templates/admin/index.html`. The reason for the extra ``admin`` folder is to avoid getting our template overridden by a template named ``index.html`` in the actual application template folder. To further reiterate this: if you have a blueprint named ``admin`` and you want to render a template called :file:`index.html` which is specific to this blueprint, the best idea is to lay out your templates like this:: yourpackage/ blueprints/ admin/ templates/ admin/ index.html __init__.py And then when you want to render the template, use :file:`admin/index.html` as the name to look up the template by. If you encounter problems loading the correct templates enable the ``EXPLAIN_TEMPLATE_LOADING`` config variable which will instruct Flask to print out the steps it goes through to locate templates on every ``render_template`` call. Building URLs ------------- If you want to link from one page to another you can use the :func:`url_for` function just like you normally would do just that you prefix the URL endpoint with the name of the blueprint and a dot (``.``):: url_for('admin.index') Additionally if you are in a view function of a blueprint or a rendered template and you want to link to another endpoint of the same blueprint, you can use relative redirects by prefixing the endpoint with a dot only:: url_for('.index') This will link to ``admin.index`` for instance in case the current request was dispatched to any other admin blueprint endpoint. Blueprint Error Handlers ------------------------ Blueprints support the ``errorhandler`` decorator just like the :class:`Flask` application object, so it is easy to make Blueprint-specific custom error pages. Here is an example for a "404 Page Not Found" exception:: @simple_page.errorhandler(404) def page_not_found(e): return render_template('pages/404.html') Most errorhandlers will simply work as expected; however, there is a caveat concerning handlers for 404 and 405 exceptions. These errorhandlers are only invoked from an appropriate ``raise`` statement or a call to ``abort`` in another of the blueprint's view functions; they are not invoked by, e.g., an invalid URL access. This is because the blueprint does not "own" a certain URL space, so the application instance has no way of knowing which blueprint error handler it should run if given an invalid URL. If you would like to execute different handling strategies for these errors based on URL prefixes, they may be defined at the application level using the ``request`` proxy object:: @app.errorhandler(404) @app.errorhandler(405) def _handle_api_error(ex): if request.path.startswith('/api/'): return jsonify(error=str(ex)), ex.code else: return ex See :doc:`/errorhandling`. flask-3.1.1/docs/changes.rst000066400000000000000000000000551501065614000157040ustar00rootroot00000000000000Changes ======= .. include:: ../CHANGES.rst flask-3.1.1/docs/cli.rst000066400000000000000000000404751501065614000150550ustar00rootroot00000000000000.. currentmodule:: flask Command Line Interface ====================== Installing Flask installs the ``flask`` script, a `Click`_ command line interface, in your virtualenv. Executed from the terminal, this script gives access to built-in, extension, and application-defined commands. The ``--help`` option will give more information about any commands and options. .. _Click: https://click.palletsprojects.com/ Application Discovery --------------------- The ``flask`` command is installed by Flask, not your application; it must be told where to find your application in order to use it. The ``--app`` option is used to specify how to load the application. While ``--app`` supports a variety of options for specifying your application, most use cases should be simple. Here are the typical values: (nothing) The name "app" or "wsgi" is imported (as a ".py" file, or package), automatically detecting an app (``app`` or ``application``) or factory (``create_app`` or ``make_app``). ``--app hello`` The given name is imported, automatically detecting an app (``app`` or ``application``) or factory (``create_app`` or ``make_app``). ---- ``--app`` has three parts: an optional path that sets the current working directory, a Python file or dotted import path, and an optional variable name of the instance or factory. If the name is a factory, it can optionally be followed by arguments in parentheses. The following values demonstrate these parts: ``--app src/hello`` Sets the current working directory to ``src`` then imports ``hello``. ``--app hello.web`` Imports the path ``hello.web``. ``--app hello:app2`` Uses the ``app2`` Flask instance in ``hello``. ``--app 'hello:create_app("dev")'`` The ``create_app`` factory in ``hello`` is called with the string ``'dev'`` as the argument. If ``--app`` is not set, the command will try to import "app" or "wsgi" (as a ".py" file, or package) and try to detect an application instance or factory. Within the given import, the command looks for an application instance named ``app`` or ``application``, then any application instance. If no instance is found, the command looks for a factory function named ``create_app`` or ``make_app`` that returns an instance. If parentheses follow the factory name, their contents are parsed as Python literals and passed as arguments and keyword arguments to the function. This means that strings must still be in quotes. Run the Development Server -------------------------- The :func:`run ` command will start the development server. It replaces the :meth:`Flask.run` method in most cases. :: $ flask --app hello run * Serving Flask app "hello" * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) .. warning:: Do not use this command to run your application in production. Only use the development server during development. The development server is provided for convenience, but is not designed to be particularly secure, stable, or efficient. See :doc:`/deploying/index` for how to run in production. If another program is already using port 5000, you'll see ``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the server tries to start. See :ref:`address-already-in-use` for how to handle that. Debug Mode ~~~~~~~~~~ In debug mode, the ``flask run`` command will enable the interactive debugger and the reloader by default, and make errors easier to see and debug. To enable debug mode, use the ``--debug`` option. .. code-block:: console $ flask --app hello run --debug * Serving Flask app "hello" * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with inotify reloader * Debugger is active! * Debugger PIN: 223-456-919 The ``--debug`` option can also be passed to the top level ``flask`` command to enable debug mode for any command. The following two ``run`` calls are equivalent. .. code-block:: console $ flask --app hello --debug run $ flask --app hello run --debug Watch and Ignore Files with the Reloader ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When using debug mode, the reloader will trigger whenever your Python code or imported modules change. The reloader can watch additional files with the ``--extra-files`` option. Multiple paths are separated with ``:``, or ``;`` on Windows. .. code-block:: text $ flask run --extra-files file1:dirA/file2:dirB/ * Running on http://127.0.0.1:8000/ * Detected change in '/path/to/file1', reloading The reloader can also ignore files using :mod:`fnmatch` patterns with the ``--exclude-patterns`` option. Multiple patterns are separated with ``:``, or ``;`` on Windows. Open a Shell ------------ To explore the data in your application, you can start an interactive Python shell with the :func:`shell ` command. An application context will be active, and the app instance will be imported. :: $ flask shell Python 3.10.0 (default, Oct 27 2021, 06:59:51) [GCC 11.1.0] on linux App: example [production] Instance: /home/david/Projects/pallets/flask/instance >>> Use :meth:`~Flask.shell_context_processor` to add other automatic imports. .. _dotenv: Environment Variables From dotenv --------------------------------- The ``flask`` command supports setting any option for any command with environment variables. The variables are named like ``FLASK_OPTION`` or ``FLASK_COMMAND_OPTION``, for example ``FLASK_APP`` or ``FLASK_RUN_PORT``. Rather than passing options every time you run a command, or environment variables every time you open a new terminal, you can use Flask's dotenv support to set environment variables automatically. If `python-dotenv`_ is installed, running the ``flask`` command will set environment variables defined in the files ``.env`` and ``.flaskenv``. You can also specify an extra file to load with the ``--env-file`` option. Dotenv files can be used to avoid having to set ``--app`` or ``FLASK_APP`` manually, and to set configuration using environment variables similar to how some deployment services work. Variables set on the command line are used over those set in :file:`.env`, which are used over those set in :file:`.flaskenv`. :file:`.flaskenv` should be used for public variables, such as ``FLASK_APP``, while :file:`.env` should not be committed to your repository so that it can set private variables. Directories are scanned upwards from the directory you call ``flask`` from to locate the files. The files are only loaded by the ``flask`` command or calling :meth:`~Flask.run`. If you would like to load these files when running in production, you should call :func:`~cli.load_dotenv` manually. .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme Setting Command Options ~~~~~~~~~~~~~~~~~~~~~~~ Click is configured to load default values for command options from environment variables. The variables use the pattern ``FLASK_COMMAND_OPTION``. For example, to set the port for the run command, instead of ``flask run --port 8000``: .. tabs:: .. group-tab:: Bash .. code-block:: text $ export FLASK_RUN_PORT=8000 $ flask run * Running on http://127.0.0.1:8000/ .. group-tab:: Fish .. code-block:: text $ set -x FLASK_RUN_PORT 8000 $ flask run * Running on http://127.0.0.1:8000/ .. group-tab:: CMD .. code-block:: text > set FLASK_RUN_PORT=8000 > flask run * Running on http://127.0.0.1:8000/ .. group-tab:: Powershell .. code-block:: text > $env:FLASK_RUN_PORT = 8000 > flask run * Running on http://127.0.0.1:8000/ These can be added to the ``.flaskenv`` file just like ``FLASK_APP`` to control default command options. Disable dotenv ~~~~~~~~~~~~~~ The ``flask`` command will show a message if it detects dotenv files but python-dotenv is not installed. .. code-block:: bash $ flask run * Tip: There are .env files present. Do "pip install python-dotenv" to use them. You can tell Flask not to load dotenv files even when python-dotenv is installed by setting the ``FLASK_SKIP_DOTENV`` environment variable. This can be useful if you want to load them manually, or if you're using a project runner that loads them already. Keep in mind that the environment variables must be set before the app loads or it won't configure as expected. .. tabs:: .. group-tab:: Bash .. code-block:: text $ export FLASK_SKIP_DOTENV=1 $ flask run .. group-tab:: Fish .. code-block:: text $ set -x FLASK_SKIP_DOTENV 1 $ flask run .. group-tab:: CMD .. code-block:: text > set FLASK_SKIP_DOTENV=1 > flask run .. group-tab:: Powershell .. code-block:: text > $env:FLASK_SKIP_DOTENV = 1 > flask run Environment Variables From virtualenv ------------------------------------- If you do not want to install dotenv support, you can still set environment variables by adding them to the end of the virtualenv's :file:`activate` script. Activating the virtualenv will set the variables. .. tabs:: .. group-tab:: Bash Unix Bash, :file:`.venv/bin/activate`:: $ export FLASK_APP=hello .. group-tab:: Fish Fish, :file:`.venv/bin/activate.fish`:: $ set -x FLASK_APP hello .. group-tab:: CMD Windows CMD, :file:`.venv\\Scripts\\activate.bat`:: > set FLASK_APP=hello .. group-tab:: Powershell Windows Powershell, :file:`.venv\\Scripts\\activate.ps1`:: > $env:FLASK_APP = "hello" It is preferred to use dotenv support over this, since :file:`.flaskenv` can be committed to the repository so that it works automatically wherever the project is checked out. Custom Commands --------------- The ``flask`` command is implemented using `Click`_. See that project's documentation for full information about writing commands. This example adds the command ``create-user`` that takes the argument ``name``. :: import click from flask import Flask app = Flask(__name__) @app.cli.command("create-user") @click.argument("name") def create_user(name): ... :: $ flask create-user admin This example adds the same command, but as ``user create``, a command in a group. This is useful if you want to organize multiple related commands. :: import click from flask import Flask from flask.cli import AppGroup app = Flask(__name__) user_cli = AppGroup('user') @user_cli.command('create') @click.argument('name') def create_user(name): ... app.cli.add_command(user_cli) :: $ flask user create demo See :ref:`testing-cli` for an overview of how to test your custom commands. Registering Commands with Blueprints ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If your application uses blueprints, you can optionally register CLI commands directly onto them. When your blueprint is registered onto your application, the associated commands will be available to the ``flask`` command. By default, those commands will be nested in a group matching the name of the blueprint. .. code-block:: python from flask import Blueprint bp = Blueprint('students', __name__) @bp.cli.command('create') @click.argument('name') def create(name): ... app.register_blueprint(bp) .. code-block:: text $ flask students create alice You can alter the group name by specifying the ``cli_group`` parameter when creating the :class:`Blueprint` object, or later with :meth:`app.register_blueprint(bp, cli_group='...') `. The following are equivalent: .. code-block:: python bp = Blueprint('students', __name__, cli_group='other') # or app.register_blueprint(bp, cli_group='other') .. code-block:: text $ flask other create alice Specifying ``cli_group=None`` will remove the nesting and merge the commands directly to the application's level: .. code-block:: python bp = Blueprint('students', __name__, cli_group=None) # or app.register_blueprint(bp, cli_group=None) .. code-block:: text $ flask create alice Application Context ~~~~~~~~~~~~~~~~~~~ Commands added using the Flask app's :attr:`~Flask.cli` or :class:`~flask.cli.FlaskGroup` :meth:`~cli.AppGroup.command` decorator will be executed with an application context pushed, so your custom commands and parameters have access to the app and its configuration. The :func:`~cli.with_appcontext` decorator can be used to get the same behavior, but is not needed in most cases. .. code-block:: python import click from flask.cli import with_appcontext @click.command() @with_appcontext def do_work(): ... app.cli.add_command(do_work) Plugins ------- Flask will automatically load commands specified in the ``flask.commands`` `entry point`_. This is useful for extensions that want to add commands when they are installed. Entry points are specified in :file:`pyproject.toml`: .. code-block:: toml [project.entry-points."flask.commands"] my-command = "my_extension.commands:cli" .. _entry point: https://packaging.python.org/tutorials/packaging-projects/#entry-points Inside :file:`my_extension/commands.py` you can then export a Click object:: import click @click.command() def cli(): ... Once that package is installed in the same virtualenv as your Flask project, you can run ``flask my-command`` to invoke the command. .. _custom-scripts: Custom Scripts -------------- When you are using the app factory pattern, it may be more convenient to define your own Click script. Instead of using ``--app`` and letting Flask load your application, you can create your own Click object and export it as a `console script`_ entry point. Create an instance of :class:`~cli.FlaskGroup` and pass it the factory:: import click from flask import Flask from flask.cli import FlaskGroup def create_app(): app = Flask('wiki') # other setup return app @click.group(cls=FlaskGroup, create_app=create_app) def cli(): """Management script for the Wiki application.""" Define the entry point in :file:`pyproject.toml`: .. code-block:: toml [project.scripts] wiki = "wiki:cli" Install the application in the virtualenv in editable mode and the custom script is available. Note that you don't need to set ``--app``. :: $ pip install -e . $ wiki run .. admonition:: Errors in Custom Scripts When using a custom script, if you introduce an error in your module-level code, the reloader will fail because it can no longer load the entry point. The ``flask`` command, being separate from your code, does not have this issue and is recommended in most cases. .. _console script: https://packaging.python.org/tutorials/packaging-projects/#console-scripts PyCharm Integration ------------------- PyCharm Professional provides a special Flask run configuration to run the development server. For the Community Edition, and for other commands besides ``run``, you need to create a custom run configuration. These instructions should be similar for any other IDE you use. In PyCharm, with your project open, click on *Run* from the menu bar and go to *Edit Configurations*. You'll see a screen similar to this: .. image:: _static/pycharm-run-config.png :align: center :class: screenshot :alt: Screenshot of PyCharm run configuration. Once you create a configuration for the ``flask run``, you can copy and change it to call any other command. Click the *+ (Add New Configuration)* button and select *Python*. Give the configuration a name such as "flask run". Click the *Script path* dropdown and change it to *Module name*, then input ``flask``. The *Parameters* field is set to the CLI command to execute along with any arguments. This example uses ``--app hello run --debug``, which will run the development server in debug mode. ``--app hello`` should be the import or file with your Flask app. If you installed your project as a package in your virtualenv, you may uncheck the *PYTHONPATH* options. This will more accurately match how you deploy later. Click *OK* to save and close the configuration. Select the configuration in the main PyCharm window and click the play button next to it to run the server. Now that you have a configuration for ``flask run``, you can copy that configuration and change the *Parameters* argument to run a different CLI command. flask-3.1.1/docs/conf.py000066400000000000000000000065011501065614000150430ustar00rootroot00000000000000import packaging.version from pallets_sphinx_themes import get_version from pallets_sphinx_themes import ProjectLink # Project -------------------------------------------------------------- project = "Flask" copyright = "2010 Pallets" author = "Pallets" release, version = get_version("Flask") # General -------------------------------------------------------------- default_role = "code" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinxcontrib.log_cabinet", "sphinx_tabs.tabs", "pallets_sphinx_themes", ] autodoc_member_order = "bysource" autodoc_typehints = "description" autodoc_preserve_defaults = True extlinks = { "issue": ("https://github.com/pallets/flask/issues/%s", "#%s"), "pr": ("https://github.com/pallets/flask/pull/%s", "#%s"), "ghsa": ("https://github.com/pallets/flask/security/advisories/GHSA-%s", "GHSA-%s"), } intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "werkzeug": ("https://werkzeug.palletsprojects.com/", None), "click": ("https://click.palletsprojects.com/", None), "jinja": ("https://jinja.palletsprojects.com/", None), "itsdangerous": ("https://itsdangerous.palletsprojects.com/", None), "sqlalchemy": ("https://docs.sqlalchemy.org/", None), "wtforms": ("https://wtforms.readthedocs.io/", None), "blinker": ("https://blinker.readthedocs.io/", None), } # HTML ----------------------------------------------------------------- html_theme = "flask" html_theme_options = {"index_sidebar_logo": False} html_context = { "project_links": [ ProjectLink("Donate", "https://palletsprojects.com/donate"), ProjectLink("PyPI Releases", "https://pypi.org/project/Flask/"), ProjectLink("Source Code", "https://github.com/pallets/flask/"), ProjectLink("Issue Tracker", "https://github.com/pallets/flask/issues/"), ProjectLink("Chat", "https://discord.gg/pallets"), ] } html_sidebars = { "index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"], "**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"], } singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]} html_static_path = ["_static"] html_favicon = "_static/shortcut-icon.png" html_logo = "_static/flask-vertical.png" html_title = f"Flask Documentation ({version})" html_show_sourcelink = False gettext_uuid = True gettext_compact = False # Local Extensions ----------------------------------------------------- def github_link(name, rawtext, text, lineno, inliner, options=None, content=None): app = inliner.document.settings.env.app release = app.config.release base_url = "https://github.com/pallets/flask/tree/" if text.endswith(">"): words, text = text[:-1].rsplit("<", 1) words = words.strip() else: words = None if packaging.version.parse(release).is_devrelease: url = f"{base_url}main/{text}" else: url = f"{base_url}{release}/{text}" if words is None: words = url from docutils.nodes import reference from docutils.parsers.rst.roles import set_classes options = options or {} set_classes(options) node = reference(rawtext, words, refuri=url, **options) return [node], [] def setup(app): app.add_role("gh", github_link) flask-3.1.1/docs/config.rst000066400000000000000000000705231501065614000155500ustar00rootroot00000000000000Configuration Handling ====================== Applications need some kind of configuration. There are different settings you might want to change depending on the application environment like toggling the debug mode, setting the secret key, and other such environment-specific things. The way Flask is designed usually requires the configuration to be available when the application starts up. You can hard code the configuration in the code, which for many small applications is not actually that bad, but there are better ways. Independent of how you load your config, there is a config object available which holds the loaded configuration values: The :attr:`~flask.Flask.config` attribute of the :class:`~flask.Flask` object. This is the place where Flask itself puts certain configuration values and also where extensions can put their configuration values. But this is also where you can have your own configuration. Configuration Basics -------------------- The :attr:`~flask.Flask.config` is actually a subclass of a dictionary and can be modified just like any dictionary:: app = Flask(__name__) app.config['TESTING'] = True Certain configuration values are also forwarded to the :attr:`~flask.Flask` object so you can read and write them from there:: app.testing = True To update multiple keys at once you can use the :meth:`dict.update` method:: app.config.update( TESTING=True, SECRET_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' ) Debug Mode ---------- The :data:`DEBUG` config value is special because it may behave inconsistently if changed after the app has begun setting up. In order to set debug mode reliably, use the ``--debug`` option on the ``flask`` or ``flask run`` command. ``flask run`` will use the interactive debugger and reloader by default in debug mode. .. code-block:: text $ flask --app hello run --debug Using the option is recommended. While it is possible to set :data:`DEBUG` in your config or code, this is strongly discouraged. It can't be read early by the ``flask run`` command, and some systems or extensions may have already configured themselves based on a previous value. Builtin Configuration Values ---------------------------- The following configuration values are used internally by Flask: .. py:data:: DEBUG Whether debug mode is enabled. When using ``flask run`` to start the development server, an interactive debugger will be shown for unhandled exceptions, and the server will be reloaded when code changes. The :attr:`~flask.Flask.debug` attribute maps to this config key. This is set with the ``FLASK_DEBUG`` environment variable. It may not behave as expected if set in code. **Do not enable debug mode when deploying in production.** Default: ``False`` .. py:data:: TESTING Enable testing mode. Exceptions are propagated rather than handled by the the app's error handlers. Extensions may also change their behavior to facilitate easier testing. You should enable this in your own tests. Default: ``False`` .. py:data:: PROPAGATE_EXCEPTIONS Exceptions are re-raised rather than being handled by the app's error handlers. If not set, this is implicitly true if ``TESTING`` or ``DEBUG`` is enabled. Default: ``None`` .. py:data:: TRAP_HTTP_EXCEPTIONS If there is no handler for an ``HTTPException``-type exception, re-raise it to be handled by the interactive debugger instead of returning it as a simple error response. Default: ``False`` .. py:data:: TRAP_BAD_REQUEST_ERRORS Trying to access a key that doesn't exist from request dicts like ``args`` and ``form`` will return a 400 Bad Request error page. Enable this to treat the error as an unhandled exception instead so that you get the interactive debugger. This is a more specific version of ``TRAP_HTTP_EXCEPTIONS``. If unset, it is enabled in debug mode. Default: ``None`` .. py:data:: SECRET_KEY A secret key that will be used for securely signing the session cookie and can be used for any other security related needs by extensions or your application. It should be a long random ``bytes`` or ``str``. For example, copy the output of this to your config:: $ python -c 'import secrets; print(secrets.token_hex())' '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' **Do not reveal the secret key when posting questions or committing code.** Default: ``None`` .. py:data:: SECRET_KEY_FALLBACKS A list of old secret keys that can still be used for unsigning. This allows a project to implement key rotation without invalidating active sessions or other recently-signed secrets. Keys should be removed after an appropriate period of time, as checking each additional key adds some overhead. Order should not matter, but the default implementation will test the last key in the list first, so it might make sense to order oldest to newest. Flask's built-in secure cookie session supports this. Extensions that use :data:`SECRET_KEY` may not support this yet. Default: ``None`` .. versionadded:: 3.1 .. py:data:: SESSION_COOKIE_NAME The name of the session cookie. Can be changed in case you already have a cookie with the same name. Default: ``'session'`` .. py:data:: SESSION_COOKIE_DOMAIN The value of the ``Domain`` parameter on the session cookie. If not set, browsers will only send the cookie to the exact domain it was set from. Otherwise, they will send it to any subdomain of the given value as well. Not setting this value is more restricted and secure than setting it. Default: ``None`` .. warning:: If this is changed after the browser created a cookie is created with one setting, it may result in another being created. Browsers may send send both in an undefined order. In that case, you may want to change :data:`SESSION_COOKIE_NAME` as well or otherwise invalidate old sessions. .. versionchanged:: 2.3 Not set by default, does not fall back to ``SERVER_NAME``. .. py:data:: SESSION_COOKIE_PATH The path that the session cookie will be valid for. If not set, the cookie will be valid underneath ``APPLICATION_ROOT`` or ``/`` if that is not set. Default: ``None`` .. py:data:: SESSION_COOKIE_HTTPONLY Browsers will not allow JavaScript access to cookies marked as "HTTP only" for security. Default: ``True`` .. py:data:: SESSION_COOKIE_SECURE Browsers will only send cookies with requests over HTTPS if the cookie is marked "secure". The application must be served over HTTPS for this to make sense. Default: ``False`` .. py:data:: SESSION_COOKIE_PARTITIONED Browsers will send cookies based on the top-level document's domain, rather than only the domain of the document setting the cookie. This prevents third party cookies set in iframes from "leaking" between separate sites. Browsers are beginning to disallow non-partitioned third party cookies, so you need to mark your cookies partitioned if you expect them to work in such embedded situations. Enabling this implicitly enables :data:`SESSION_COOKIE_SECURE` as well, as it is only valid when served over HTTPS. Default: ``False`` .. versionadded:: 3.1 .. py:data:: SESSION_COOKIE_SAMESITE Restrict how cookies are sent with requests from external sites. Can be set to ``'Lax'`` (recommended) or ``'Strict'``. See :ref:`security-cookie`. Default: ``None`` .. versionadded:: 1.0 .. py:data:: PERMANENT_SESSION_LIFETIME If ``session.permanent`` is true, the cookie's expiration will be set this number of seconds in the future. Can either be a :class:`datetime.timedelta` or an ``int``. Flask's default cookie implementation validates that the cryptographic signature is not older than this value. Default: ``timedelta(days=31)`` (``2678400`` seconds) .. py:data:: SESSION_REFRESH_EACH_REQUEST Control whether the cookie is sent with every response when ``session.permanent`` is true. Sending the cookie every time (the default) can more reliably keep the session from expiring, but uses more bandwidth. Non-permanent sessions are not affected. Default: ``True`` .. py:data:: USE_X_SENDFILE When serving files, set the ``X-Sendfile`` header instead of serving the data with Flask. Some web servers, such as Apache, recognize this and serve the data more efficiently. This only makes sense when using such a server. Default: ``False`` .. py:data:: SEND_FILE_MAX_AGE_DEFAULT When serving files, set the cache control max age to this number of seconds. Can be a :class:`datetime.timedelta` or an ``int``. Override this value on a per-file basis using :meth:`~flask.Flask.get_send_file_max_age` on the application or blueprint. If ``None``, ``send_file`` tells the browser to use conditional requests will be used instead of a timed cache, which is usually preferable. Default: ``None`` .. py:data:: TRUSTED_HOSTS Validate :attr:`.Request.host` and other attributes that use it against these trusted values. Raise a :exc:`~werkzeug.exceptions.SecurityError` if the host is invalid, which results in a 400 error. If it is ``None``, all hosts are valid. Each value is either an exact match, or can start with a dot ``.`` to match any subdomain. Validation is done during routing against this value. ``before_request`` and ``after_request`` callbacks will still be called. Default: ``None`` .. versionadded:: 3.1 .. py:data:: SERVER_NAME Inform the application what host and port it is bound to. Must be set if ``subdomain_matching`` is enabled, to be able to extract the subdomain from the request. Must be set for ``url_for`` to generate external URLs outside of a request context. Default: ``None`` .. versionchanged:: 3.1 Does not restrict requests to only this domain, for both ``subdomain_matching`` and ``host_matching``. .. versionchanged:: 1.0 Does not implicitly enable ``subdomain_matching``. .. versionchanged:: 2.3 Does not affect ``SESSION_COOKIE_DOMAIN``. .. py:data:: APPLICATION_ROOT Inform the application what path it is mounted under by the application / web server. This is used for generating URLs outside the context of a request (inside a request, the dispatcher is responsible for setting ``SCRIPT_NAME`` instead; see :doc:`/patterns/appdispatch` for examples of dispatch configuration). Will be used for the session cookie path if ``SESSION_COOKIE_PATH`` is not set. Default: ``'/'`` .. py:data:: PREFERRED_URL_SCHEME Use this scheme for generating external URLs when not in a request context. Default: ``'http'`` .. py:data:: MAX_CONTENT_LENGTH The maximum number of bytes that will be read during this request. If this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it is set to ``None``, no limit is enforced at the Flask application level. However, if it is ``None`` and the request has no ``Content-Length`` header and the WSGI server does not indicate that it terminates the stream, then no data is read to avoid an infinite stream. Each request defaults to this config. It can be set on a specific :attr:`.Request.max_content_length` to apply the limit to that specific view. This should be set appropriately based on an application's or view's specific needs. Default: ``None`` .. versionadded:: 0.6 .. py:data:: MAX_FORM_MEMORY_SIZE The maximum size in bytes any non-file form field may be in a ``multipart/form-data`` body. If this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it is set to ``None``, no limit is enforced at the Flask application level. Each request defaults to this config. It can be set on a specific :attr:`.Request.max_form_memory_parts` to apply the limit to that specific view. This should be set appropriately based on an application's or view's specific needs. Default: ``500_000`` .. versionadded:: 3.1 .. py:data:: MAX_FORM_PARTS The maximum number of fields that may be present in a ``multipart/form-data`` body. If this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it is set to ``None``, no limit is enforced at the Flask application level. Each request defaults to this config. It can be set on a specific :attr:`.Request.max_form_parts` to apply the limit to that specific view. This should be set appropriately based on an application's or view's specific needs. Default: ``1_000`` .. versionadded:: 3.1 .. py:data:: TEMPLATES_AUTO_RELOAD Reload templates when they are changed. If not set, it will be enabled in debug mode. Default: ``None`` .. py:data:: EXPLAIN_TEMPLATE_LOADING Log debugging information tracing how a template file was loaded. This can be useful to figure out why a template was not loaded or the wrong file appears to be loaded. Default: ``False`` .. py:data:: MAX_COOKIE_SIZE Warn if cookie headers are larger than this many bytes. Defaults to ``4093``. Larger cookies may be silently ignored by browsers. Set to ``0`` to disable the warning. .. py:data:: PROVIDE_AUTOMATIC_OPTIONS Set to ``False`` to disable the automatic addition of OPTIONS responses. This can be overridden per route by altering the ``provide_automatic_options`` attribute. .. versionadded:: 0.4 ``LOGGER_NAME`` .. versionadded:: 0.5 ``SERVER_NAME`` .. versionadded:: 0.6 ``MAX_CONTENT_LENGTH`` .. versionadded:: 0.7 ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` .. versionadded:: 0.8 ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``, ``APPLICATION_ROOT``, ``SESSION_COOKIE_DOMAIN``, ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``, ``SESSION_COOKIE_SECURE`` .. versionadded:: 0.9 ``PREFERRED_URL_SCHEME`` .. versionadded:: 0.10 ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR`` .. versionadded:: 0.11 ``SESSION_REFRESH_EACH_REQUEST``, ``TEMPLATES_AUTO_RELOAD``, ``LOGGER_HANDLER_POLICY``, ``EXPLAIN_TEMPLATE_LOADING`` .. versionchanged:: 1.0 ``LOGGER_NAME`` and ``LOGGER_HANDLER_POLICY`` were removed. See :doc:`/logging` for information about configuration. Added :data:`ENV` to reflect the :envvar:`FLASK_ENV` environment variable. Added :data:`SESSION_COOKIE_SAMESITE` to control the session cookie's ``SameSite`` option. Added :data:`MAX_COOKIE_SIZE` to control a warning from Werkzeug. .. versionchanged:: 2.2 Removed ``PRESERVE_CONTEXT_ON_EXCEPTION``. .. versionchanged:: 2.3 ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` were removed. The default ``app.json`` provider has equivalent attributes instead. .. versionchanged:: 2.3 ``ENV`` was removed. .. versionadded:: 3.10 Added :data:`PROVIDE_AUTOMATIC_OPTIONS` to control the default addition of autogenerated OPTIONS responses. Configuring from Python Files ----------------------------- Configuration becomes more useful if you can store it in a separate file, ideally located outside the actual application package. You can deploy your application, then separately configure it for the specific deployment. A common pattern is this:: app = Flask(__name__) app.config.from_object('yourapplication.default_settings') app.config.from_envvar('YOURAPPLICATION_SETTINGS') This first loads the configuration from the `yourapplication.default_settings` module and then overrides the values with the contents of the file the :envvar:`YOURAPPLICATION_SETTINGS` environment variable points to. This environment variable can be set in the shell before starting the server: .. tabs:: .. group-tab:: Bash .. code-block:: text $ export YOURAPPLICATION_SETTINGS=/path/to/settings.cfg $ flask run * Running on http://127.0.0.1:5000/ .. group-tab:: Fish .. code-block:: text $ set -x YOURAPPLICATION_SETTINGS /path/to/settings.cfg $ flask run * Running on http://127.0.0.1:5000/ .. group-tab:: CMD .. code-block:: text > set YOURAPPLICATION_SETTINGS=\path\to\settings.cfg > flask run * Running on http://127.0.0.1:5000/ .. group-tab:: Powershell .. code-block:: text > $env:YOURAPPLICATION_SETTINGS = "\path\to\settings.cfg" > flask run * Running on http://127.0.0.1:5000/ The configuration files themselves are actual Python files. Only values in uppercase are actually stored in the config object later on. So make sure to use uppercase letters for your config keys. Here is an example of a configuration file:: # Example configuration SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' Make sure to load the configuration very early on, so that extensions have the ability to access the configuration when starting up. There are other methods on the config object as well to load from individual files. For a complete reference, read the :class:`~flask.Config` object's documentation. Configuring from Data Files --------------------------- It is also possible to load configuration from a file in a format of your choice using :meth:`~flask.Config.from_file`. For example to load from a TOML file: .. code-block:: python import tomllib app.config.from_file("config.toml", load=tomllib.load, text=False) Or from a JSON file: .. code-block:: python import json app.config.from_file("config.json", load=json.load) Configuring from Environment Variables -------------------------------------- In addition to pointing to configuration files using environment variables, you may find it useful (or necessary) to control your configuration values directly from the environment. Flask can be instructed to load all environment variables starting with a specific prefix into the config using :meth:`~flask.Config.from_prefixed_env`. Environment variables can be set in the shell before starting the server: .. tabs:: .. group-tab:: Bash .. code-block:: text $ export FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f" $ export FLASK_MAIL_ENABLED=false $ flask run * Running on http://127.0.0.1:5000/ .. group-tab:: Fish .. code-block:: text $ set -x FLASK_SECRET_KEY "5f352379324c22463451387a0aec5d2f" $ set -x FLASK_MAIL_ENABLED false $ flask run * Running on http://127.0.0.1:5000/ .. group-tab:: CMD .. code-block:: text > set FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f" > set FLASK_MAIL_ENABLED=false > flask run * Running on http://127.0.0.1:5000/ .. group-tab:: Powershell .. code-block:: text > $env:FLASK_SECRET_KEY = "5f352379324c22463451387a0aec5d2f" > $env:FLASK_MAIL_ENABLED = "false" > flask run * Running on http://127.0.0.1:5000/ The variables can then be loaded and accessed via the config with a key equal to the environment variable name without the prefix i.e. .. code-block:: python app.config.from_prefixed_env() app.config["SECRET_KEY"] # Is "5f352379324c22463451387a0aec5d2f" The prefix is ``FLASK_`` by default. This is configurable via the ``prefix`` argument of :meth:`~flask.Config.from_prefixed_env`. Values will be parsed to attempt to convert them to a more specific type than strings. By default :func:`json.loads` is used, so any valid JSON value is possible, including lists and dicts. This is configurable via the ``loads`` argument of :meth:`~flask.Config.from_prefixed_env`. When adding a boolean value with the default JSON parsing, only "true" and "false", lowercase, are valid values. Keep in mind that any non-empty string is considered ``True`` by Python. It is possible to set keys in nested dictionaries by separating the keys with double underscore (``__``). Any intermediate keys that don't exist on the parent dict will be initialized to an empty dict. .. code-block:: text $ export FLASK_MYAPI__credentials__username=user123 .. code-block:: python app.config["MYAPI"]["credentials"]["username"] # Is "user123" On Windows, environment variable keys are always uppercase, therefore the above example would end up as ``MYAPI__CREDENTIALS__USERNAME``. For even more config loading features, including merging and case-insensitive Windows support, try a dedicated library such as Dynaconf_, which includes integration with Flask. .. _Dynaconf: https://www.dynaconf.com/ Configuration Best Practices ---------------------------- The downside with the approach mentioned earlier is that it makes testing a little harder. There is no single 100% solution for this problem in general, but there are a couple of things you can keep in mind to improve that experience: 1. Create your application in a function and register blueprints on it. That way you can create multiple instances of your application with different configurations attached which makes unit testing a lot easier. You can use this to pass in configuration as needed. 2. Do not write code that needs the configuration at import time. If you limit yourself to request-only accesses to the configuration you can reconfigure the object later on as needed. 3. Make sure to load the configuration very early on, so that extensions can access the configuration when calling ``init_app``. .. _config-dev-prod: Development / Production ------------------------ Most applications need more than one configuration. There should be at least separate configurations for the production server and the one used during development. The easiest way to handle this is to use a default configuration that is always loaded and part of the version control, and a separate configuration that overrides the values as necessary as mentioned in the example above:: app = Flask(__name__) app.config.from_object('yourapplication.default_settings') app.config.from_envvar('YOURAPPLICATION_SETTINGS') Then you just have to add a separate :file:`config.py` file and export ``YOURAPPLICATION_SETTINGS=/path/to/config.py`` and you are done. However there are alternative ways as well. For example you could use imports or subclassing. What is very popular in the Django world is to make the import explicit in the config file by adding ``from yourapplication.default_settings import *`` to the top of the file and then overriding the changes by hand. You could also inspect an environment variable like ``YOURAPPLICATION_MODE`` and set that to `production`, `development` etc and import different hard-coded files based on that. An interesting pattern is also to use classes and inheritance for configuration:: class Config(object): TESTING = False class ProductionConfig(Config): DATABASE_URI = 'mysql://user@localhost/foo' class DevelopmentConfig(Config): DATABASE_URI = "sqlite:////tmp/foo.db" class TestingConfig(Config): DATABASE_URI = 'sqlite:///:memory:' TESTING = True To enable such a config you just have to call into :meth:`~flask.Config.from_object`:: app.config.from_object('configmodule.ProductionConfig') Note that :meth:`~flask.Config.from_object` does not instantiate the class object. If you need to instantiate the class, such as to access a property, then you must do so before calling :meth:`~flask.Config.from_object`:: from configmodule import ProductionConfig app.config.from_object(ProductionConfig()) # Alternatively, import via string: from werkzeug.utils import import_string cfg = import_string('configmodule.ProductionConfig')() app.config.from_object(cfg) Instantiating the configuration object allows you to use ``@property`` in your configuration classes:: class Config(object): """Base config, uses staging database server.""" TESTING = False DB_SERVER = '192.168.1.56' @property def DATABASE_URI(self): # Note: all caps return f"mysql://user@{self.DB_SERVER}/foo" class ProductionConfig(Config): """Uses production database server.""" DB_SERVER = '192.168.19.32' class DevelopmentConfig(Config): DB_SERVER = 'localhost' class TestingConfig(Config): DB_SERVER = 'localhost' DATABASE_URI = 'sqlite:///:memory:' There are many different ways and it's up to you how you want to manage your configuration files. However here a list of good recommendations: - Keep a default configuration in version control. Either populate the config with this default configuration or import it in your own configuration files before overriding values. - Use an environment variable to switch between the configurations. This can be done from outside the Python interpreter and makes development and deployment much easier because you can quickly and easily switch between different configs without having to touch the code at all. If you are working often on different projects you can even create your own script for sourcing that activates a virtualenv and exports the development configuration for you. - Use a tool like `fabric`_ to push code and configuration separately to the production server(s). .. _fabric: https://www.fabfile.org/ .. _instance-folders: Instance Folders ---------------- .. versionadded:: 0.8 Flask 0.8 introduces instance folders. Flask for a long time made it possible to refer to paths relative to the application's folder directly (via :attr:`Flask.root_path`). This was also how many developers loaded configurations stored next to the application. Unfortunately however this only works well if applications are not packages in which case the root path refers to the contents of the package. With Flask 0.8 a new attribute was introduced: :attr:`Flask.instance_path`. It refers to a new concept called the “instance folderâ€. The instance folder is designed to not be under version control and be deployment specific. It's the perfect place to drop things that either change at runtime or configuration files. You can either explicitly provide the path of the instance folder when creating the Flask application or you can let Flask autodetect the instance folder. For explicit configuration use the `instance_path` parameter:: app = Flask(__name__, instance_path='/path/to/instance/folder') Please keep in mind that this path *must* be absolute when provided. If the `instance_path` parameter is not provided the following default locations are used: - Uninstalled module:: /myapp.py /instance - Uninstalled package:: /myapp /__init__.py /instance - Installed module or package:: $PREFIX/lib/pythonX.Y/site-packages/myapp $PREFIX/var/myapp-instance ``$PREFIX`` is the prefix of your Python installation. This can be ``/usr`` or the path to your virtualenv. You can print the value of ``sys.prefix`` to see what the prefix is set to. Since the config object provided loading of configuration files from relative filenames we made it possible to change the loading via filenames to be relative to the instance path if wanted. The behavior of relative paths in config files can be flipped between “relative to the application root†(the default) to “relative to instance folder†via the `instance_relative_config` switch to the application constructor:: app = Flask(__name__, instance_relative_config=True) Here is a full example of how to configure Flask to preload the config from a module and then override the config from a file in the instance folder if it exists:: app = Flask(__name__, instance_relative_config=True) app.config.from_object('yourapplication.default_settings') app.config.from_pyfile('application.cfg', silent=True) The path to the instance folder can be found via the :attr:`Flask.instance_path`. Flask also provides a shortcut to open a file from the instance folder with :meth:`Flask.open_instance_resource`. Example usage for both:: filename = os.path.join(app.instance_path, 'application.cfg') with open(filename) as f: config = f.read() # or via open_instance_resource: with app.open_instance_resource('application.cfg') as f: config = f.read() flask-3.1.1/docs/contributing.rst000066400000000000000000000004221501065614000170010ustar00rootroot00000000000000Contributing ============ See the Pallets `detailed contributing documentation <_contrib>`_ for many ways to contribute, including reporting issues, requesting features, asking or answering questions, and making PRs. .. _contrib: https://palletsprojects.com/contributing/ flask-3.1.1/docs/debugging.rst000066400000000000000000000066061501065614000162370ustar00rootroot00000000000000Debugging Application Errors ============================ In Production ------------- **Do not run the development server, or enable the built-in debugger, in a production environment.** The debugger allows executing arbitrary Python code from the browser. It's protected by a pin, but that should not be relied on for security. Use an error logging tool, such as Sentry, as described in :ref:`error-logging-tools`, or enable logging and notifications as described in :doc:`/logging`. If you have access to the server, you could add some code to start an external debugger if ``request.remote_addr`` matches your IP. Some IDE debuggers also have a remote mode so breakpoints on the server can be interacted with locally. Only enable a debugger temporarily. The Built-In Debugger --------------------- The built-in Werkzeug development server provides a debugger which shows an interactive traceback in the browser when an unhandled error occurs during a request. This debugger should only be used during development. .. image:: _static/debugger.png :align: center :class: screenshot :alt: screenshot of debugger in action .. warning:: The debugger allows executing arbitrary Python code from the browser. It is protected by a pin, but still represents a major security risk. Do not run the development server or debugger in a production environment. The debugger is enabled by default when the development server is run in debug mode. .. code-block:: text $ flask --app hello run --debug When running from Python code, passing ``debug=True`` enables debug mode, which is mostly equivalent. .. code-block:: python app.run(debug=True) :doc:`/server` and :doc:`/cli` have more information about running the debugger and debug mode. More information about the debugger can be found in the `Werkzeug documentation `__. External Debuggers ------------------ External debuggers, such as those provided by IDEs, can offer a more powerful debugging experience than the built-in debugger. They can also be used to step through code during a request before an error is raised, or if no error is raised. Some even have a remote mode so you can debug code running on another machine. When using an external debugger, the app should still be in debug mode, otherwise Flask turns unhandled errors into generic 500 error pages. However, the built-in debugger and reloader should be disabled so they don't interfere with the external debugger. .. code-block:: text $ flask --app hello run --debug --no-debugger --no-reload When running from Python: .. code-block:: python app.run(debug=True, use_debugger=False, use_reloader=False) Disabling these isn't required, an external debugger will continue to work with the following caveats. - If the built-in debugger is not disabled, it will catch unhandled exceptions before the external debugger can. - If the reloader is not disabled, it could cause an unexpected reload if code changes during a breakpoint. - The development server will still catch unhandled exceptions if the built-in debugger is disabled, otherwise it would crash on any error. If you want that (and usually you don't) pass ``passthrough_errors=True`` to ``app.run``. .. code-block:: python app.run( debug=True, passthrough_errors=True, use_debugger=False, use_reloader=False ) flask-3.1.1/docs/deploying/000077500000000000000000000000001501065614000155345ustar00rootroot00000000000000flask-3.1.1/docs/deploying/apache-httpd.rst000066400000000000000000000044741501065614000206410ustar00rootroot00000000000000Apache httpd ============ `Apache httpd`_ is a fast, production level HTTP server. When serving your application with one of the WSGI servers listed in :doc:`index`, it is often good or necessary to put a dedicated HTTP server in front of it. This "reverse proxy" can handle incoming requests, TLS, and other security and performance concerns better than the WSGI server. httpd can be installed using your system package manager, or a pre-built executable for Windows. Installing and running httpd itself is outside the scope of this doc. This page outlines the basics of configuring httpd to proxy your application. Be sure to read its documentation to understand what features are available. .. _Apache httpd: https://httpd.apache.org/ Domain Name ----------- Acquiring and configuring a domain name is outside the scope of this doc. In general, you will buy a domain name from a registrar, pay for server space with a hosting provider, and then point your registrar at the hosting provider's name servers. To simulate this, you can also edit your ``hosts`` file, located at ``/etc/hosts`` on Linux. Add a line that associates a name with the local IP. Modern Linux systems may be configured to treat any domain name that ends with ``.localhost`` like this without adding it to the ``hosts`` file. .. code-block:: python :caption: ``/etc/hosts`` 127.0.0.1 hello.localhost Configuration ------------- The httpd configuration is located at ``/etc/httpd/conf/httpd.conf`` on Linux. It may be different depending on your operating system. Check the docs and look for ``httpd.conf``. Remove or comment out any existing ``DocumentRoot`` directive. Add the config lines below. We'll assume the WSGI server is listening locally at ``http://127.0.0.1:8000``. .. code-block:: apache :caption: ``/etc/httpd/conf/httpd.conf`` LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_http_module modules/mod_proxy_http.so ProxyPass / http://127.0.0.1:8000/ RequestHeader set X-Forwarded-Proto http RequestHeader set X-Forwarded-Prefix / The ``LoadModule`` lines might already exist. If so, make sure they are uncommented instead of adding them manually. Then :doc:`proxy_fix` so that your application uses the ``X-Forwarded`` headers. ``X-Forwarded-For`` and ``X-Forwarded-Host`` are automatically set by ``ProxyPass``. flask-3.1.1/docs/deploying/asgi.rst000066400000000000000000000012411501065614000172070ustar00rootroot00000000000000ASGI ==== If you'd like to use an ASGI server you will need to utilise WSGI to ASGI middleware. The asgiref `WsgiToAsgi `_ adapter is recommended as it integrates with the event loop used for Flask's :ref:`async_await` support. You can use the adapter by wrapping the Flask app, .. code-block:: python from asgiref.wsgi import WsgiToAsgi from flask import Flask app = Flask(__name__) ... asgi_app = WsgiToAsgi(app) and then serving the ``asgi_app`` with the ASGI server, e.g. using `Hypercorn `_, .. sourcecode:: text $ hypercorn module:asgi_app flask-3.1.1/docs/deploying/eventlet.rst000066400000000000000000000045451501065614000201240ustar00rootroot00000000000000eventlet ======== Prefer using :doc:`gunicorn` with eventlet workers rather than using `eventlet`_ directly. Gunicorn provides a much more configurable and production-tested server. `eventlet`_ allows writing asynchronous, coroutine-based code that looks like standard synchronous Python. It uses `greenlet`_ to enable task switching without writing ``async/await`` or using ``asyncio``. :doc:`gevent` is another library that does the same thing. Certain dependencies you have, or other considerations, may affect which of the two you choose to use. eventlet provides a WSGI server that can handle many connections at once instead of one per worker process. You must actually use eventlet in your own code to see any benefit to using the server. .. _eventlet: https://eventlet.net/ .. _greenlet: https://greenlet.readthedocs.io/en/latest/ Installing ---------- When using eventlet, greenlet>=1.0 is required, otherwise context locals such as ``request`` will not work as expected. When using PyPy, PyPy>=7.3.7 is required. Create a virtualenv, install your application, then install ``eventlet``. .. code-block:: text $ cd hello-app $ python -m venv .venv $ . .venv/bin/activate $ pip install . # install your application $ pip install eventlet Running ------- To use eventlet to serve your application, write a script that imports its ``wsgi.server``, as well as your app or app factory. .. code-block:: python :caption: ``wsgi.py`` import eventlet from eventlet import wsgi from hello import create_app app = create_app() wsgi.server(eventlet.listen(("127.0.0.1", 8000)), app) .. code-block:: text $ python wsgi.py (x) wsgi starting up on http://127.0.0.1:8000 Binding Externally ------------------ eventlet should not be run as root because it would cause your application code to run as root, which is not secure. However, this means it will not be possible to bind to port 80 or 443. Instead, a reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used in front of eventlet. You can bind to all external IPs on a non-privileged port by using ``0.0.0.0`` in the server arguments shown in the previous section. Don't do this when using a reverse proxy setup, otherwise it will be possible to bypass the proxy. ``0.0.0.0`` is not a valid address to navigate to, you'd use a specific IP address in your browser. flask-3.1.1/docs/deploying/gevent.rst000066400000000000000000000045611501065614000175640ustar00rootroot00000000000000gevent ====== Prefer using :doc:`gunicorn` or :doc:`uwsgi` with gevent workers rather than using `gevent`_ directly. Gunicorn and uWSGI provide much more configurable and production-tested servers. `gevent`_ allows writing asynchronous, coroutine-based code that looks like standard synchronous Python. It uses `greenlet`_ to enable task switching without writing ``async/await`` or using ``asyncio``. :doc:`eventlet` is another library that does the same thing. Certain dependencies you have, or other considerations, may affect which of the two you choose to use. gevent provides a WSGI server that can handle many connections at once instead of one per worker process. You must actually use gevent in your own code to see any benefit to using the server. .. _gevent: https://www.gevent.org/ .. _greenlet: https://greenlet.readthedocs.io/en/latest/ Installing ---------- When using gevent, greenlet>=1.0 is required, otherwise context locals such as ``request`` will not work as expected. When using PyPy, PyPy>=7.3.7 is required. Create a virtualenv, install your application, then install ``gevent``. .. code-block:: text $ cd hello-app $ python -m venv .venv $ . .venv/bin/activate $ pip install . # install your application $ pip install gevent Running ------- To use gevent to serve your application, write a script that imports its ``WSGIServer``, as well as your app or app factory. .. code-block:: python :caption: ``wsgi.py`` from gevent.pywsgi import WSGIServer from hello import create_app app = create_app() http_server = WSGIServer(("127.0.0.1", 8000), app) http_server.serve_forever() .. code-block:: text $ python wsgi.py No output is shown when the server starts. Binding Externally ------------------ gevent should not be run as root because it would cause your application code to run as root, which is not secure. However, this means it will not be possible to bind to port 80 or 443. Instead, a reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used in front of gevent. You can bind to all external IPs on a non-privileged port by using ``0.0.0.0`` in the server arguments shown in the previous section. Don't do this when using a reverse proxy setup, otherwise it will be possible to bypass the proxy. ``0.0.0.0`` is not a valid address to navigate to, you'd use a specific IP address in your browser. flask-3.1.1/docs/deploying/gunicorn.rst000066400000000000000000000077121501065614000201210ustar00rootroot00000000000000Gunicorn ======== `Gunicorn`_ is a pure Python WSGI server with simple configuration and multiple worker implementations for performance tuning. * It tends to integrate easily with hosting platforms. * It does not support Windows (but does run on WSL). * It is easy to install as it does not require additional dependencies or compilation. * It has built-in async worker support using gevent or eventlet. This page outlines the basics of running Gunicorn. Be sure to read its `documentation`_ and use ``gunicorn --help`` to understand what features are available. .. _Gunicorn: https://gunicorn.org/ .. _documentation: https://docs.gunicorn.org/ Installing ---------- Gunicorn is easy to install, as it does not require external dependencies or compilation. It runs on Windows only under WSL. Create a virtualenv, install your application, then install ``gunicorn``. .. code-block:: text $ cd hello-app $ python -m venv .venv $ . .venv/bin/activate $ pip install . # install your application $ pip install gunicorn Running ------- The only required argument to Gunicorn tells it how to load your Flask application. The syntax is ``{module_import}:{app_variable}``. ``module_import`` is the dotted import name to the module with your application. ``app_variable`` is the variable with the application. It can also be a function call (with any arguments) if you're using the app factory pattern. .. code-block:: text # equivalent to 'from hello import app' $ gunicorn -w 4 'hello:app' # equivalent to 'from hello import create_app; create_app()' $ gunicorn -w 4 'hello:create_app()' Starting gunicorn 20.1.0 Listening at: http://127.0.0.1:8000 (x) Using worker: sync Booting worker with pid: x Booting worker with pid: x Booting worker with pid: x Booting worker with pid: x The ``-w`` option specifies the number of processes to run; a starting value could be ``CPU * 2``. The default is only 1 worker, which is probably not what you want for the default worker type. Logs for each request aren't shown by default, only worker info and errors are shown. To show access logs on stdout, use the ``--access-logfile=-`` option. Binding Externally ------------------ Gunicorn should not be run as root because it would cause your application code to run as root, which is not secure. However, this means it will not be possible to bind to port 80 or 443. Instead, a reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used in front of Gunicorn. You can bind to all external IPs on a non-privileged port using the ``-b 0.0.0.0`` option. Don't do this when using a reverse proxy setup, otherwise it will be possible to bypass the proxy. .. code-block:: text $ gunicorn -w 4 -b 0.0.0.0 'hello:create_app()' Listening at: http://0.0.0.0:8000 (x) ``0.0.0.0`` is not a valid address to navigate to, you'd use a specific IP address in your browser. Async with gevent or eventlet ----------------------------- The default sync worker is appropriate for many use cases. If you need asynchronous support, Gunicorn provides workers using either `gevent`_ or `eventlet`_. This is not the same as Python's ``async/await``, or the ASGI server spec. You must actually use gevent/eventlet in your own code to see any benefit to using the workers. When using either gevent or eventlet, greenlet>=1.0 is required, otherwise context locals such as ``request`` will not work as expected. When using PyPy, PyPy>=7.3.7 is required. To use gevent: .. code-block:: text $ gunicorn -k gevent 'hello:create_app()' Starting gunicorn 20.1.0 Listening at: http://127.0.0.1:8000 (x) Using worker: gevent Booting worker with pid: x To use eventlet: .. code-block:: text $ gunicorn -k eventlet 'hello:create_app()' Starting gunicorn 20.1.0 Listening at: http://127.0.0.1:8000 (x) Using worker: eventlet Booting worker with pid: x .. _gevent: https://www.gevent.org/ .. _eventlet: https://eventlet.net/ flask-3.1.1/docs/deploying/index.rst000066400000000000000000000061621501065614000174020ustar00rootroot00000000000000Deploying to Production ======================= After developing your application, you'll want to make it available publicly to other users. When you're developing locally, you're probably using the built-in development server, debugger, and reloader. These should not be used in production. Instead, you should use a dedicated WSGI server or hosting platform, some of which will be described here. "Production" means "not development", which applies whether you're serving your application publicly to millions of users or privately / locally to a single user. **Do not use the development server when deploying to production. It is intended for use only during local development. It is not designed to be particularly secure, stable, or efficient.** Self-Hosted Options ------------------- Flask is a WSGI *application*. A WSGI *server* is used to run the application, converting incoming HTTP requests to the standard WSGI environ, and converting outgoing WSGI responses to HTTP responses. The primary goal of these docs is to familiarize you with the concepts involved in running a WSGI application using a production WSGI server and HTTP server. There are many WSGI servers and HTTP servers, with many configuration possibilities. The pages below discuss the most common servers, and show the basics of running each one. The next section discusses platforms that can manage this for you. .. toctree:: :maxdepth: 1 gunicorn waitress mod_wsgi uwsgi gevent eventlet asgi WSGI servers have HTTP servers built-in. However, a dedicated HTTP server may be safer, more efficient, or more capable. Putting an HTTP server in front of the WSGI server is called a "reverse proxy." .. toctree:: :maxdepth: 1 proxy_fix nginx apache-httpd This list is not exhaustive, and you should evaluate these and other servers based on your application's needs. Different servers will have different capabilities, configuration, and support. Hosting Platforms ----------------- There are many services available for hosting web applications without needing to maintain your own server, networking, domain, etc. Some services may have a free tier up to a certain time or bandwidth. Many of these services use one of the WSGI servers described above, or a similar interface. The links below are for some of the most common platforms, which have instructions for Flask, WSGI, or Python. - `PythonAnywhere `_ - `Google App Engine `_ - `Google Cloud Run `_ - `AWS Elastic Beanstalk `_ - `Microsoft Azure `_ This list is not exhaustive, and you should evaluate these and other services based on your application's needs. Different services will have different capabilities, configuration, pricing, and support. You'll probably need to :doc:`proxy_fix` when using most hosting platforms. flask-3.1.1/docs/deploying/mod_wsgi.rst000066400000000000000000000053751501065614000201100ustar00rootroot00000000000000mod_wsgi ======== `mod_wsgi`_ is a WSGI server integrated with the `Apache httpd`_ server. The modern `mod_wsgi-express`_ command makes it easy to configure and start the server without needing to write Apache httpd configuration. * Tightly integrated with Apache httpd. * Supports Windows directly. * Requires a compiler and the Apache development headers to install. * Does not require a reverse proxy setup. This page outlines the basics of running mod_wsgi-express, not the more complex installation and configuration with httpd. Be sure to read the `mod_wsgi-express`_, `mod_wsgi`_, and `Apache httpd`_ documentation to understand what features are available. .. _mod_wsgi-express: https://pypi.org/project/mod-wsgi/ .. _mod_wsgi: https://modwsgi.readthedocs.io/ .. _Apache httpd: https://httpd.apache.org/ Installing ---------- Installing mod_wsgi requires a compiler and the Apache server and development headers installed. You will get an error if they are not. How to install them depends on the OS and package manager that you use. Create a virtualenv, install your application, then install ``mod_wsgi``. .. code-block:: text $ cd hello-app $ python -m venv .venv $ . .venv/bin/activate $ pip install . # install your application $ pip install mod_wsgi Running ------- The only argument to ``mod_wsgi-express`` specifies a script containing your Flask application, which must be called ``application``. You can write a small script to import your app with this name, or to create it if using the app factory pattern. .. code-block:: python :caption: ``wsgi.py`` from hello import app application = app .. code-block:: python :caption: ``wsgi.py`` from hello import create_app application = create_app() Now run the ``mod_wsgi-express start-server`` command. .. code-block:: text $ mod_wsgi-express start-server wsgi.py --processes 4 The ``--processes`` option specifies the number of worker processes to run; a starting value could be ``CPU * 2``. Logs for each request aren't show in the terminal. If an error occurs, its information is written to the error log file shown when starting the server. Binding Externally ------------------ Unlike the other WSGI servers in these docs, mod_wsgi can be run as root to bind to privileged ports like 80 and 443. However, it must be configured to drop permissions to a different user and group for the worker processes. For example, if you created a ``hello`` user and group, you should install your virtualenv and application as that user, then tell mod_wsgi to drop to that user after starting. .. code-block:: text $ sudo /home/hello/.venv/bin/mod_wsgi-express start-server \ /home/hello/wsgi.py \ --user hello --group hello --port 80 --processes 4 flask-3.1.1/docs/deploying/nginx.rst000066400000000000000000000044141501065614000174140ustar00rootroot00000000000000nginx ===== `nginx`_ is a fast, production level HTTP server. When serving your application with one of the WSGI servers listed in :doc:`index`, it is often good or necessary to put a dedicated HTTP server in front of it. This "reverse proxy" can handle incoming requests, TLS, and other security and performance concerns better than the WSGI server. Nginx can be installed using your system package manager, or a pre-built executable for Windows. Installing and running Nginx itself is outside the scope of this doc. This page outlines the basics of configuring Nginx to proxy your application. Be sure to read its documentation to understand what features are available. .. _nginx: https://nginx.org/ Domain Name ----------- Acquiring and configuring a domain name is outside the scope of this doc. In general, you will buy a domain name from a registrar, pay for server space with a hosting provider, and then point your registrar at the hosting provider's name servers. To simulate this, you can also edit your ``hosts`` file, located at ``/etc/hosts`` on Linux. Add a line that associates a name with the local IP. Modern Linux systems may be configured to treat any domain name that ends with ``.localhost`` like this without adding it to the ``hosts`` file. .. code-block:: python :caption: ``/etc/hosts`` 127.0.0.1 hello.localhost Configuration ------------- The nginx configuration is located at ``/etc/nginx/nginx.conf`` on Linux. It may be different depending on your operating system. Check the docs and look for ``nginx.conf``. Remove or comment out any existing ``server`` section. Add a ``server`` section and use the ``proxy_pass`` directive to point to the address the WSGI server is listening on. We'll assume the WSGI server is listening locally at ``http://127.0.0.1:8000``. .. code-block:: nginx :caption: ``/etc/nginx.conf`` server { listen 80; server_name _; location / { proxy_pass http://127.0.0.1:8000/; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Prefix /; } } Then :doc:`proxy_fix` so that your application uses these headers. flask-3.1.1/docs/deploying/proxy_fix.rst000066400000000000000000000025251501065614000203210ustar00rootroot00000000000000Tell Flask it is Behind a Proxy =============================== When using a reverse proxy, or many Python hosting platforms, the proxy will intercept and forward all external requests to the local WSGI server. From the WSGI server and Flask application's perspectives, requests are now coming from the HTTP server to the local address, rather than from the remote address to the external server address. HTTP servers should set ``X-Forwarded-`` headers to pass on the real values to the application. The application can then be told to trust and use those values by wrapping it with the :doc:`werkzeug:middleware/proxy_fix` middleware provided by Werkzeug. This middleware should only be used if the application is actually behind a proxy, and should be configured with the number of proxies that are chained in front of it. Not all proxies set all the headers. Since incoming headers can be faked, you must set how many proxies are setting each header so the middleware knows what to trust. .. code-block:: python from werkzeug.middleware.proxy_fix import ProxyFix app.wsgi_app = ProxyFix( app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 ) Remember, only apply this middleware if you are behind a proxy, and set the correct number of proxies that set each header. It can be a security issue if you get this configuration wrong. flask-3.1.1/docs/deploying/uwsgi.rst000066400000000000000000000112401501065614000174220ustar00rootroot00000000000000uWSGI ===== `uWSGI`_ is a fast, compiled server suite with extensive configuration and capabilities beyond a basic server. * It can be very performant due to being a compiled program. * It is complex to configure beyond the basic application, and has so many options that it can be difficult for beginners to understand. * It does not support Windows (but does run on WSL). * It requires a compiler to install in some cases. This page outlines the basics of running uWSGI. Be sure to read its documentation to understand what features are available. .. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/ Installing ---------- uWSGI has multiple ways to install it. The most straightforward is to install the ``pyuwsgi`` package, which provides precompiled wheels for common platforms. However, it does not provide SSL support, which can be provided with a reverse proxy instead. Create a virtualenv, install your application, then install ``pyuwsgi``. .. code-block:: text $ cd hello-app $ python -m venv .venv $ . .venv/bin/activate $ pip install . # install your application $ pip install pyuwsgi If you have a compiler available, you can install the ``uwsgi`` package instead. Or install the ``pyuwsgi`` package from sdist instead of wheel. Either method will include SSL support. .. code-block:: text $ pip install uwsgi # or $ pip install --no-binary pyuwsgi pyuwsgi Running ------- The most basic way to run uWSGI is to tell it to start an HTTP server and import your application. .. code-block:: text $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w hello:app *** Starting uWSGI 2.0.20 (64bit) on [x] *** *** Operational MODE: preforking *** mounting hello:app on / spawned uWSGI master process (pid: x) spawned uWSGI worker 1 (pid: x, cores: 1) spawned uWSGI worker 2 (pid: x, cores: 1) spawned uWSGI worker 3 (pid: x, cores: 1) spawned uWSGI worker 4 (pid: x, cores: 1) spawned uWSGI http 1 (pid: x) If you're using the app factory pattern, you'll need to create a small Python file to create the app, then point uWSGI at that. .. code-block:: python :caption: ``wsgi.py`` from hello import create_app app = create_app() .. code-block:: text $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w wsgi:app The ``--http`` option starts an HTTP server at 127.0.0.1 port 8000. The ``--master`` option specifies the standard worker manager. The ``-p`` option starts 4 worker processes; a starting value could be ``CPU * 2``. The ``-w`` option tells uWSGI how to import your application Binding Externally ------------------ uWSGI should not be run as root with the configuration shown in this doc because it would cause your application code to run as root, which is not secure. However, this means it will not be possible to bind to port 80 or 443. Instead, a reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used in front of uWSGI. It is possible to run uWSGI as root securely, but that is beyond the scope of this doc. uWSGI has optimized integration with `Nginx uWSGI`_ and `Apache mod_proxy_uwsgi`_, and possibly other servers, instead of using a standard HTTP proxy. That configuration is beyond the scope of this doc, see the links for more information. .. _Nginx uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/Nginx.html .. _Apache mod_proxy_uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/Apache.html#mod-proxy-uwsgi You can bind to all external IPs on a non-privileged port using the ``--http 0.0.0.0:8000`` option. Don't do this when using a reverse proxy setup, otherwise it will be possible to bypass the proxy. .. code-block:: text $ uwsgi --http 0.0.0.0:8000 --master -p 4 -w wsgi:app ``0.0.0.0`` is not a valid address to navigate to, you'd use a specific IP address in your browser. Async with gevent ----------------- The default sync worker is appropriate for many use cases. If you need asynchronous support, uWSGI provides a `gevent`_ worker. This is not the same as Python's ``async/await``, or the ASGI server spec. You must actually use gevent in your own code to see any benefit to using the worker. When using gevent, greenlet>=1.0 is required, otherwise context locals such as ``request`` will not work as expected. When using PyPy, PyPy>=7.3.7 is required. .. code-block:: text $ uwsgi --http 127.0.0.1:8000 --master --gevent 100 -w wsgi:app *** Starting uWSGI 2.0.20 (64bit) on [x] *** *** Operational MODE: async *** mounting hello:app on / spawned uWSGI master process (pid: x) spawned uWSGI worker 1 (pid: x, cores: 100) spawned uWSGI http 1 (pid: x) *** running gevent loop engine [addr:x] *** .. _gevent: https://www.gevent.org/ flask-3.1.1/docs/deploying/waitress.rst000066400000000000000000000044411501065614000201320ustar00rootroot00000000000000Waitress ======== `Waitress`_ is a pure Python WSGI server. * It is easy to configure. * It supports Windows directly. * It is easy to install as it does not require additional dependencies or compilation. * It does not support streaming requests, full request data is always buffered. * It uses a single process with multiple thread workers. This page outlines the basics of running Waitress. Be sure to read its documentation and ``waitress-serve --help`` to understand what features are available. .. _Waitress: https://docs.pylonsproject.org/projects/waitress/ Installing ---------- Create a virtualenv, install your application, then install ``waitress``. .. code-block:: text $ cd hello-app $ python -m venv .venv $ . .venv/bin/activate $ pip install . # install your application $ pip install waitress Running ------- The only required argument to ``waitress-serve`` tells it how to load your Flask application. The syntax is ``{module}:{app}``. ``module`` is the dotted import name to the module with your application. ``app`` is the variable with the application. If you're using the app factory pattern, use ``--call {module}:{factory}`` instead. .. code-block:: text # equivalent to 'from hello import app' $ waitress-serve --host 127.0.0.1 hello:app # equivalent to 'from hello import create_app; create_app()' $ waitress-serve --host 127.0.0.1 --call hello:create_app Serving on http://127.0.0.1:8080 The ``--host`` option binds the server to local ``127.0.0.1`` only. Logs for each request aren't shown, only errors are shown. Logging can be configured through the Python interface instead of the command line. Binding Externally ------------------ Waitress should not be run as root because it would cause your application code to run as root, which is not secure. However, this means it will not be possible to bind to port 80 or 443. Instead, a reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used in front of Waitress. You can bind to all external IPs on a non-privileged port by not specifying the ``--host`` option. Don't do this when using a reverse proxy setup, otherwise it will be possible to bypass the proxy. ``0.0.0.0`` is not a valid address to navigate to, you'd use a specific IP address in your browser. flask-3.1.1/docs/design.rst000066400000000000000000000255021501065614000155510ustar00rootroot00000000000000Design Decisions in Flask ========================= If you are curious why Flask does certain things the way it does and not differently, this section is for you. This should give you an idea about some of the design decisions that may appear arbitrary and surprising at first, especially in direct comparison with other frameworks. The Explicit Application Object ------------------------------- A Python web application based on WSGI has to have one central callable object that implements the actual application. In Flask this is an instance of the :class:`~flask.Flask` class. Each Flask application has to create an instance of this class itself and pass it the name of the module, but why can't Flask do that itself? Without such an explicit application object the following code:: from flask import Flask app = Flask(__name__) @app.route('/') def index(): return 'Hello World!' Would look like this instead:: from hypothetical_flask import route @route('/') def index(): return 'Hello World!' There are three major reasons for this. The most important one is that implicit application objects require that there may only be one instance at the time. There are ways to fake multiple applications with a single application object, like maintaining a stack of applications, but this causes some problems I won't outline here in detail. Now the question is: when does a microframework need more than one application at the same time? A good example for this is unit testing. When you want to test something it can be very helpful to create a minimal application to test specific behavior. When the application object is deleted everything it allocated will be freed again. Another thing that becomes possible when you have an explicit object lying around in your code is that you can subclass the base class (:class:`~flask.Flask`) to alter specific behavior. This would not be possible without hacks if the object were created ahead of time for you based on a class that is not exposed to you. But there is another very important reason why Flask depends on an explicit instantiation of that class: the package name. Whenever you create a Flask instance you usually pass it `__name__` as package name. Flask depends on that information to properly load resources relative to your module. With Python's outstanding support for reflection it can then access the package to figure out where the templates and static files are stored (see :meth:`~flask.Flask.open_resource`). Now obviously there are frameworks around that do not need any configuration and will still be able to load templates relative to your application module. But they have to use the current working directory for that, which is a very unreliable way to determine where the application is. The current working directory is process-wide and if you are running multiple applications in one process (which could happen in a webserver without you knowing) the paths will be off. Worse: many webservers do not set the working directory to the directory of your application but to the document root which does not have to be the same folder. The third reason is "explicit is better than implicit". That object is your WSGI application, you don't have to remember anything else. If you want to apply a WSGI middleware, just wrap it and you're done (though there are better ways to do that so that you do not lose the reference to the application object :meth:`~flask.Flask.wsgi_app`). Furthermore this design makes it possible to use a factory function to create the application which is very helpful for unit testing and similar things (:doc:`/patterns/appfactories`). The Routing System ------------------ Flask uses the Werkzeug routing system which was designed to automatically order routes by complexity. This means that you can declare routes in arbitrary order and they will still work as expected. This is a requirement if you want to properly implement decorator based routing since decorators could be fired in undefined order when the application is split into multiple modules. Another design decision with the Werkzeug routing system is that routes in Werkzeug try to ensure that URLs are unique. Werkzeug will go quite far with that in that it will automatically redirect to a canonical URL if a route is ambiguous. One Template Engine ------------------- Flask decides on one template engine: Jinja2. Why doesn't Flask have a pluggable template engine interface? You can obviously use a different template engine, but Flask will still configure Jinja2 for you. While that limitation that Jinja2 is *always* configured will probably go away, the decision to bundle one template engine and use that will not. Template engines are like programming languages and each of those engines has a certain understanding about how things work. On the surface they all work the same: you tell the engine to evaluate a template with a set of variables and take the return value as string. But that's about where similarities end. Jinja2 for example has an extensive filter system, a certain way to do template inheritance, support for reusable blocks (macros) that can be used from inside templates and also from Python code, supports iterative template rendering, configurable syntax and more. On the other hand an engine like Genshi is based on XML stream evaluation, template inheritance by taking the availability of XPath into account and more. Mako on the other hand treats templates similar to Python modules. When it comes to connecting a template engine with an application or framework there is more than just rendering templates. For instance, Flask uses Jinja2's extensive autoescaping support. Also it provides ways to access macros from Jinja2 templates. A template abstraction layer that would not take the unique features of the template engines away is a science on its own and a too large undertaking for a microframework like Flask. Furthermore extensions can then easily depend on one template language being present. You can easily use your own templating language, but an extension could still depend on Jinja itself. What does "micro" mean? ----------------------- “Micro†does not mean that your whole web application has to fit into a single Python file (although it certainly can), nor does it mean that Flask is lacking in functionality. The "micro" in microframework means Flask aims to keep the core simple but extensible. Flask won't make many decisions for you, such as what database to use. Those decisions that it does make, such as what templating engine to use, are easy to change. Everything else is up to you, so that Flask can be everything you need and nothing you don't. By default, Flask does not include a database abstraction layer, form validation or anything else where different libraries already exist that can handle that. Instead, Flask supports extensions to add such functionality to your application as if it was implemented in Flask itself. Numerous extensions provide database integration, form validation, upload handling, various open authentication technologies, and more. Flask may be "micro", but it's ready for production use on a variety of needs. Why does Flask call itself a microframework and yet it depends on two libraries (namely Werkzeug and Jinja2). Why shouldn't it? If we look over to the Ruby side of web development there we have a protocol very similar to WSGI. Just that it's called Rack there, but besides that it looks very much like a WSGI rendition for Ruby. But nearly all applications in Ruby land do not work with Rack directly, but on top of a library with the same name. This Rack library has two equivalents in Python: WebOb (formerly Paste) and Werkzeug. Paste is still around but from my understanding it's sort of deprecated in favour of WebOb. The development of WebOb and Werkzeug started side by side with similar ideas in mind: be a good implementation of WSGI for other applications to take advantage. Flask is a framework that takes advantage of the work already done by Werkzeug to properly interface WSGI (which can be a complex task at times). Thanks to recent developments in the Python package infrastructure, packages with dependencies are no longer an issue and there are very few reasons against having libraries that depend on others. Thread Locals ------------- Flask uses thread local objects (context local objects in fact, they support greenlet contexts as well) for request, session and an extra object you can put your own things on (:data:`~flask.g`). Why is that and isn't that a bad idea? Yes it is usually not such a bright idea to use thread locals. They cause troubles for servers that are not based on the concept of threads and make large applications harder to maintain. However Flask is just not designed for large applications or asynchronous servers. Flask wants to make it quick and easy to write a traditional web application. Async/await and ASGI support ---------------------------- Flask supports ``async`` coroutines for view functions by executing the coroutine on a separate thread instead of using an event loop on the main thread as an async-first (ASGI) framework would. This is necessary for Flask to remain backwards compatible with extensions and code built before ``async`` was introduced into Python. This compromise introduces a performance cost compared with the ASGI frameworks, due to the overhead of the threads. Due to how tied to WSGI Flask's code is, it's not clear if it's possible to make the ``Flask`` class support ASGI and WSGI at the same time. Work is currently being done in Werkzeug to work with ASGI, which may eventually enable support in Flask as well. See :doc:`/async-await` for more discussion. What Flask is, What Flask is Not -------------------------------- Flask will never have a database layer. It will not have a form library or anything else in that direction. Flask itself just bridges to Werkzeug to implement a proper WSGI application and to Jinja2 to handle templating. It also binds to a few common standard library packages such as logging. Everything else is up for extensions. Why is this the case? Because people have different preferences and requirements and Flask could not meet those if it would force any of this into the core. The majority of web applications will need a template engine in some sort. However not every application needs a SQL database. As your codebase grows, you are free to make the design decisions appropriate for your project. Flask will continue to provide a very simple glue layer to the best that Python has to offer. You can implement advanced patterns in SQLAlchemy or another database tool, introduce non-relational data persistence as appropriate, and take advantage of framework-agnostic tools built for WSGI, the Python web interface. The idea of Flask is to build a good foundation for all applications. Everything else is up to you or extensions. flask-3.1.1/docs/errorhandling.rst000066400000000000000000000437051501065614000171430ustar00rootroot00000000000000Handling Application Errors =========================== Applications fail, servers fail. Sooner or later you will see an exception in production. Even if your code is 100% correct, you will still see exceptions from time to time. Why? Because everything else involved will fail. Here are some situations where perfectly fine code can lead to server errors: - the client terminated the request early and the application was still reading from the incoming data - the database server was overloaded and could not handle the query - a filesystem is full - a harddrive crashed - a backend server overloaded - a programming error in a library you are using - network connection of the server to another system failed And that's just a small sample of issues you could be facing. So how do we deal with that sort of problem? By default if your application runs in production mode, and an exception is raised Flask will display a very simple page for you and log the exception to the :attr:`~flask.Flask.logger`. But there is more you can do, and we will cover some better setups to deal with errors including custom exceptions and 3rd party tools. .. _error-logging-tools: Error Logging Tools ------------------- Sending error mails, even if just for critical ones, can become overwhelming if enough users are hitting the error and log files are typically never looked at. This is why we recommend using `Sentry `_ for dealing with application errors. It's available as a source-available project `on GitHub `_ and is also available as a `hosted version `_ which you can try for free. Sentry aggregates duplicate errors, captures the full stack trace and local variables for debugging, and sends you mails based on new errors or frequency thresholds. To use Sentry you need to install the ``sentry-sdk`` client with extra ``flask`` dependencies. .. code-block:: text $ pip install sentry-sdk[flask] And then add this to your Flask app: .. code-block:: python import sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()]) The ``YOUR_DSN_HERE`` value needs to be replaced with the DSN value you get from your Sentry installation. After installation, failures leading to an Internal Server Error are automatically reported to Sentry and from there you can receive error notifications. See also: - Sentry also supports catching errors from a worker queue (RQ, Celery, etc.) in a similar fashion. See the `Python SDK docs `__ for more information. - `Flask-specific documentation `__ Error Handlers -------------- When an error occurs in Flask, an appropriate `HTTP status code `__ will be returned. 400-499 indicate errors with the client's request data, or about the data requested. 500-599 indicate errors with the server or application itself. You might want to show custom error pages to the user when an error occurs. This can be done by registering error handlers. An error handler is a function that returns a response when a type of error is raised, similar to how a view is a function that returns a response when a request URL is matched. It is passed the instance of the error being handled, which is most likely a :exc:`~werkzeug.exceptions.HTTPException`. The status code of the response will not be set to the handler's code. Make sure to provide the appropriate HTTP status code when returning a response from a handler. Registering ``````````` Register handlers by decorating a function with :meth:`~flask.Flask.errorhandler`. Or use :meth:`~flask.Flask.register_error_handler` to register the function later. Remember to set the error code when returning the response. .. code-block:: python @app.errorhandler(werkzeug.exceptions.BadRequest) def handle_bad_request(e): return 'bad request!', 400 # or, without the decorator app.register_error_handler(400, handle_bad_request) :exc:`werkzeug.exceptions.HTTPException` subclasses like :exc:`~werkzeug.exceptions.BadRequest` and their HTTP codes are interchangeable when registering handlers. (``BadRequest.code == 400``) Non-standard HTTP codes cannot be registered by code because they are not known by Werkzeug. Instead, define a subclass of :class:`~werkzeug.exceptions.HTTPException` with the appropriate code and register and raise that exception class. .. code-block:: python class InsufficientStorage(werkzeug.exceptions.HTTPException): code = 507 description = 'Not enough storage space.' app.register_error_handler(InsufficientStorage, handle_507) raise InsufficientStorage() Handlers can be registered for any exception class, not just :exc:`~werkzeug.exceptions.HTTPException` subclasses or HTTP status codes. Handlers can be registered for a specific class, or for all subclasses of a parent class. Handling ```````` When building a Flask application you *will* run into exceptions. If some part of your code breaks while handling a request (and you have no error handlers registered), a "500 Internal Server Error" (:exc:`~werkzeug.exceptions.InternalServerError`) will be returned by default. Similarly, "404 Not Found" (:exc:`~werkzeug.exceptions.NotFound`) error will occur if a request is sent to an unregistered route. If a route receives an unallowed request method, a "405 Method Not Allowed" (:exc:`~werkzeug.exceptions.MethodNotAllowed`) will be raised. These are all subclasses of :class:`~werkzeug.exceptions.HTTPException` and are provided by default in Flask. Flask gives you the ability to raise any HTTP exception registered by Werkzeug. However, the default HTTP exceptions return simple exception pages. You might want to show custom error pages to the user when an error occurs. This can be done by registering error handlers. When Flask catches an exception while handling a request, it is first looked up by code. If no handler is registered for the code, Flask looks up the error by its class hierarchy; the most specific handler is chosen. If no handler is registered, :class:`~werkzeug.exceptions.HTTPException` subclasses show a generic message about their code, while other exceptions are converted to a generic "500 Internal Server Error". For example, if an instance of :exc:`ConnectionRefusedError` is raised, and a handler is registered for :exc:`ConnectionError` and :exc:`ConnectionRefusedError`, the more specific :exc:`ConnectionRefusedError` handler is called with the exception instance to generate the response. Handlers registered on the blueprint take precedence over those registered globally on the application, assuming a blueprint is handling the request that raises the exception. However, the blueprint cannot handle 404 routing errors because the 404 occurs at the routing level before the blueprint can be determined. Generic Exception Handlers `````````````````````````` It is possible to register error handlers for very generic base classes such as ``HTTPException`` or even ``Exception``. However, be aware that these will catch more than you might expect. For example, an error handler for ``HTTPException`` might be useful for turning the default HTML errors pages into JSON. However, this handler will trigger for things you don't cause directly, such as 404 and 405 errors during routing. Be sure to craft your handler carefully so you don't lose information about the HTTP error. .. code-block:: python from flask import json from werkzeug.exceptions import HTTPException @app.errorhandler(HTTPException) def handle_exception(e): """Return JSON instead of HTML for HTTP errors.""" # start with the correct headers and status code from the error response = e.get_response() # replace the body with JSON response.data = json.dumps({ "code": e.code, "name": e.name, "description": e.description, }) response.content_type = "application/json" return response An error handler for ``Exception`` might seem useful for changing how all errors, even unhandled ones, are presented to the user. However, this is similar to doing ``except Exception:`` in Python, it will capture *all* otherwise unhandled errors, including all HTTP status codes. In most cases it will be safer to register handlers for more specific exceptions. Since ``HTTPException`` instances are valid WSGI responses, you could also pass them through directly. .. code-block:: python from werkzeug.exceptions import HTTPException @app.errorhandler(Exception) def handle_exception(e): # pass through HTTP errors if isinstance(e, HTTPException): return e # now you're handling non-HTTP exceptions only return render_template("500_generic.html", e=e), 500 Error handlers still respect the exception class hierarchy. If you register handlers for both ``HTTPException`` and ``Exception``, the ``Exception`` handler will not handle ``HTTPException`` subclasses because the ``HTTPException`` handler is more specific. Unhandled Exceptions ```````````````````` When there is no error handler registered for an exception, a 500 Internal Server Error will be returned instead. See :meth:`flask.Flask.handle_exception` for information about this behavior. If there is an error handler registered for ``InternalServerError``, this will be invoked. As of Flask 1.1.0, this error handler will always be passed an instance of ``InternalServerError``, not the original unhandled error. The original error is available as ``e.original_exception``. An error handler for "500 Internal Server Error" will be passed uncaught exceptions in addition to explicit 500 errors. In debug mode, a handler for "500 Internal Server Error" will not be used. Instead, the interactive debugger will be shown. Custom Error Pages ------------------ Sometimes when building a Flask application, you might want to raise a :exc:`~werkzeug.exceptions.HTTPException` to signal to the user that something is wrong with the request. Fortunately, Flask comes with a handy :func:`~flask.abort` function that aborts a request with a HTTP error from werkzeug as desired. It will also provide a plain black and white error page for you with a basic description, but nothing fancy. Depending on the error code it is less or more likely for the user to actually see such an error. Consider the code below, we might have a user profile route, and if the user fails to pass a username we can raise a "400 Bad Request". If the user passes a username and we can't find it, we raise a "404 Not Found". .. code-block:: python from flask import abort, render_template, request # a username needs to be supplied in the query args # a successful request would be like /profile?username=jack @app.route("/profile") def user_profile(): username = request.arg.get("username") # if a username isn't supplied in the request, return a 400 bad request if username is None: abort(400) user = get_user(username=username) # if a user can't be found by their username, return 404 not found if user is None: abort(404) return render_template("profile.html", user=user) Here is another example implementation for a "404 Page Not Found" exception: .. code-block:: python from flask import render_template @app.errorhandler(404) def page_not_found(e): # note that we set the 404 status explicitly return render_template('404.html'), 404 When using :doc:`/patterns/appfactories`: .. code-block:: python from flask import Flask, render_template def page_not_found(e): return render_template('404.html'), 404 def create_app(config_filename): app = Flask(__name__) app.register_error_handler(404, page_not_found) return app An example template might be this: .. code-block:: html+jinja {% extends "layout.html" %} {% block title %}Page Not Found{% endblock %} {% block body %}

Page Not Found

What you were looking for is just not there.

go somewhere nice {% endblock %} Further Examples ```````````````` The above examples wouldn't actually be an improvement on the default exception pages. We can create a custom 500.html template like this: .. code-block:: html+jinja {% extends "layout.html" %} {% block title %}Internal Server Error{% endblock %} {% block body %}

Internal Server Error

Oops... we seem to have made a mistake, sorry!

Go somewhere nice instead {% endblock %} It can be implemented by rendering the template on "500 Internal Server Error": .. code-block:: python from flask import render_template @app.errorhandler(500) def internal_server_error(e): # note that we set the 500 status explicitly return render_template('500.html'), 500 When using :doc:`/patterns/appfactories`: .. code-block:: python from flask import Flask, render_template def internal_server_error(e): return render_template('500.html'), 500 def create_app(): app = Flask(__name__) app.register_error_handler(500, internal_server_error) return app When using :doc:`/blueprints`: .. code-block:: python from flask import Blueprint blog = Blueprint('blog', __name__) # as a decorator @blog.errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500 # or with register_error_handler blog.register_error_handler(500, internal_server_error) Blueprint Error Handlers ------------------------ In :doc:`/blueprints`, most error handlers will work as expected. However, there is a caveat concerning handlers for 404 and 405 exceptions. These error handlers are only invoked from an appropriate ``raise`` statement or a call to ``abort`` in another of the blueprint's view functions; they are not invoked by, e.g., an invalid URL access. This is because the blueprint does not "own" a certain URL space, so the application instance has no way of knowing which blueprint error handler it should run if given an invalid URL. If you would like to execute different handling strategies for these errors based on URL prefixes, they may be defined at the application level using the ``request`` proxy object. .. code-block:: python from flask import jsonify, render_template # at the application level # not the blueprint level @app.errorhandler(404) def page_not_found(e): # if a request is in our blog URL space if request.path.startswith('/blog/'): # we return a custom blog 404 page return render_template("blog/404.html"), 404 else: # otherwise we return our generic site-wide 404 page return render_template("404.html"), 404 @app.errorhandler(405) def method_not_allowed(e): # if a request has the wrong method to our API if request.path.startswith('/api/'): # we return a json saying so return jsonify(message="Method Not Allowed"), 405 else: # otherwise we return a generic site-wide 405 page return render_template("405.html"), 405 Returning API Errors as JSON ---------------------------- When building APIs in Flask, some developers realise that the built-in exceptions are not expressive enough for APIs and that the content type of :mimetype:`text/html` they are emitting is not very useful for API consumers. Using the same techniques as above and :func:`~flask.json.jsonify` we can return JSON responses to API errors. :func:`~flask.abort` is called with a ``description`` parameter. The error handler will use that as the JSON error message, and set the status code to 404. .. code-block:: python from flask import abort, jsonify @app.errorhandler(404) def resource_not_found(e): return jsonify(error=str(e)), 404 @app.route("/cheese") def get_one_cheese(): resource = get_resource() if resource is None: abort(404, description="Resource not found") return jsonify(resource) We can also create custom exception classes. For instance, we can introduce a new custom exception for an API that can take a proper human readable message, a status code for the error and some optional payload to give more context for the error. This is a simple example: .. code-block:: python from flask import jsonify, request class InvalidAPIUsage(Exception): status_code = 400 def __init__(self, message, status_code=None, payload=None): super().__init__() self.message = message if status_code is not None: self.status_code = status_code self.payload = payload def to_dict(self): rv = dict(self.payload or ()) rv['message'] = self.message return rv @app.errorhandler(InvalidAPIUsage) def invalid_api_usage(e): return jsonify(e.to_dict()), e.status_code # an API app route for getting user information # a correct request might be /api/user?user_id=420 @app.route("/api/user") def user_api(user_id): user_id = request.arg.get("user_id") if not user_id: raise InvalidAPIUsage("No user id provided!") user = get_user(user_id=user_id) if not user: raise InvalidAPIUsage("No such user!", status_code=404) return jsonify(user.to_dict()) A view can now raise that exception with an error message. Additionally some extra payload can be provided as a dictionary through the `payload` parameter. Logging ------- See :doc:`/logging` for information about how to log exceptions, such as by emailing them to admins. Debugging --------- See :doc:`/debugging` for information about how to debug errors in development and production. flask-3.1.1/docs/extensiondev.rst000066400000000000000000000307631501065614000170200ustar00rootroot00000000000000Flask Extension Development =========================== .. currentmodule:: flask Extensions are extra packages that add functionality to a Flask application. While `PyPI`_ contains many Flask extensions, you may not find one that fits your need. If this is the case, you can create your own, and publish it for others to use as well. This guide will show how to create a Flask extension, and some of the common patterns and requirements involved. Since extensions can do anything, this guide won't be able to cover every possibility. The best ways to learn about extensions are to look at how other extensions you use are written, and discuss with others. Discuss your design ideas with others on our `Discord Chat`_ or `GitHub Discussions`_. The best extensions share common patterns, so that anyone familiar with using one extension won't feel completely lost with another. This can only work if collaboration happens early. Naming ------ A Flask extension typically has ``flask`` in its name as a prefix or suffix. If it wraps another library, it should include the library name as well. This makes it easy to search for extensions, and makes their purpose clearer. A general Python packaging recommendation is that the install name from the package index and the name used in ``import`` statements should be related. The import name is lowercase, with words separated by underscores (``_``). The install name is either lower case or title case, with words separated by dashes (``-``). If it wraps another library, prefer using the same case as that library's name. Here are some example install and import names: - ``Flask-Name`` imported as ``flask_name`` - ``flask-name-lower`` imported as ``flask_name_lower`` - ``Flask-ComboName`` imported as ``flask_comboname`` - ``Name-Flask`` imported as ``name_flask`` The Extension Class and Initialization -------------------------------------- All extensions will need some entry point that initializes the extension with the application. The most common pattern is to create a class that represents the extension's configuration and behavior, with an ``init_app`` method to apply the extension instance to the given application instance. .. code-block:: python class HelloExtension: def __init__(self, app=None): if app is not None: self.init_app(app) def init_app(self, app): app.before_request(...) It is important that the app is not stored on the extension, don't do ``self.app = app``. The only time the extension should have direct access to an app is during ``init_app``, otherwise it should use :data:`current_app`. This allows the extension to support the application factory pattern, avoids circular import issues when importing the extension instance elsewhere in a user's code, and makes testing with different configurations easier. .. code-block:: python hello = HelloExtension() def create_app(): app = Flask(__name__) hello.init_app(app) return app Above, the ``hello`` extension instance exists independently of the application. This means that other modules in a user's project can do ``from project import hello`` and use the extension in blueprints before the app exists. The :attr:`Flask.extensions` dict can be used to store a reference to the extension on the application, or some other state specific to the application. Be aware that this is a single namespace, so use a name unique to your extension, such as the extension's name without the "flask" prefix. Adding Behavior --------------- There are many ways that an extension can add behavior. Any setup methods that are available on the :class:`Flask` object can be used during an extension's ``init_app`` method. A common pattern is to use :meth:`~Flask.before_request` to initialize some data or a connection at the beginning of each request, then :meth:`~Flask.teardown_request` to clean it up at the end. This can be stored on :data:`g`, discussed more below. A more lazy approach is to provide a method that initializes and caches the data or connection. For example, a ``ext.get_db`` method could create a database connection the first time it's called, so that a view that doesn't use the database doesn't create a connection. Besides doing something before and after every view, your extension might want to add some specific views as well. In this case, you could define a :class:`Blueprint`, then call :meth:`~Flask.register_blueprint` during ``init_app`` to add the blueprint to the app. Configuration Techniques ------------------------ There can be multiple levels and sources of configuration for an extension. You should consider what parts of your extension fall into each one. - Configuration per application instance, through ``app.config`` values. This is configuration that could reasonably change for each deployment of an application. A common example is a URL to an external resource, such as a database. Configuration keys should start with the extension's name so that they don't interfere with other extensions. - Configuration per extension instance, through ``__init__`` arguments. This configuration usually affects how the extension is used, such that it wouldn't make sense to change it per deployment. - Configuration per extension instance, through instance attributes and decorator methods. It might be more ergonomic to assign to ``ext.value``, or use a ``@ext.register`` decorator to register a function, after the extension instance has been created. - Global configuration through class attributes. Changing a class attribute like ``Ext.connection_class`` can customize default behavior without making a subclass. This could be combined per-extension configuration to override defaults. - Subclassing and overriding methods and attributes. Making the API of the extension itself something that can be overridden provides a very powerful tool for advanced customization. The :class:`~flask.Flask` object itself uses all of these techniques. It's up to you to decide what configuration is appropriate for your extension, based on what you need and what you want to support. Configuration should not be changed after the application setup phase is complete and the server begins handling requests. Configuration is global, any changes to it are not guaranteed to be visible to other workers. Data During a Request --------------------- When writing a Flask application, the :data:`~flask.g` object is used to store information during a request. For example the :doc:`tutorial ` stores a connection to a SQLite database as ``g.db``. Extensions can also use this, with some care. Since ``g`` is a single global namespace, extensions must use unique names that won't collide with user data. For example, use the extension name as a prefix, or as a namespace. .. code-block:: python # an internal prefix with the extension name g._hello_user_id = 2 # or an internal prefix as a namespace from types import SimpleNamespace g._hello = SimpleNamespace() g._hello.user_id = 2 The data in ``g`` lasts for an application context. An application context is active when a request context is, or when a CLI command is run. If you're storing something that should be closed, use :meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed when the application context ends. If it should only be valid during a request, or would not be used in the CLI outside a request, use :meth:`~flask.Flask.teardown_request`. Views and Models ---------------- Your extension views might want to interact with specific models in your database, or some other extension or data connected to your application. For example, let's consider a ``Flask-SimpleBlog`` extension that works with Flask-SQLAlchemy to provide a ``Post`` model and views to write and read posts. The ``Post`` model needs to subclass the Flask-SQLAlchemy ``db.Model`` object, but that's only available once you've created an instance of that extension, not when your extension is defining its views. So how can the view code, defined before the model exists, access the model? One method could be to use :doc:`views`. During ``__init__``, create the model, then create the views by passing the model to the view class's :meth:`~views.View.as_view` method. .. code-block:: python class PostAPI(MethodView): def __init__(self, model): self.model = model def get(self, id): post = self.model.query.get(id) return jsonify(post.to_json()) class BlogExtension: def __init__(self, db): class Post(db.Model): id = db.Column(primary_key=True) title = db.Column(db.String, nullable=False) self.post_model = Post def init_app(self, app): api_view = PostAPI.as_view(model=self.post_model) db = SQLAlchemy() blog = BlogExtension(db) db.init_app(app) blog.init_app(app) Another technique could be to use an attribute on the extension, such as ``self.post_model`` from above. Add the extension to ``app.extensions`` in ``init_app``, then access ``current_app.extensions["simple_blog"].post_model`` from views. You may also want to provide base classes so that users can provide their own ``Post`` model that conforms to the API your extension expects. So they could implement ``class Post(blog.BasePost)``, then set it as ``blog.post_model``. As you can see, this can get a bit complex. Unfortunately, there's no perfect solution here, only different strategies and tradeoffs depending on your needs and how much customization you want to offer. Luckily, this sort of resource dependency is not a common need for most extensions. Remember, if you need help with design, ask on our `Discord Chat`_ or `GitHub Discussions`_. Recommended Extension Guidelines -------------------------------- Flask previously had the concept of "approved extensions", where the Flask maintainers evaluated the quality, support, and compatibility of the extensions before listing them. While the list became too difficult to maintain over time, the guidelines are still relevant to all extensions maintained and developed today, as they help the Flask ecosystem remain consistent and compatible. 1. An extension requires a maintainer. In the event an extension author would like to move beyond the project, the project should find a new maintainer and transfer access to the repository, documentation, PyPI, and any other services. The `Pallets-Eco`_ organization on GitHub allows for community maintenance with oversight from the Pallets maintainers. 2. The naming scheme is *Flask-ExtensionName* or *ExtensionName-Flask*. It must provide exactly one package or module named ``flask_extension_name``. 3. The extension must use an open source license. The Python web ecosystem tends to prefer BSD or MIT. It must be open source and publicly available. 4. The extension's API must have the following characteristics: - It must support multiple applications running in the same Python process. Use ``current_app`` instead of ``self.app``, store configuration and state per application instance. - It must be possible to use the factory pattern for creating applications. Use the ``ext.init_app()`` pattern. 5. From a clone of the repository, an extension with its dependencies must be installable in editable mode with ``pip install -e .``. 6. It must ship tests that can be invoked with a common tool like ``tox -e py``, ``nox -s test`` or ``pytest``. If not using ``tox``, the test dependencies should be specified in a requirements file. The tests must be part of the sdist distribution. 7. A link to the documentation or project website must be in the PyPI metadata or the readme. The documentation should use the Flask theme from the `Official Pallets Themes`_. 8. The extension's dependencies should not use upper bounds or assume any particular version scheme, but should use lower bounds to indicate minimum compatibility support. For example, ``sqlalchemy>=1.4``. 9. Indicate the versions of Python supported using ``python_requires=">=version"``. Flask itself supports Python >=3.9 as of October 2024, and this will update over time. .. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask .. _Discord Chat: https://discord.gg/pallets .. _GitHub Discussions: https://github.com/pallets/flask/discussions .. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/ .. _Pallets-Eco: https://github.com/pallets-eco flask-3.1.1/docs/extensions.rst000066400000000000000000000025341501065614000164770ustar00rootroot00000000000000Extensions ========== Extensions are extra packages that add functionality to a Flask application. For example, an extension might add support for sending email or connecting to a database. Some extensions add entire new frameworks to help build certain types of applications, like a REST API. Finding Extensions ------------------ Flask extensions are usually named "Flask-Foo" or "Foo-Flask". You can search PyPI for packages tagged with `Framework :: Flask `_. Using Extensions ---------------- Consult each extension's documentation for installation, configuration, and usage instructions. Generally, extensions pull their own configuration from :attr:`app.config ` and are passed an application instance during initialization. For example, an extension called "Flask-Foo" might be used like this:: from flask_foo import Foo foo = Foo() app = Flask(__name__) app.config.update( FOO_BAR='baz', FOO_SPAM='eggs', ) foo.init_app(app) Building Extensions ------------------- While `PyPI `_ contains many Flask extensions, you may not find an extension that fits your need. If this is the case, you can create your own, and publish it for others to use as well. Read :doc:`extensiondev` to develop your own Flask extension. .. _pypi: https://pypi.org/search/?c=Framework+%3A%3A+Flask flask-3.1.1/docs/index.rst000066400000000000000000000040101501065614000153760ustar00rootroot00000000000000.. rst-class:: hide-header Welcome to Flask ================ .. image:: _static/flask-horizontal.png :align: center Welcome to Flask's documentation. Flask is a lightweight WSGI web application framework. It is designed to make getting started quick and easy, with the ability to scale up to complex applications. Get started with :doc:`installation` and then get an overview with the :doc:`quickstart`. There is also a more detailed :doc:`tutorial/index` that shows how to create a small but complete application with Flask. Common patterns are described in the :doc:`patterns/index` section. The rest of the docs describe each component of Flask in detail, with a full reference in the :doc:`api` section. Flask depends on the `Werkzeug`_ WSGI toolkit, the `Jinja`_ template engine, and the `Click`_ CLI toolkit. Be sure to check their documentation as well as Flask's when looking for information. .. _Werkzeug: https://werkzeug.palletsprojects.com .. _Jinja: https://jinja.palletsprojects.com .. _Click: https://click.palletsprojects.com User's Guide ------------ Flask provides configuration and conventions, with sensible defaults, to get started. This section of the documentation explains the different parts of the Flask framework and how they can be used, customized, and extended. Beyond Flask itself, look for community-maintained extensions to add even more functionality. .. toctree:: :maxdepth: 2 installation quickstart tutorial/index templating testing errorhandling debugging logging config signals views lifecycle appcontext reqcontext blueprints extensions cli server shell patterns/index web-security deploying/index async-await API Reference ------------- If you are looking for information on a specific function, class or method, this part of the documentation is for you. .. toctree:: :maxdepth: 2 api Additional Notes ---------------- .. toctree:: :maxdepth: 2 design extensiondev contributing license changes flask-3.1.1/docs/installation.rst000066400000000000000000000072731501065614000170060ustar00rootroot00000000000000Installation ============ Python Version -------------- We recommend using the latest version of Python. Flask supports Python 3.9 and newer. Dependencies ------------ These distributions will be installed automatically when installing Flask. * `Werkzeug`_ implements WSGI, the standard Python interface between applications and servers. * `Jinja`_ is a template language that renders the pages your application serves. * `MarkupSafe`_ comes with Jinja. It escapes untrusted input when rendering templates to avoid injection attacks. * `ItsDangerous`_ securely signs data to ensure its integrity. This is used to protect Flask's session cookie. * `Click`_ is a framework for writing command line applications. It provides the ``flask`` command and allows adding custom management commands. * `Blinker`_ provides support for :doc:`signals`. .. _Werkzeug: https://palletsprojects.com/p/werkzeug/ .. _Jinja: https://palletsprojects.com/p/jinja/ .. _MarkupSafe: https://palletsprojects.com/p/markupsafe/ .. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ .. _Click: https://palletsprojects.com/p/click/ .. _Blinker: https://blinker.readthedocs.io/ Optional dependencies ~~~~~~~~~~~~~~~~~~~~~ These distributions will not be installed automatically. Flask will detect and use them if you install them. * `python-dotenv`_ enables support for :ref:`dotenv` when running ``flask`` commands. * `Watchdog`_ provides a faster, more efficient reloader for the development server. .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme .. _watchdog: https://pythonhosted.org/watchdog/ greenlet ~~~~~~~~ You may choose to use gevent or eventlet with your application. In this case, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is required. These are not minimum supported versions, they only indicate the first versions that added necessary features. You should use the latest versions of each. Virtual environments -------------------- Use a virtual environment to manage the dependencies for your project, both in development and in production. What problem does a virtual environment solve? The more Python projects you have, the more likely it is that you need to work with different versions of Python libraries, or even Python itself. Newer versions of libraries for one project can break compatibility in another project. Virtual environments are independent groups of Python libraries, one for each project. Packages installed for one project will not affect other projects or the operating system's packages. Python comes bundled with the :mod:`venv` module to create virtual environments. .. _install-create-env: Create an environment ~~~~~~~~~~~~~~~~~~~~~ Create a project folder and a :file:`.venv` folder within: .. tabs:: .. group-tab:: macOS/Linux .. code-block:: text $ mkdir myproject $ cd myproject $ python3 -m venv .venv .. group-tab:: Windows .. code-block:: text > mkdir myproject > cd myproject > py -3 -m venv .venv .. _install-activate-env: Activate the environment ~~~~~~~~~~~~~~~~~~~~~~~~ Before you work on your project, activate the corresponding environment: .. tabs:: .. group-tab:: macOS/Linux .. code-block:: text $ . .venv/bin/activate .. group-tab:: Windows .. code-block:: text > .venv\Scripts\activate Your shell prompt will change to show the name of the activated environment. Install Flask ------------- Within the activated environment, use the following command to install Flask: .. code-block:: sh $ pip install Flask Flask is now installed. Check out the :doc:`/quickstart` or go to the :doc:`Documentation Overview `. flask-3.1.1/docs/license.rst000066400000000000000000000001421501065614000157130ustar00rootroot00000000000000BSD-3-Clause License ==================== .. literalinclude:: ../LICENSE.txt :language: text flask-3.1.1/docs/lifecycle.rst000066400000000000000000000213631501065614000162400ustar00rootroot00000000000000Application Structure and Lifecycle =================================== Flask makes it pretty easy to write a web application. But there are quite a few different parts to an application and to each request it handles. Knowing what happens during application setup, serving, and handling requests will help you know what's possible in Flask and how to structure your application. Application Setup ----------------- The first step in creating a Flask application is creating the application object. Each Flask application is an instance of the :class:`.Flask` class, which collects all configuration, extensions, and views. .. code-block:: python from flask import Flask app = Flask(__name__) app.config.from_mapping( SECRET_KEY="dev", ) app.config.from_prefixed_env() @app.route("/") def index(): return "Hello, World!" This is known as the "application setup phase", it's the code you write that's outside any view functions or other handlers. It can be split up between different modules and sub-packages, but all code that you want to be part of your application must be imported in order for it to be registered. All application setup must be completed before you start serving your application and handling requests. This is because WSGI servers divide work between multiple workers, or can be distributed across multiple machines. If the configuration changed in one worker, there's no way for Flask to ensure consistency between other workers. Flask tries to help developers catch some of these setup ordering issues by showing an error if setup-related methods are called after requests are handled. In that case you'll see this error: The setup method 'route' can no longer be called on the application. It has already handled its first request, any changes will not be applied consistently. Make sure all imports, decorators, functions, etc. needed to set up the application are done before running it. However, it is not possible for Flask to detect all cases of out-of-order setup. In general, don't do anything to modify the ``Flask`` app object and ``Blueprint`` objects from within view functions that run during requests. This includes: - Adding routes, view functions, and other request handlers with ``@app.route``, ``@app.errorhandler``, ``@app.before_request``, etc. - Registering blueprints. - Loading configuration with ``app.config``. - Setting up the Jinja template environment with ``app.jinja_env``. - Setting a session interface, instead of the default itsdangerous cookie. - Setting a JSON provider with ``app.json``, instead of the default provider. - Creating and initializing Flask extensions. Serving the Application ----------------------- Flask is a WSGI application framework. The other half of WSGI is the WSGI server. During development, Flask, through Werkzeug, provides a development WSGI server with the ``flask run`` CLI command. When you are done with development, use a production server to serve your application, see :doc:`deploying/index`. Regardless of what server you're using, it will follow the :pep:`3333` WSGI spec. The WSGI server will be told how to access your Flask application object, which is the WSGI application. Then it will start listening for HTTP requests, translate the request data into a WSGI environ, and call the WSGI application with that data. The WSGI application will return data that is translated into an HTTP response. #. Browser or other client makes HTTP request. #. WSGI server receives request. #. WSGI server converts HTTP data to WSGI ``environ`` dict. #. WSGI server calls WSGI application with the ``environ``. #. Flask, the WSGI application, does all its internal processing to route the request to a view function, handle errors, etc. #. Flask translates View function return into WSGI response data, passes it to WSGI server. #. WSGI server creates and send an HTTP response. #. Client receives the HTTP response. Middleware ~~~~~~~~~~ The WSGI application above is a callable that behaves in a certain way. Middleware is a WSGI application that wraps another WSGI application. It's a similar concept to Python decorators. The outermost middleware will be called by the server. It can modify the data passed to it, then call the WSGI application (or further middleware) that it wraps, and so on. And it can take the return value of that call and modify it further. From the WSGI server's perspective, there is one WSGI application, the one it calls directly. Typically, Flask is the "real" application at the end of the chain of middleware. But even Flask can call further WSGI applications, although that's an advanced, uncommon use case. A common middleware you'll see used with Flask is Werkzeug's :class:`~werkzeug.middleware.proxy_fix.ProxyFix`, which modifies the request to look like it came directly from a client even if it passed through HTTP proxies on the way. There are other middleware that can handle serving static files, authentication, etc. How a Request is Handled ------------------------ For us, the interesting part of the steps above is when Flask gets called by the WSGI server (or middleware). At that point, it will do quite a lot to handle the request and generate the response. At the most basic, it will match the URL to a view function, call the view function, and pass the return value back to the server. But there are many more parts that you can use to customize its behavior. #. WSGI server calls the Flask object, which calls :meth:`.Flask.wsgi_app`. #. A :class:`.RequestContext` object is created. This converts the WSGI ``environ`` dict into a :class:`.Request` object. It also creates an :class:`AppContext` object. #. The :doc:`app context ` is pushed, which makes :data:`.current_app` and :data:`.g` available. #. The :data:`.appcontext_pushed` signal is sent. #. The :doc:`request context ` is pushed, which makes :attr:`.request` and :class:`.session` available. #. The session is opened, loading any existing session data using the app's :attr:`~.Flask.session_interface`, an instance of :class:`.SessionInterface`. #. The URL is matched against the URL rules registered with the :meth:`~.Flask.route` decorator during application setup. If there is no match, the error - usually a 404, 405, or redirect - is stored to be handled later. #. The :data:`.request_started` signal is sent. #. Any :meth:`~.Flask.url_value_preprocessor` decorated functions are called. #. Any :meth:`~.Flask.before_request` decorated functions are called. If any of these function returns a value it is treated as the response immediately. #. If the URL didn't match a route a few steps ago, that error is raised now. #. The :meth:`~.Flask.route` decorated view function associated with the matched URL is called and returns a value to be used as the response. #. If any step so far raised an exception, and there is an :meth:`~.Flask.errorhandler` decorated function that matches the exception class or HTTP error code, it is called to handle the error and return a response. #. Whatever returned a response value - a before request function, the view, or an error handler, that value is converted to a :class:`.Response` object. #. Any :func:`~.after_this_request` decorated functions are called, then cleared. #. Any :meth:`~.Flask.after_request` decorated functions are called, which can modify the response object. #. The session is saved, persisting any modified session data using the app's :attr:`~.Flask.session_interface`. #. The :data:`.request_finished` signal is sent. #. If any step so far raised an exception, and it was not handled by an error handler function, it is handled now. HTTP exceptions are treated as responses with their corresponding status code, other exceptions are converted to a generic 500 response. The :data:`.got_request_exception` signal is sent. #. The response object's status, headers, and body are returned to the WSGI server. #. Any :meth:`~.Flask.teardown_request` decorated functions are called. #. The :data:`.request_tearing_down` signal is sent. #. The request context is popped, :attr:`.request` and :class:`.session` are no longer available. #. Any :meth:`~.Flask.teardown_appcontext` decorated functions are called. #. The :data:`.appcontext_tearing_down` signal is sent. #. The app context is popped, :data:`.current_app` and :data:`.g` are no longer available. #. The :data:`.appcontext_popped` signal is sent. There are even more decorators and customization points than this, but that aren't part of every request lifecycle. They're more specific to certain things you might use during a request, such as templates, building URLs, or handling JSON data. See the rest of this documentation, as well as the :doc:`api` to explore further. flask-3.1.1/docs/logging.rst000066400000000000000000000134471501065614000157330ustar00rootroot00000000000000Logging ======= Flask uses standard Python :mod:`logging`. Messages about your Flask application are logged with :meth:`app.logger `, which takes the same name as :attr:`app.name `. This logger can also be used to log your own messages. .. code-block:: python @app.route('/login', methods=['POST']) def login(): user = get_user(request.form['username']) if user.check_password(request.form['password']): login_user(user) app.logger.info('%s logged in successfully', user.username) return redirect(url_for('index')) else: app.logger.info('%s failed to log in', user.username) abort(401) If you don't configure logging, Python's default log level is usually 'warning'. Nothing below the configured level will be visible. Basic Configuration ------------------- When you want to configure logging for your project, you should do it as soon as possible when the program starts. If :meth:`app.logger ` is accessed before logging is configured, it will add a default handler. If possible, configure logging before creating the application object. This example uses :func:`~logging.config.dictConfig` to create a logging configuration similar to Flask's default, except for all logs:: from logging.config import dictConfig dictConfig({ 'version': 1, 'formatters': {'default': { 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', }}, 'handlers': {'wsgi': { 'class': 'logging.StreamHandler', 'stream': 'ext://flask.logging.wsgi_errors_stream', 'formatter': 'default' }}, 'root': { 'level': 'INFO', 'handlers': ['wsgi'] } }) app = Flask(__name__) Default Configuration ````````````````````` If you do not configure logging yourself, Flask will add a :class:`~logging.StreamHandler` to :meth:`app.logger ` automatically. During requests, it will write to the stream specified by the WSGI server in ``environ['wsgi.errors']`` (which is usually :data:`sys.stderr`). Outside a request, it will log to :data:`sys.stderr`. Removing the Default Handler ```````````````````````````` If you configured logging after accessing :meth:`app.logger `, and need to remove the default handler, you can import and remove it:: from flask.logging import default_handler app.logger.removeHandler(default_handler) Email Errors to Admins ---------------------- When running the application on a remote server for production, you probably won't be looking at the log messages very often. The WSGI server will probably send log messages to a file, and you'll only check that file if a user tells you something went wrong. To be proactive about discovering and fixing bugs, you can configure a :class:`logging.handlers.SMTPHandler` to send an email when errors and higher are logged. :: import logging from logging.handlers import SMTPHandler mail_handler = SMTPHandler( mailhost='127.0.0.1', fromaddr='server-error@example.com', toaddrs=['admin@example.com'], subject='Application Error' ) mail_handler.setLevel(logging.ERROR) mail_handler.setFormatter(logging.Formatter( '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' )) if not app.debug: app.logger.addHandler(mail_handler) This requires that you have an SMTP server set up on the same server. See the Python docs for more information about configuring the handler. Injecting Request Information ----------------------------- Seeing more information about the request, such as the IP address, may help debugging some errors. You can subclass :class:`logging.Formatter` to inject your own fields that can be used in messages. You can change the formatter for Flask's default handler, the mail handler defined above, or any other handler. :: from flask import has_request_context, request from flask.logging import default_handler class RequestFormatter(logging.Formatter): def format(self, record): if has_request_context(): record.url = request.url record.remote_addr = request.remote_addr else: record.url = None record.remote_addr = None return super().format(record) formatter = RequestFormatter( '[%(asctime)s] %(remote_addr)s requested %(url)s\n' '%(levelname)s in %(module)s: %(message)s' ) default_handler.setFormatter(formatter) mail_handler.setFormatter(formatter) Other Libraries --------------- Other libraries may use logging extensively, and you want to see relevant messages from those logs too. The simplest way to do this is to add handlers to the root logger instead of only the app logger. :: from flask.logging import default_handler root = logging.getLogger() root.addHandler(default_handler) root.addHandler(mail_handler) Depending on your project, it may be more useful to configure each logger you care about separately, instead of configuring only the root logger. :: for logger in ( logging.getLogger(app.name), logging.getLogger('sqlalchemy'), logging.getLogger('other_package'), ): logger.addHandler(default_handler) logger.addHandler(mail_handler) Werkzeug ```````` Werkzeug logs basic request/response information to the ``'werkzeug'`` logger. If the root logger has no handlers configured, Werkzeug adds a :class:`~logging.StreamHandler` to its logger. Flask Extensions ```````````````` Depending on the situation, an extension may choose to log to :meth:`app.logger ` or its own named logger. Consult each extension's documentation for details. flask-3.1.1/docs/make.bat000066400000000000000000000013701501065614000151500ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd flask-3.1.1/docs/patterns/000077500000000000000000000000001501065614000154025ustar00rootroot00000000000000flask-3.1.1/docs/patterns/appdispatch.rst000066400000000000000000000145531501065614000204440ustar00rootroot00000000000000Application Dispatching ======================= Application dispatching is the process of combining multiple Flask applications on the WSGI level. You can combine not only Flask applications but any WSGI application. This would allow you to run a Django and a Flask application in the same interpreter side by side if you want. The usefulness of this depends on how the applications work internally. The fundamental difference from :doc:`packages` is that in this case you are running the same or different Flask applications that are entirely isolated from each other. They run different configurations and are dispatched on the WSGI level. Working with this Document -------------------------- Each of the techniques and examples below results in an ``application`` object that can be run with any WSGI server. For development, use the ``flask run`` command to start a development server. For production, see :doc:`/deploying/index`. .. code-block:: python from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello World!' Combining Applications ---------------------- If you have entirely separated applications and you want them to work next to each other in the same Python interpreter process you can take advantage of the :class:`werkzeug.wsgi.DispatcherMiddleware`. The idea here is that each Flask application is a valid WSGI application and they are combined by the dispatcher middleware into a larger one that is dispatched based on prefix. For example you could have your main application run on ``/`` and your backend interface on ``/backend``. .. code-block:: python from werkzeug.middleware.dispatcher import DispatcherMiddleware from frontend_app import application as frontend from backend_app import application as backend application = DispatcherMiddleware(frontend, { '/backend': backend }) Dispatch by Subdomain --------------------- Sometimes you might want to use multiple instances of the same application with different configurations. Assuming the application is created inside a function and you can call that function to instantiate it, that is really easy to implement. In order to develop your application to support creating new instances in functions have a look at the :doc:`appfactories` pattern. A very common example would be creating applications per subdomain. For instance you configure your webserver to dispatch all requests for all subdomains to your application and you then use the subdomain information to create user-specific instances. Once you have your server set up to listen on all subdomains you can use a very simple WSGI application to do the dynamic application creation. The perfect level for abstraction in that regard is the WSGI layer. You write your own WSGI application that looks at the request that comes and delegates it to your Flask application. If that application does not exist yet, it is dynamically created and remembered. .. code-block:: python from threading import Lock class SubdomainDispatcher: def __init__(self, domain, create_app): self.domain = domain self.create_app = create_app self.lock = Lock() self.instances = {} def get_application(self, host): host = host.split(':')[0] assert host.endswith(self.domain), 'Configuration error' subdomain = host[:-len(self.domain)].rstrip('.') with self.lock: app = self.instances.get(subdomain) if app is None: app = self.create_app(subdomain) self.instances[subdomain] = app return app def __call__(self, environ, start_response): app = self.get_application(environ['HTTP_HOST']) return app(environ, start_response) This dispatcher can then be used like this: .. code-block:: python from myapplication import create_app, get_user_for_subdomain from werkzeug.exceptions import NotFound def make_app(subdomain): user = get_user_for_subdomain(subdomain) if user is None: # if there is no user for that subdomain we still have # to return a WSGI application that handles that request. # We can then just return the NotFound() exception as # application which will render a default 404 page. # You might also redirect the user to the main page then return NotFound() # otherwise create the application for the specific user return create_app(user) application = SubdomainDispatcher('example.com', make_app) Dispatch by Path ---------------- Dispatching by a path on the URL is very similar. Instead of looking at the ``Host`` header to figure out the subdomain one simply looks at the request path up to the first slash. .. code-block:: python from threading import Lock from wsgiref.util import shift_path_info class PathDispatcher: def __init__(self, default_app, create_app): self.default_app = default_app self.create_app = create_app self.lock = Lock() self.instances = {} def get_application(self, prefix): with self.lock: app = self.instances.get(prefix) if app is None: app = self.create_app(prefix) if app is not None: self.instances[prefix] = app return app def __call__(self, environ, start_response): app = self.get_application(_peek_path_info(environ)) if app is not None: shift_path_info(environ) else: app = self.default_app return app(environ, start_response) def _peek_path_info(environ): segments = environ.get("PATH_INFO", "").lstrip("/").split("/", 1) if segments: return segments[0] return None The big difference between this and the subdomain one is that this one falls back to another application if the creator function returns ``None``. .. code-block:: python from myapplication import create_app, default_app, get_user_for_prefix def make_app(prefix): user = get_user_for_prefix(prefix) if user is not None: return create_app(user) application = PathDispatcher(default_app, make_app) flask-3.1.1/docs/patterns/appfactories.rst000066400000000000000000000100021501065614000206050ustar00rootroot00000000000000Application Factories ===================== If you are already using packages and blueprints for your application (:doc:`/blueprints`) there are a couple of really nice ways to further improve the experience. A common pattern is creating the application object when the blueprint is imported. But if you move the creation of this object into a function, you can then create multiple instances of this app later. So why would you want to do this? 1. Testing. You can have instances of the application with different settings to test every case. 2. Multiple instances. Imagine you want to run different versions of the same application. Of course you could have multiple instances with different configs set up in your webserver, but if you use factories, you can have multiple instances of the same application running in the same application process which can be handy. So how would you then actually implement that? Basic Factories --------------- The idea is to set up the application in a function. Like this:: def create_app(config_filename): app = Flask(__name__) app.config.from_pyfile(config_filename) from yourapplication.model import db db.init_app(app) from yourapplication.views.admin import admin from yourapplication.views.frontend import frontend app.register_blueprint(admin) app.register_blueprint(frontend) return app The downside is that you cannot use the application object in the blueprints at import time. You can however use it from within a request. How do you get access to the application with the config? Use :data:`~flask.current_app`:: from flask import current_app, Blueprint, render_template admin = Blueprint('admin', __name__, url_prefix='/admin') @admin.route('/') def index(): return render_template(current_app.config['INDEX_TEMPLATE']) Here we look up the name of a template in the config. Factories & Extensions ---------------------- It's preferable to create your extensions and app factories so that the extension object does not initially get bound to the application. Using `Flask-SQLAlchemy `_, as an example, you should not do something along those lines:: def create_app(config_filename): app = Flask(__name__) app.config.from_pyfile(config_filename) db = SQLAlchemy(app) But, rather, in model.py (or equivalent):: db = SQLAlchemy() and in your application.py (or equivalent):: def create_app(config_filename): app = Flask(__name__) app.config.from_pyfile(config_filename) from yourapplication.model import db db.init_app(app) Using this design pattern, no application-specific state is stored on the extension object, so one extension object can be used for multiple apps. For more information about the design of extensions refer to :doc:`/extensiondev`. Using Applications ------------------ To run such an application, you can use the :command:`flask` command: .. code-block:: text $ flask --app hello run Flask will automatically detect the factory if it is named ``create_app`` or ``make_app`` in ``hello``. You can also pass arguments to the factory like this: .. code-block:: text $ flask --app 'hello:create_app(local_auth=True)' run Then the ``create_app`` factory in ``hello`` is called with the keyword argument ``local_auth=True``. See :doc:`/cli` for more detail. Factory Improvements -------------------- The factory function above is not very clever, but you can improve it. The following changes are straightforward to implement: 1. Make it possible to pass in configuration values for unit tests so that you don't have to create config files on the filesystem. 2. Call a function from a blueprint when the application is setting up so that you have a place to modify attributes of the application (like hooking in before/after request handlers etc.) 3. Add in WSGI middlewares when the application is being created if necessary. flask-3.1.1/docs/patterns/caching.rst000066400000000000000000000012151501065614000175270ustar00rootroot00000000000000Caching ======= When your application runs slow, throw some caches in. Well, at least it's the easiest way to speed up things. What does a cache do? Say you have a function that takes some time to complete but the results would still be good enough if they were 5 minutes old. So then the idea is that you actually put the result of that calculation into a cache for some time. Flask itself does not provide caching for you, but `Flask-Caching`_, an extension for Flask does. Flask-Caching supports various backends, and it is even possible to develop your own caching backend. .. _Flask-Caching: https://flask-caching.readthedocs.io/en/latest/ flask-3.1.1/docs/patterns/celery.rst000066400000000000000000000207361501065614000174270ustar00rootroot00000000000000Background Tasks with Celery ============================ If your application has a long running task, such as processing some uploaded data or sending email, you don't want to wait for it to finish during a request. Instead, use a task queue to send the necessary data to another process that will run the task in the background while the request returns immediately. `Celery`_ is a powerful task queue that can be used for simple background tasks as well as complex multi-stage programs and schedules. This guide will show you how to configure Celery using Flask. Read Celery's `First Steps with Celery`_ guide to learn how to use Celery itself. .. _Celery: https://celery.readthedocs.io .. _First Steps with Celery: https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html The Flask repository contains `an example `_ based on the information on this page, which also shows how to use JavaScript to submit tasks and poll for progress and results. Install ------- Install Celery from PyPI, for example using pip: .. code-block:: text $ pip install celery Integrate Celery with Flask --------------------------- You can use Celery without any integration with Flask, but it's convenient to configure it through Flask's config, and to let tasks access the Flask application. Celery uses similar ideas to Flask, with a ``Celery`` app object that has configuration and registers tasks. While creating a Flask app, use the following code to create and configure a Celery app as well. .. code-block:: python from celery import Celery, Task def celery_init_app(app: Flask) -> Celery: class FlaskTask(Task): def __call__(self, *args: object, **kwargs: object) -> object: with app.app_context(): return self.run(*args, **kwargs) celery_app = Celery(app.name, task_cls=FlaskTask) celery_app.config_from_object(app.config["CELERY"]) celery_app.set_default() app.extensions["celery"] = celery_app return celery_app This creates and returns a ``Celery`` app object. Celery `configuration`_ is taken from the ``CELERY`` key in the Flask configuration. The Celery app is set as the default, so that it is seen during each request. The ``Task`` subclass automatically runs task functions with a Flask app context active, so that services like your database connections are available. .. _configuration: https://celery.readthedocs.io/en/stable/userguide/configuration.html Here's a basic ``example.py`` that configures Celery to use Redis for communication. We enable a result backend, but ignore results by default. This allows us to store results only for tasks where we care about the result. .. code-block:: python from flask import Flask app = Flask(__name__) app.config.from_mapping( CELERY=dict( broker_url="redis://localhost", result_backend="redis://localhost", task_ignore_result=True, ), ) celery_app = celery_init_app(app) Point the ``celery worker`` command at this and it will find the ``celery_app`` object. .. code-block:: text $ celery -A example worker --loglevel INFO You can also run the ``celery beat`` command to run tasks on a schedule. See Celery's docs for more information about defining schedules. .. code-block:: text $ celery -A example beat --loglevel INFO Application Factory ------------------- When using the Flask application factory pattern, call the ``celery_init_app`` function inside the factory. It sets ``app.extensions["celery"]`` to the Celery app object, which can be used to get the Celery app from the Flask app returned by the factory. .. code-block:: python def create_app() -> Flask: app = Flask(__name__) app.config.from_mapping( CELERY=dict( broker_url="redis://localhost", result_backend="redis://localhost", task_ignore_result=True, ), ) app.config.from_prefixed_env() celery_init_app(app) return app To use ``celery`` commands, Celery needs an app object, but that's no longer directly available. Create a ``make_celery.py`` file that calls the Flask app factory and gets the Celery app from the returned Flask app. .. code-block:: python from example import create_app flask_app = create_app() celery_app = flask_app.extensions["celery"] Point the ``celery`` command to this file. .. code-block:: text $ celery -A make_celery worker --loglevel INFO $ celery -A make_celery beat --loglevel INFO Defining Tasks -------------- Using ``@celery_app.task`` to decorate task functions requires access to the ``celery_app`` object, which won't be available when using the factory pattern. It also means that the decorated tasks are tied to the specific Flask and Celery app instances, which could be an issue during testing if you change configuration for a test. Instead, use Celery's ``@shared_task`` decorator. This creates task objects that will access whatever the "current app" is, which is a similar concept to Flask's blueprints and app context. This is why we called ``celery_app.set_default()`` above. Here's an example task that adds two numbers together and returns the result. .. code-block:: python from celery import shared_task @shared_task(ignore_result=False) def add_together(a: int, b: int) -> int: return a + b Earlier, we configured Celery to ignore task results by default. Since we want to know the return value of this task, we set ``ignore_result=False``. On the other hand, a task that didn't need a result, such as sending an email, wouldn't set this. Calling Tasks ------------- The decorated function becomes a task object with methods to call it in the background. The simplest way is to use the ``delay(*args, **kwargs)`` method. See Celery's docs for more methods. A Celery worker must be running to run the task. Starting a worker is shown in the previous sections. .. code-block:: python from flask import request @app.post("/add") def start_add() -> dict[str, object]: a = request.form.get("a", type=int) b = request.form.get("b", type=int) result = add_together.delay(a, b) return {"result_id": result.id} The route doesn't get the task's result immediately. That would defeat the purpose by blocking the response. Instead, we return the running task's result id, which we can use later to get the result. Getting Results --------------- To fetch the result of the task we started above, we'll add another route that takes the result id we returned before. We return whether the task is finished (ready), whether it finished successfully, and what the return value (or error) was if it is finished. .. code-block:: python from celery.result import AsyncResult @app.get("/result/") def task_result(id: str) -> dict[str, object]: result = AsyncResult(id) return { "ready": result.ready(), "successful": result.successful(), "value": result.result if result.ready() else None, } Now you can start the task using the first route, then poll for the result using the second route. This keeps the Flask request workers from being blocked waiting for tasks to finish. The Flask repository contains `an example `_ using JavaScript to submit tasks and poll for progress and results. Passing Data to Tasks --------------------- The "add" task above took two integers as arguments. To pass arguments to tasks, Celery has to serialize them to a format that it can pass to other processes. Therefore, passing complex objects is not recommended. For example, it would be impossible to pass a SQLAlchemy model object, since that object is probably not serializable and is tied to the session that queried it. Pass the minimal amount of data necessary to fetch or recreate any complex data within the task. Consider a task that will run when the logged in user asks for an archive of their data. The Flask request knows the logged in user, and has the user object queried from the database. It got that by querying the database for a given id, so the task can do the same thing. Pass the user's id rather than the user object. .. code-block:: python @shared_task def generate_user_archive(user_id: str) -> None: user = db.session.get(User, user_id) ... generate_user_archive.delay(current_user.id) flask-3.1.1/docs/patterns/deferredcallbacks.rst000066400000000000000000000034701501065614000215600ustar00rootroot00000000000000Deferred Request Callbacks ========================== One of the design principles of Flask is that response objects are created and passed down a chain of potential callbacks that can modify them or replace them. When the request handling starts, there is no response object yet. It is created as necessary either by a view function or by some other component in the system. What happens if you want to modify the response at a point where the response does not exist yet? A common example for that would be a :meth:`~flask.Flask.before_request` callback that wants to set a cookie on the response object. One way is to avoid the situation. Very often that is possible. For instance you can try to move that logic into a :meth:`~flask.Flask.after_request` callback instead. However, sometimes moving code there makes it more complicated or awkward to reason about. As an alternative, you can use :func:`~flask.after_this_request` to register callbacks that will execute after only the current request. This way you can defer code execution from anywhere in the application, based on the current request. At any time during a request, we can register a function to be called at the end of the request. For example you can remember the current language of the user in a cookie in a :meth:`~flask.Flask.before_request` callback:: from flask import request, after_this_request @app.before_request def detect_user_language(): language = request.cookies.get('user_lang') if language is None: language = guess_language_from_request() # when the response exists, set a cookie with the language @after_this_request def remember_language(response): response.set_cookie('user_lang', language) return response g.language = language flask-3.1.1/docs/patterns/favicon.rst000066400000000000000000000040651501065614000175660ustar00rootroot00000000000000Adding a favicon ================ A "favicon" is an icon used by browsers for tabs and bookmarks. This helps to distinguish your website and to give it a unique brand. A common question is how to add a favicon to a Flask application. First, of course, you need an icon. It should be 16 × 16 pixels and in the ICO file format. This is not a requirement but a de-facto standard supported by all relevant browsers. Put the icon in your static directory as :file:`favicon.ico`. Now, to get browsers to find your icon, the correct way is to add a link tag in your HTML. So, for example: .. sourcecode:: html+jinja That's all you need for most browsers, however some really old ones do not support this standard. The old de-facto standard is to serve this file, with this name, at the website root. If your application is not mounted at the root path of the domain you either need to configure the web server to serve the icon at the root or if you can't do that you're out of luck. If however your application is the root you can simply route a redirect:: app.add_url_rule( "/favicon.ico", endpoint="favicon", redirect_to=url_for("static", filename="favicon.ico"), ) If you want to save the extra redirect request you can also write a view using :func:`~flask.send_from_directory`:: import os from flask import send_from_directory @app.route('/favicon.ico') def favicon(): return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') We can leave out the explicit mimetype and it will be guessed, but we may as well specify it to avoid the extra guessing, as it will always be the same. The above will serve the icon via your application and if possible it's better to configure your dedicated web server to serve it; refer to the web server's documentation. See also -------- * The `Favicon `_ article on Wikipedia flask-3.1.1/docs/patterns/fileuploads.rst000066400000000000000000000162011501065614000204430ustar00rootroot00000000000000Uploading Files =============== Ah yes, the good old problem of file uploads. The basic idea of file uploads is actually quite simple. It basically works like this: 1. A ``

`` tag is marked with ``enctype=multipart/form-data`` and an ```` is placed in that form. 2. The application accesses the file from the :attr:`~flask.request.files` dictionary on the request object. 3. use the :meth:`~werkzeug.datastructures.FileStorage.save` method of the file to save the file permanently somewhere on the filesystem. A Gentle Introduction --------------------- Let's start with a very basic application that uploads a file to a specific upload folder and displays a file to the user. Let's look at the bootstrapping code for our application:: import os from flask import Flask, flash, request, redirect, url_for from werkzeug.utils import secure_filename UPLOAD_FOLDER = '/path/to/the/uploads' ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'} app = Flask(__name__) app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER So first we need a couple of imports. Most should be straightforward, the :func:`werkzeug.secure_filename` is explained a little bit later. The ``UPLOAD_FOLDER`` is where we will store the uploaded files and the ``ALLOWED_EXTENSIONS`` is the set of allowed file extensions. Why do we limit the extensions that are allowed? You probably don't want your users to be able to upload everything there if the server is directly sending out the data to the client. That way you can make sure that users are not able to upload HTML files that would cause XSS problems (see :ref:`security-xss`). Also make sure to disallow ``.php`` files if the server executes them, but who has PHP installed on their server, right? :) Next the functions that check if an extension is valid and that uploads the file and redirects the user to the URL for the uploaded file:: def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route('/', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': # check if the post request has the file part if 'file' not in request.files: flash('No file part') return redirect(request.url) file = request.files['file'] # If the user does not select a file, the browser submits an # empty file without a filename. if file.filename == '': flash('No selected file') return redirect(request.url) if file and allowed_file(file.filename): filename = secure_filename(file.filename) file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) return redirect(url_for('download_file', name=filename)) return ''' Upload new File

Upload new File

''' So what does that :func:`~werkzeug.utils.secure_filename` function actually do? Now the problem is that there is that principle called "never trust user input". This is also true for the filename of an uploaded file. All submitted form data can be forged, and filenames can be dangerous. For the moment just remember: always use that function to secure a filename before storing it directly on the filesystem. .. admonition:: Information for the Pros So you're interested in what that :func:`~werkzeug.utils.secure_filename` function does and what the problem is if you're not using it? So just imagine someone would send the following information as `filename` to your application:: filename = "../../../../home/username/.bashrc" Assuming the number of ``../`` is correct and you would join this with the ``UPLOAD_FOLDER`` the user might have the ability to modify a file on the server's filesystem he or she should not modify. This does require some knowledge about how the application looks like, but trust me, hackers are patient :) Now let's look how that function works: >>> secure_filename('../../../../home/username/.bashrc') 'home_username_.bashrc' We want to be able to serve the uploaded files so they can be downloaded by users. We'll define a ``download_file`` view to serve files in the upload folder by name. ``url_for("download_file", name=name)`` generates download URLs. .. code-block:: python from flask import send_from_directory @app.route('/uploads/') def download_file(name): return send_from_directory(app.config["UPLOAD_FOLDER"], name) If you're using middleware or the HTTP server to serve files, you can register the ``download_file`` endpoint as ``build_only`` so ``url_for`` will work without a view function. .. code-block:: python app.add_url_rule( "/uploads/", endpoint="download_file", build_only=True ) Improving Uploads ----------------- .. versionadded:: 0.6 So how exactly does Flask handle uploads? Well it will store them in the webserver's memory if the files are reasonably small, otherwise in a temporary location (as returned by :func:`tempfile.gettempdir`). But how do you specify the maximum file size after which an upload is aborted? By default Flask will happily accept file uploads with an unlimited amount of memory, but you can limit that by setting the ``MAX_CONTENT_LENGTH`` config key:: from flask import Flask, Request app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000 The code above will limit the maximum allowed payload to 16 megabytes. If a larger file is transmitted, Flask will raise a :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception. .. admonition:: Connection Reset Issue When using the local development server, you may get a connection reset error instead of a 413 response. You will get the correct status response when running the app with a production WSGI server. This feature was added in Flask 0.6 but can be achieved in older versions as well by subclassing the request object. For more information on that consult the Werkzeug documentation on file handling. Upload Progress Bars -------------------- A while ago many developers had the idea to read the incoming file in small chunks and store the upload progress in the database to be able to poll the progress with JavaScript from the client. The client asks the server every 5 seconds how much it has transmitted, but this is something it should already know. An Easier Solution ------------------ Now there are better solutions that work faster and are more reliable. There are JavaScript libraries like jQuery_ that have form plugins to ease the construction of progress bar. Because the common pattern for file uploads exists almost unchanged in all applications dealing with uploads, there are also some Flask extensions that implement a full fledged upload mechanism that allows controlling which file extensions are allowed to be uploaded. .. _jQuery: https://jquery.com/ flask-3.1.1/docs/patterns/flashing.rst000066400000000000000000000106701501065614000177330ustar00rootroot00000000000000Message Flashing ================ Good applications and user interfaces are all about feedback. If the user does not get enough feedback they will probably end up hating the application. Flask provides a really simple way to give feedback to a user with the flashing system. The flashing system basically makes it possible to record a message at the end of a request and access it next request and only next request. This is usually combined with a layout template that does this. Note that browsers and sometimes web servers enforce a limit on cookie sizes. This means that flashing messages that are too large for session cookies causes message flashing to fail silently. Simple Flashing --------------- So here is a full example:: from flask import Flask, flash, redirect, render_template, \ request, url_for app = Flask(__name__) app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' @app.route('/') def index(): return render_template('index.html') @app.route('/login', methods=['GET', 'POST']) def login(): error = None if request.method == 'POST': if request.form['username'] != 'admin' or \ request.form['password'] != 'secret': error = 'Invalid credentials' else: flash('You were successfully logged in') return redirect(url_for('index')) return render_template('login.html', error=error) And here is the :file:`layout.html` template which does the magic: .. sourcecode:: html+jinja My Application {% with messages = get_flashed_messages() %} {% if messages %}
    {% for message in messages %}
  • {{ message }}
  • {% endfor %}
{% endif %} {% endwith %} {% block body %}{% endblock %} Here is the :file:`index.html` template which inherits from :file:`layout.html`: .. sourcecode:: html+jinja {% extends "layout.html" %} {% block body %}

Overview

Do you want to log in? {% endblock %} And here is the :file:`login.html` template which also inherits from :file:`layout.html`: .. sourcecode:: html+jinja {% extends "layout.html" %} {% block body %}

Login

{% if error %}

Error: {{ error }} {% endif %}

Username:
Password:

{% endblock %} Flashing With Categories ------------------------ .. versionadded:: 0.3 It is also possible to provide categories when flashing a message. The default category if nothing is provided is ``'message'``. Alternative categories can be used to give the user better feedback. For example error messages could be displayed with a red background. To flash a message with a different category, just use the second argument to the :func:`~flask.flash` function:: flash('Invalid password provided', 'error') Inside the template you then have to tell the :func:`~flask.get_flashed_messages` function to also return the categories. The loop looks slightly different in that situation then: .. sourcecode:: html+jinja {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
    {% for category, message in messages %}
  • {{ message }}
  • {% endfor %}
{% endif %} {% endwith %} This is just one example of how to render these flashed messages. One might also use the category to add a prefix such as ``Error:`` to the message. Filtering Flash Messages ------------------------ .. versionadded:: 0.9 Optionally you can pass a list of categories which filters the results of :func:`~flask.get_flashed_messages`. This is useful if you wish to render each category in a separate block. .. sourcecode:: html+jinja {% with errors = get_flashed_messages(category_filter=["error"]) %} {% if errors %}
×
    {%- for msg in errors %}
  • {{ msg }}
  • {% endfor -%}
{% endif %} {% endwith %} flask-3.1.1/docs/patterns/index.rst000066400000000000000000000017011501065614000172420ustar00rootroot00000000000000Patterns for Flask ================== Certain features and interactions are common enough that you will find them in most web applications. For example, many applications use a relational database and user authentication. They will open a database connection at the beginning of the request and get the information for the logged in user. At the end of the request, the database connection is closed. These types of patterns may be a bit outside the scope of Flask itself, but Flask makes it easy to implement them. Some common patterns are collected in the following pages. .. toctree:: :maxdepth: 2 packages appfactories appdispatch urlprocessors sqlite3 sqlalchemy fileuploads caching viewdecorators wtforms templateinheritance flashing javascript lazyloading mongoengine favicon streaming deferredcallbacks methodoverrides requestchecksum celery subclassing singlepageapplications flask-3.1.1/docs/patterns/javascript.rst000066400000000000000000000214041501065614000203030ustar00rootroot00000000000000JavaScript, ``fetch``, and JSON =============================== You may want to make your HTML page dynamic, by changing data without reloading the entire page. Instead of submitting an HTML ``
`` and performing a redirect to re-render the template, you can add `JavaScript`_ that calls |fetch|_ and replaces content on the page. |fetch|_ is the modern, built-in JavaScript solution to making requests from a page. You may have heard of other "AJAX" methods and libraries, such as |XHR|_ or `jQuery`_. These are no longer needed in modern browsers, although you may choose to use them or another library depending on your application's requirements. These docs will only focus on built-in JavaScript features. .. _JavaScript: https://developer.mozilla.org/Web/JavaScript .. |fetch| replace:: ``fetch()`` .. _fetch: https://developer.mozilla.org/Web/API/Fetch_API .. |XHR| replace:: ``XMLHttpRequest()`` .. _XHR: https://developer.mozilla.org/Web/API/XMLHttpRequest .. _jQuery: https://jquery.com/ Rendering Templates ------------------- It is important to understand the difference between templates and JavaScript. Templates are rendered on the server, before the response is sent to the user's browser. JavaScript runs in the user's browser, after the template is rendered and sent. Therefore, it is impossible to use JavaScript to affect how the Jinja template is rendered, but it is possible to render data into the JavaScript that will run. To provide data to JavaScript when rendering the template, use the :func:`~jinja-filters.tojson` filter in a `` A less common pattern is to add the data to a ``data-`` attribute on an HTML tag. In this case, you must use single quotes around the value, not double quotes, otherwise you will produce invalid or unsafe HTML. .. code-block:: jinja
Generating URLs --------------- The other way to get data from the server to JavaScript is to make a request for it. First, you need to know the URL to request. The simplest way to generate URLs is to continue to use :func:`~flask.url_for` when rendering the template. For example: .. code-block:: javascript const user_url = {{ url_for("user", id=current_user.id)|tojson }} fetch(user_url).then(...) However, you might need to generate a URL based on information you only know in JavaScript. As discussed above, JavaScript runs in the user's browser, not as part of the template rendering, so you can't use ``url_for`` at that point. In this case, you need to know the "root URL" under which your application is served. In simple setups, this is ``/``, but it might also be something else, like ``https://example.com/myapp/``. A simple way to tell your JavaScript code about this root is to set it as a global variable when rendering the template. Then you can use it when generating URLs from JavaScript. .. code-block:: javascript const SCRIPT_ROOT = {{ request.script_root|tojson }} let user_id = ... // do something to get a user id from the page let user_url = `${SCRIPT_ROOT}/user/${user_id}` fetch(user_url).then(...) Making a Request with ``fetch`` ------------------------------- |fetch|_ takes two arguments, a URL and an object with other options, and returns a |Promise|_. We won't cover all the available options, and will only use ``then()`` on the promise, not other callbacks or ``await`` syntax. Read the linked MDN docs for more information about those features. By default, the GET method is used. If the response contains JSON, it can be used with a ``then()`` callback chain. .. code-block:: javascript const room_url = {{ url_for("room_detail", id=room.id)|tojson }} fetch(room_url) .then(response => response.json()) .then(data => { // data is a parsed JSON object }) To send data, use a data method such as POST, and pass the ``body`` option. The most common types for data are form data or JSON data. To send form data, pass a populated |FormData|_ object. This uses the same format as an HTML form, and would be accessed with ``request.form`` in a Flask view. .. code-block:: javascript let data = new FormData() data.append("name", "Flask Room") data.append("description", "Talk about Flask here.") fetch(room_url, { "method": "POST", "body": data, }).then(...) In general, prefer sending request data as form data, as would be used when submitting an HTML form. JSON can represent more complex data, but unless you need that it's better to stick with the simpler format. When sending JSON data, the ``Content-Type: application/json`` header must be sent as well, otherwise Flask will return a 400 error. .. code-block:: javascript let data = { "name": "Flask Room", "description": "Talk about Flask here.", } fetch(room_url, { "method": "POST", "headers": {"Content-Type": "application/json"}, "body": JSON.stringify(data), }).then(...) .. |Promise| replace:: ``Promise`` .. _Promise: https://developer.mozilla.org/Web/JavaScript/Reference/Global_Objects/Promise .. |FormData| replace:: ``FormData`` .. _FormData: https://developer.mozilla.org/en-US/docs/Web/API/FormData Following Redirects ------------------- A response might be a redirect, for example if you logged in with JavaScript instead of a traditional HTML form, and your view returned a redirect instead of JSON. JavaScript requests do follow redirects, but they don't change the page. If you want to make the page change you can inspect the response and apply the redirect manually. .. code-block:: javascript fetch("/login", {"body": ...}).then( response => { if (response.redirected) { window.location = response.url } else { showLoginError() } } ) Replacing Content ----------------- A response might be new HTML, either a new section of the page to add or replace, or an entirely new page. In general, if you're returning the entire page, it would be better to handle that with a redirect as shown in the previous section. The following example shows how to replace a ``
`` with the HTML returned by a request. .. code-block:: html
{{ include "geology_fact.html" }}
Return JSON from Views ---------------------- To return a JSON object from your API view, you can directly return a dict from the view. It will be serialized to JSON automatically. .. code-block:: python @app.route("/user/") def user_detail(id): user = User.query.get_or_404(id) return { "username": User.username, "email": User.email, "picture": url_for("static", filename=f"users/{id}/profile.png"), } If you want to return another JSON type, use the :func:`~flask.json.jsonify` function, which creates a response object with the given data serialized to JSON. .. code-block:: python from flask import jsonify @app.route("/users") def user_list(): users = User.query.order_by(User.name).all() return jsonify([u.to_json() for u in users]) It is usually not a good idea to return file data in a JSON response. JSON cannot represent binary data directly, so it must be base64 encoded, which can be slow, takes more bandwidth to send, and is not as easy to cache. Instead, serve files using one view, and generate a URL to the desired file to include in the JSON. Then the client can make a separate request to get the linked resource after getting the JSON. Receiving JSON in Views ----------------------- Use the :attr:`~flask.Request.json` property of the :data:`~flask.request` object to decode the request's body as JSON. If the body is not valid JSON, or the ``Content-Type`` header is not set to ``application/json``, a 400 Bad Request error will be raised. .. code-block:: python from flask import request @app.post("/user/") def user_update(id): user = User.query.get_or_404(id) user.update_from_json(request.json) db.session.commit() return user.to_json() flask-3.1.1/docs/patterns/jquery.rst000066400000000000000000000001401501065614000174460ustar00rootroot00000000000000:orphan: AJAX with jQuery ================ Obsolete, see :doc:`/patterns/javascript` instead. flask-3.1.1/docs/patterns/lazyloading.rst000066400000000000000000000074071501065614000204610ustar00rootroot00000000000000Lazily Loading Views ==================== Flask is usually used with the decorators. Decorators are simple and you have the URL right next to the function that is called for that specific URL. However there is a downside to this approach: it means all your code that uses decorators has to be imported upfront or Flask will never actually find your function. This can be a problem if your application has to import quick. It might have to do that on systems like Google's App Engine or other systems. So if you suddenly notice that your application outgrows this approach you can fall back to a centralized URL mapping. The system that enables having a central URL map is the :meth:`~flask.Flask.add_url_rule` function. Instead of using decorators, you have a file that sets up the application with all URLs. Converting to Centralized URL Map --------------------------------- Imagine the current application looks somewhat like this:: from flask import Flask app = Flask(__name__) @app.route('/') def index(): pass @app.route('/user/') def user(username): pass Then, with the centralized approach you would have one file with the views (:file:`views.py`) but without any decorator:: def index(): pass def user(username): pass And then a file that sets up an application which maps the functions to URLs:: from flask import Flask from yourapplication import views app = Flask(__name__) app.add_url_rule('/', view_func=views.index) app.add_url_rule('/user/', view_func=views.user) Loading Late ------------ So far we only split up the views and the routing, but the module is still loaded upfront. The trick is to actually load the view function as needed. This can be accomplished with a helper class that behaves just like a function but internally imports the real function on first use:: from werkzeug.utils import import_string, cached_property class LazyView(object): def __init__(self, import_name): self.__module__, self.__name__ = import_name.rsplit('.', 1) self.import_name = import_name @cached_property def view(self): return import_string(self.import_name) def __call__(self, *args, **kwargs): return self.view(*args, **kwargs) What's important here is is that `__module__` and `__name__` are properly set. This is used by Flask internally to figure out how to name the URL rules in case you don't provide a name for the rule yourself. Then you can define your central place to combine the views like this:: from flask import Flask from yourapplication.helpers import LazyView app = Flask(__name__) app.add_url_rule('/', view_func=LazyView('yourapplication.views.index')) app.add_url_rule('/user/', view_func=LazyView('yourapplication.views.user')) You can further optimize this in terms of amount of keystrokes needed to write this by having a function that calls into :meth:`~flask.Flask.add_url_rule` by prefixing a string with the project name and a dot, and by wrapping `view_func` in a `LazyView` as needed. :: def url(import_name, url_rules=[], **options): view = LazyView(f"yourapplication.{import_name}") for url_rule in url_rules: app.add_url_rule(url_rule, view_func=view, **options) # add a single route to the index view url('views.index', ['/']) # add two routes to a single function endpoint url_rules = ['/user/','/user/'] url('views.user', url_rules) One thing to keep in mind is that before and after request handlers have to be in a file that is imported upfront to work properly on the first request. The same goes for any kind of remaining decorator. flask-3.1.1/docs/patterns/methodoverrides.rst000066400000000000000000000026601501065614000213430ustar00rootroot00000000000000Adding HTTP Method Overrides ============================ Some HTTP proxies do not support arbitrary HTTP methods or newer HTTP methods (such as PATCH). In that case it's possible to "proxy" HTTP methods through another HTTP method in total violation of the protocol. The way this works is by letting the client do an HTTP POST request and set the ``X-HTTP-Method-Override`` header. Then the method is replaced with the header value before being passed to Flask. This can be accomplished with an HTTP middleware:: class HTTPMethodOverrideMiddleware(object): allowed_methods = frozenset([ 'GET', 'HEAD', 'POST', 'DELETE', 'PUT', 'PATCH', 'OPTIONS' ]) bodyless_methods = frozenset(['GET', 'HEAD', 'OPTIONS', 'DELETE']) def __init__(self, app): self.app = app def __call__(self, environ, start_response): method = environ.get('HTTP_X_HTTP_METHOD_OVERRIDE', '').upper() if method in self.allowed_methods: environ['REQUEST_METHOD'] = method if method in self.bodyless_methods: environ['CONTENT_LENGTH'] = '0' return self.app(environ, start_response) To use this with Flask, wrap the app object with the middleware:: from flask import Flask app = Flask(__name__) app.wsgi_app = HTTPMethodOverrideMiddleware(app.wsgi_app) flask-3.1.1/docs/patterns/mongoengine.rst000066400000000000000000000054511501065614000204460ustar00rootroot00000000000000MongoDB with MongoEngine ======================== Using a document database like MongoDB is a common alternative to relational SQL databases. This pattern shows how to use `MongoEngine`_, a document mapper library, to integrate with MongoDB. A running MongoDB server and `Flask-MongoEngine`_ are required. :: pip install flask-mongoengine .. _MongoEngine: http://mongoengine.org .. _Flask-MongoEngine: https://flask-mongoengine.readthedocs.io Configuration ------------- Basic setup can be done by defining ``MONGODB_SETTINGS`` on ``app.config`` and creating a ``MongoEngine`` instance. :: from flask import Flask from flask_mongoengine import MongoEngine app = Flask(__name__) app.config['MONGODB_SETTINGS'] = { "db": "myapp", } db = MongoEngine(app) Mapping Documents ----------------- To declare a model that represents a Mongo document, create a class that inherits from ``Document`` and declare each of the fields. :: import mongoengine as me class Movie(me.Document): title = me.StringField(required=True) year = me.IntField() rated = me.StringField() director = me.StringField() actors = me.ListField() If the document has nested fields, use ``EmbeddedDocument`` to defined the fields of the embedded document and ``EmbeddedDocumentField`` to declare it on the parent document. :: class Imdb(me.EmbeddedDocument): imdb_id = me.StringField() rating = me.DecimalField() votes = me.IntField() class Movie(me.Document): ... imdb = me.EmbeddedDocumentField(Imdb) Creating Data ------------- Instantiate your document class with keyword arguments for the fields. You can also assign values to the field attributes after instantiation. Then call ``doc.save()``. :: bttf = Movie(title="Back To The Future", year=1985) bttf.actors = [ "Michael J. Fox", "Christopher Lloyd" ] bttf.imdb = Imdb(imdb_id="tt0088763", rating=8.5) bttf.save() Queries ------- Use the class ``objects`` attribute to make queries. A keyword argument looks for an equal value on the field. :: bttf = Movie.objects(title="Back To The Future").get_or_404() Query operators may be used by concatenating them with the field name using a double-underscore. ``objects``, and queries returned by calling it, are iterable. :: some_theron_movie = Movie.objects(actors__in=["Charlize Theron"]).first() for recents in Movie.objects(year__gte=2017): print(recents.title) Documentation ------------- There are many more ways to define and query documents with MongoEngine. For more information, check out the `official documentation `_. Flask-MongoEngine adds helpful utilities on top of MongoEngine. Check out their `documentation `_ as well. flask-3.1.1/docs/patterns/packages.rst000066400000000000000000000100111501065614000177030ustar00rootroot00000000000000Large Applications as Packages ============================== Imagine a simple flask application structure that looks like this:: /yourapplication yourapplication.py /static style.css /templates layout.html index.html login.html ... While this is fine for small applications, for larger applications it's a good idea to use a package instead of a module. The :doc:`/tutorial/index` is structured to use the package pattern, see the :gh:`example code `. Simple Packages --------------- To convert that into a larger one, just create a new folder :file:`yourapplication` inside the existing one and move everything below it. Then rename :file:`yourapplication.py` to :file:`__init__.py`. (Make sure to delete all ``.pyc`` files first, otherwise things would most likely break) You should then end up with something like that:: /yourapplication /yourapplication __init__.py /static style.css /templates layout.html index.html login.html ... But how do you run your application now? The naive ``python yourapplication/__init__.py`` will not work. Let's just say that Python does not want modules in packages to be the startup file. But that is not a big problem, just add a new file called :file:`pyproject.toml` next to the inner :file:`yourapplication` folder with the following contents: .. code-block:: toml [project] name = "yourapplication" dependencies = [ "flask", ] [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" Install your application so it is importable: .. code-block:: text $ pip install -e . To use the ``flask`` command and run your application you need to set the ``--app`` option that tells Flask where to find the application instance: .. code-block:: text $ flask --app yourapplication run What did we gain from this? Now we can restructure the application a bit into multiple modules. The only thing you have to remember is the following quick checklist: 1. the `Flask` application object creation has to be in the :file:`__init__.py` file. That way each module can import it safely and the `__name__` variable will resolve to the correct package. 2. all the view functions (the ones with a :meth:`~flask.Flask.route` decorator on top) have to be imported in the :file:`__init__.py` file. Not the object itself, but the module it is in. Import the view module **after the application object is created**. Here's an example :file:`__init__.py`:: from flask import Flask app = Flask(__name__) import yourapplication.views And this is what :file:`views.py` would look like:: from yourapplication import app @app.route('/') def index(): return 'Hello World!' You should then end up with something like that:: /yourapplication pyproject.toml /yourapplication __init__.py views.py /static style.css /templates layout.html index.html login.html ... .. admonition:: Circular Imports Every Python programmer hates them, and yet we just added some: circular imports (That's when two modules depend on each other. In this case :file:`views.py` depends on :file:`__init__.py`). Be advised that this is a bad idea in general but here it is actually fine. The reason for this is that we are not actually using the views in :file:`__init__.py` and just ensuring the module is imported and we are doing that at the bottom of the file. Working with Blueprints ----------------------- If you have larger applications it's recommended to divide them into smaller groups where each group is implemented with the help of a blueprint. For a gentle introduction into this topic refer to the :doc:`/blueprints` chapter of the documentation. flask-3.1.1/docs/patterns/requestchecksum.rst000066400000000000000000000035051501065614000213520ustar00rootroot00000000000000Request Content Checksums ========================= Various pieces of code can consume the request data and preprocess it. For instance JSON data ends up on the request object already read and processed, form data ends up there as well but goes through a different code path. This seems inconvenient when you want to calculate the checksum of the incoming request data. This is necessary sometimes for some APIs. Fortunately this is however very simple to change by wrapping the input stream. The following example calculates the SHA1 checksum of the incoming data as it gets read and stores it in the WSGI environment:: import hashlib class ChecksumCalcStream(object): def __init__(self, stream): self._stream = stream self._hash = hashlib.sha1() def read(self, bytes): rv = self._stream.read(bytes) self._hash.update(rv) return rv def readline(self, size_hint): rv = self._stream.readline(size_hint) self._hash.update(rv) return rv def generate_checksum(request): env = request.environ stream = ChecksumCalcStream(env['wsgi.input']) env['wsgi.input'] = stream return stream._hash To use this, all you need to do is to hook the calculating stream in before the request starts consuming data. (Eg: be careful accessing ``request.form`` or anything of that nature. ``before_request_handlers`` for instance should be careful not to access it). Example usage:: @app.route('/special-api', methods=['POST']) def special_api(): hash = generate_checksum(request) # Accessing this parses the input stream files = request.files # At this point the hash is fully constructed. checksum = hash.hexdigest() return f"Hash was: {checksum}" flask-3.1.1/docs/patterns/singlepageapplications.rst000066400000000000000000000013331501065614000226610ustar00rootroot00000000000000Single-Page Applications ======================== Flask can be used to serve Single-Page Applications (SPA) by placing static files produced by your frontend framework in a subfolder inside of your project. You will also need to create a catch-all endpoint that routes all requests to your SPA. The following example demonstrates how to serve an SPA along with an API:: from flask import Flask, jsonify app = Flask(__name__, static_folder='app', static_url_path="/app") @app.route("/heartbeat") def heartbeat(): return jsonify({"status": "healthy"}) @app.route('/', defaults={'path': ''}) @app.route('/') def catch_all(path): return app.send_static_file("index.html") flask-3.1.1/docs/patterns/sqlalchemy.rst000066400000000000000000000161761501065614000203110ustar00rootroot00000000000000SQLAlchemy in Flask =================== Many people prefer `SQLAlchemy`_ for database access. In this case it's encouraged to use a package instead of a module for your flask application and drop the models into a separate module (:doc:`packages`). While that is not necessary, it makes a lot of sense. There are four very common ways to use SQLAlchemy. I will outline each of them here: Flask-SQLAlchemy Extension -------------------------- Because SQLAlchemy is a common database abstraction layer and object relational mapper that requires a little bit of configuration effort, there is a Flask extension that handles that for you. This is recommended if you want to get started quickly. You can download `Flask-SQLAlchemy`_ from `PyPI `_. .. _Flask-SQLAlchemy: https://flask-sqlalchemy.palletsprojects.com/ Declarative ----------- The declarative extension in SQLAlchemy is the most recent method of using SQLAlchemy. It allows you to define tables and models in one go, similar to how Django works. In addition to the following text I recommend the official documentation on the `declarative`_ extension. Here's the example :file:`database.py` module for your application:: from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker, declarative_base engine = create_engine('sqlite:////tmp/test.db') db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) Base = declarative_base() Base.query = db_session.query_property() def init_db(): # import all modules here that might define models so that # they will be registered properly on the metadata. Otherwise # you will have to import them first before calling init_db() import yourapplication.models Base.metadata.create_all(bind=engine) To define your models, just subclass the `Base` class that was created by the code above. If you are wondering why we don't have to care about threads here (like we did in the SQLite3 example above with the :data:`~flask.g` object): that's because SQLAlchemy does that for us already with the :class:`~sqlalchemy.orm.scoped_session`. To use SQLAlchemy in a declarative way with your application, you just have to put the following code into your application module. Flask will automatically remove database sessions at the end of the request or when the application shuts down:: from yourapplication.database import db_session @app.teardown_appcontext def shutdown_session(exception=None): db_session.remove() Here is an example model (put this into :file:`models.py`, e.g.):: from sqlalchemy import Column, Integer, String from yourapplication.database import Base class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) name = Column(String(50), unique=True) email = Column(String(120), unique=True) def __init__(self, name=None, email=None): self.name = name self.email = email def __repr__(self): return f'' To create the database you can use the `init_db` function: >>> from yourapplication.database import init_db >>> init_db() You can insert entries into the database like this: >>> from yourapplication.database import db_session >>> from yourapplication.models import User >>> u = User('admin', 'admin@localhost') >>> db_session.add(u) >>> db_session.commit() Querying is simple as well: >>> User.query.all() [] >>> User.query.filter(User.name == 'admin').first() .. _SQLAlchemy: https://www.sqlalchemy.org/ .. _declarative: https://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/ Manual Object Relational Mapping -------------------------------- Manual object relational mapping has a few upsides and a few downsides versus the declarative approach from above. The main difference is that you define tables and classes separately and map them together. It's more flexible but a little more to type. In general it works like the declarative approach, so make sure to also split up your application into multiple modules in a package. Here is an example :file:`database.py` module for your application:: from sqlalchemy import create_engine, MetaData from sqlalchemy.orm import scoped_session, sessionmaker engine = create_engine('sqlite:////tmp/test.db') metadata = MetaData() db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) def init_db(): metadata.create_all(bind=engine) As in the declarative approach, you need to close the session after each request or application context shutdown. Put this into your application module:: from yourapplication.database import db_session @app.teardown_appcontext def shutdown_session(exception=None): db_session.remove() Here is an example table and model (put this into :file:`models.py`):: from sqlalchemy import Table, Column, Integer, String from sqlalchemy.orm import mapper from yourapplication.database import metadata, db_session class User(object): query = db_session.query_property() def __init__(self, name=None, email=None): self.name = name self.email = email def __repr__(self): return f'' users = Table('users', metadata, Column('id', Integer, primary_key=True), Column('name', String(50), unique=True), Column('email', String(120), unique=True) ) mapper(User, users) Querying and inserting works exactly the same as in the example above. SQL Abstraction Layer --------------------- If you just want to use the database system (and SQL) abstraction layer you basically only need the engine:: from sqlalchemy import create_engine, MetaData, Table engine = create_engine('sqlite:////tmp/test.db') metadata = MetaData(bind=engine) Then you can either declare the tables in your code like in the examples above, or automatically load them:: from sqlalchemy import Table users = Table('users', metadata, autoload=True) To insert data you can use the `insert` method. We have to get a connection first so that we can use a transaction: >>> con = engine.connect() >>> con.execute(users.insert(), name='admin', email='admin@localhost') SQLAlchemy will automatically commit for us. To query your database, you use the engine directly or use a connection: >>> users.select(users.c.id == 1).execute().first() (1, 'admin', 'admin@localhost') These results are also dict-like tuples: >>> r = users.select(users.c.id == 1).execute().first() >>> r['name'] 'admin' You can also pass strings of SQL statements to the :meth:`~sqlalchemy.engine.base.Connection.execute` method: >>> engine.execute('select * from users where id = :1', [1]).first() (1, 'admin', 'admin@localhost') For more information about SQLAlchemy, head over to the `website `_. flask-3.1.1/docs/patterns/sqlite3.rst000066400000000000000000000115341501065614000175240ustar00rootroot00000000000000Using SQLite 3 with Flask ========================= In Flask you can easily implement the opening of database connections on demand and closing them when the context dies (usually at the end of the request). Here is a simple example of how you can use SQLite 3 with Flask:: import sqlite3 from flask import g DATABASE = '/path/to/database.db' def get_db(): db = getattr(g, '_database', None) if db is None: db = g._database = sqlite3.connect(DATABASE) return db @app.teardown_appcontext def close_connection(exception): db = getattr(g, '_database', None) if db is not None: db.close() Now, to use the database, the application must either have an active application context (which is always true if there is a request in flight) or create an application context itself. At that point the ``get_db`` function can be used to get the current database connection. Whenever the context is destroyed the database connection will be terminated. Example:: @app.route('/') def index(): cur = get_db().cursor() ... .. note:: Please keep in mind that the teardown request and appcontext functions are always executed, even if a before-request handler failed or was never executed. Because of this we have to make sure here that the database is there before we close it. Connect on Demand ----------------- The upside of this approach (connecting on first use) is that this will only open the connection if truly necessary. If you want to use this code outside a request context you can use it in a Python shell by opening the application context by hand:: with app.app_context(): # now you can use get_db() Easy Querying ------------- Now in each request handling function you can access `get_db()` to get the current open database connection. To simplify working with SQLite, a row factory function is useful. It is executed for every result returned from the database to convert the result. For instance, in order to get dictionaries instead of tuples, this could be inserted into the ``get_db`` function we created above:: def make_dicts(cursor, row): return dict((cursor.description[idx][0], value) for idx, value in enumerate(row)) db.row_factory = make_dicts This will make the sqlite3 module return dicts for this database connection, which are much nicer to deal with. Even more simply, we could place this in ``get_db`` instead:: db.row_factory = sqlite3.Row This would use Row objects rather than dicts to return the results of queries. These are ``namedtuple`` s, so we can access them either by index or by key. For example, assuming we have a ``sqlite3.Row`` called ``r`` for the rows ``id``, ``FirstName``, ``LastName``, and ``MiddleInitial``:: >>> # You can get values based on the row's name >>> r['FirstName'] John >>> # Or, you can get them based on index >>> r[1] John # Row objects are also iterable: >>> for value in r: ... print(value) 1 John Doe M Additionally, it is a good idea to provide a query function that combines getting the cursor, executing and fetching the results:: def query_db(query, args=(), one=False): cur = get_db().execute(query, args) rv = cur.fetchall() cur.close() return (rv[0] if rv else None) if one else rv This handy little function, in combination with a row factory, makes working with the database much more pleasant than it is by just using the raw cursor and connection objects. Here is how you can use it:: for user in query_db('select * from users'): print(user['username'], 'has the id', user['user_id']) Or if you just want a single result:: user = query_db('select * from users where username = ?', [the_username], one=True) if user is None: print('No such user') else: print(the_username, 'has the id', user['user_id']) To pass variable parts to the SQL statement, use a question mark in the statement and pass in the arguments as a list. Never directly add them to the SQL statement with string formatting because this makes it possible to attack the application using `SQL Injections `_. Initial Schemas --------------- Relational databases need schemas, so applications often ship a `schema.sql` file that creates the database. It's a good idea to provide a function that creates the database based on that schema. This function can do that for you:: def init_db(): with app.app_context(): db = get_db() with app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) db.commit() You can then create such a database from the Python shell: >>> from yourapplication import init_db >>> init_db() flask-3.1.1/docs/patterns/streaming.rst000066400000000000000000000051131501065614000201250ustar00rootroot00000000000000Streaming Contents ================== Sometimes you want to send an enormous amount of data to the client, much more than you want to keep in memory. When you are generating the data on the fly though, how do you send that back to the client without the roundtrip to the filesystem? The answer is by using generators and direct responses. Basic Usage ----------- This is a basic view function that generates a lot of CSV data on the fly. The trick is to have an inner function that uses a generator to generate data and to then invoke that function and pass it to a response object:: @app.route('/large.csv') def generate_large_csv(): def generate(): for row in iter_all_rows(): yield f"{','.join(row)}\n" return generate(), {"Content-Type": "text/csv"} Each ``yield`` expression is directly sent to the browser. Note though that some WSGI middlewares might break streaming, so be careful there in debug environments with profilers and other things you might have enabled. Streaming from Templates ------------------------ The Jinja2 template engine supports rendering a template piece by piece, returning an iterator of strings. Flask provides the :func:`~flask.stream_template` and :func:`~flask.stream_template_string` functions to make this easier to use. .. code-block:: python from flask import stream_template @app.get("/timeline") def timeline(): return stream_template("timeline.html") The parts yielded by the render stream tend to match statement blocks in the template. Streaming with Context ---------------------- The :data:`~flask.request` will not be active while the generator is running, because the view has already returned at that point. If you try to access ``request``, you'll get a ``RuntimeError``. If your generator function relies on data in ``request``, use the :func:`~flask.stream_with_context` wrapper. This will keep the request context active during the generator. .. code-block:: python from flask import stream_with_context, request from markupsafe import escape @app.route('/stream') def streamed_response(): def generate(): yield '

Hello ' yield escape(request.args['name']) yield '!

' return stream_with_context(generate()) It can also be used as a decorator. .. code-block:: python @stream_with_context def generate(): ... return generate() The :func:`~flask.stream_template` and :func:`~flask.stream_template_string` functions automatically use :func:`~flask.stream_with_context` if a request is active. flask-3.1.1/docs/patterns/subclassing.rst000066400000000000000000000012441501065614000204520ustar00rootroot00000000000000Subclassing Flask ================= The :class:`~flask.Flask` class is designed for subclassing. For example, you may want to override how request parameters are handled to preserve their order:: from flask import Flask, Request from werkzeug.datastructures import ImmutableOrderedMultiDict class MyRequest(Request): """Request subclass to override request parameter storage""" parameter_storage_class = ImmutableOrderedMultiDict class MyFlask(Flask): """Flask subclass using the custom request class""" request_class = MyRequest This is the recommended approach for overriding or augmenting Flask's internal functionality. flask-3.1.1/docs/patterns/templateinheritance.rst000066400000000000000000000042251501065614000221640ustar00rootroot00000000000000Template Inheritance ==================== The most powerful part of Jinja is template inheritance. Template inheritance allows you to build a base "skeleton" template that contains all the common elements of your site and defines **blocks** that child templates can override. Sounds complicated but is very basic. It's easiest to understand it by starting with an example. Base Template ------------- This template, which we'll call :file:`layout.html`, defines a simple HTML skeleton document that you might use for a simple two-column page. It's the job of "child" templates to fill the empty blocks with content: .. sourcecode:: html+jinja {% block head %} {% block title %}{% endblock %} - My Webpage {% endblock %}
{% block content %}{% endblock %}
In this example, the ``{% block %}`` tags define four blocks that child templates can fill in. All the `block` tag does is tell the template engine that a child template may override those portions of the template. Child Template -------------- A child template might look like this: .. sourcecode:: html+jinja {% extends "layout.html" %} {% block title %}Index{% endblock %} {% block head %} {{ super() }} {% endblock %} {% block content %}

Index

Welcome on my awesome homepage. {% endblock %} The ``{% extends %}`` tag is the key here. It tells the template engine that this template "extends" another template. When the template system evaluates this template, first it locates the parent. The extends tag must be the first tag in the template. To render the contents of a block defined in the parent template, use ``{{ super() }}``. flask-3.1.1/docs/patterns/urlprocessors.rst000066400000000000000000000103221501065614000210570ustar00rootroot00000000000000Using URL Processors ==================== .. versionadded:: 0.7 Flask 0.7 introduces the concept of URL processors. The idea is that you might have a bunch of resources with common parts in the URL that you don't always explicitly want to provide. For instance you might have a bunch of URLs that have the language code in it but you don't want to have to handle it in every single function yourself. URL processors are especially helpful when combined with blueprints. We will handle both application specific URL processors here as well as blueprint specifics. Internationalized Application URLs ---------------------------------- Consider an application like this:: from flask import Flask, g app = Flask(__name__) @app.route('//') def index(lang_code): g.lang_code = lang_code ... @app.route('//about') def about(lang_code): g.lang_code = lang_code ... This is an awful lot of repetition as you have to handle the language code setting on the :data:`~flask.g` object yourself in every single function. Sure, a decorator could be used to simplify this, but if you want to generate URLs from one function to another you would have to still provide the language code explicitly which can be annoying. For the latter, this is where :func:`~flask.Flask.url_defaults` functions come in. They can automatically inject values into a call to :func:`~flask.url_for`. The code below checks if the language code is not yet in the dictionary of URL values and if the endpoint wants a value named ``'lang_code'``:: @app.url_defaults def add_language_code(endpoint, values): if 'lang_code' in values or not g.lang_code: return if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): values['lang_code'] = g.lang_code The method :meth:`~werkzeug.routing.Map.is_endpoint_expecting` of the URL map can be used to figure out if it would make sense to provide a language code for the given endpoint. The reverse of that function are :meth:`~flask.Flask.url_value_preprocessor`\s. They are executed right after the request was matched and can execute code based on the URL values. The idea is that they pull information out of the values dictionary and put it somewhere else:: @app.url_value_preprocessor def pull_lang_code(endpoint, values): g.lang_code = values.pop('lang_code', None) That way you no longer have to do the `lang_code` assignment to :data:`~flask.g` in every function. You can further improve that by writing your own decorator that prefixes URLs with the language code, but the more beautiful solution is using a blueprint. Once the ``'lang_code'`` is popped from the values dictionary and it will no longer be forwarded to the view function reducing the code to this:: from flask import Flask, g app = Flask(__name__) @app.url_defaults def add_language_code(endpoint, values): if 'lang_code' in values or not g.lang_code: return if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): values['lang_code'] = g.lang_code @app.url_value_preprocessor def pull_lang_code(endpoint, values): g.lang_code = values.pop('lang_code', None) @app.route('//') def index(): ... @app.route('//about') def about(): ... Internationalized Blueprint URLs -------------------------------- Because blueprints can automatically prefix all URLs with a common string it's easy to automatically do that for every function. Furthermore blueprints can have per-blueprint URL processors which removes a whole lot of logic from the :meth:`~flask.Flask.url_defaults` function because it no longer has to check if the URL is really interested in a ``'lang_code'`` parameter:: from flask import Blueprint, g bp = Blueprint('frontend', __name__, url_prefix='/') @bp.url_defaults def add_language_code(endpoint, values): values.setdefault('lang_code', g.lang_code) @bp.url_value_preprocessor def pull_lang_code(endpoint, values): g.lang_code = values.pop('lang_code') @bp.route('/') def index(): ... @bp.route('/about') def about(): ... flask-3.1.1/docs/patterns/viewdecorators.rst000066400000000000000000000142561501065614000212040ustar00rootroot00000000000000View Decorators =============== Python has a really interesting feature called function decorators. This allows some really neat things for web applications. Because each view in Flask is a function, decorators can be used to inject additional functionality to one or more functions. The :meth:`~flask.Flask.route` decorator is the one you probably used already. But there are use cases for implementing your own decorator. For instance, imagine you have a view that should only be used by people that are logged in. If a user goes to the site and is not logged in, they should be redirected to the login page. This is a good example of a use case where a decorator is an excellent solution. Login Required Decorator ------------------------ So let's implement such a decorator. A decorator is a function that wraps and replaces another function. Since the original function is replaced, you need to remember to copy the original function's information to the new function. Use :func:`functools.wraps` to handle this for you. This example assumes that the login page is called ``'login'`` and that the current user is stored in ``g.user`` and is ``None`` if there is no-one logged in. :: from functools import wraps from flask import g, request, redirect, url_for def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): if g.user is None: return redirect(url_for('login', next=request.url)) return f(*args, **kwargs) return decorated_function To use the decorator, apply it as innermost decorator to a view function. When applying further decorators, always remember that the :meth:`~flask.Flask.route` decorator is the outermost. :: @app.route('/secret_page') @login_required def secret_page(): pass .. note:: The ``next`` value will exist in ``request.args`` after a ``GET`` request for the login page. You'll have to pass it along when sending the ``POST`` request from the login form. You can do this with a hidden input tag, then retrieve it from ``request.form`` when logging the user in. :: Caching Decorator ----------------- Imagine you have a view function that does an expensive calculation and because of that you would like to cache the generated results for a certain amount of time. A decorator would be nice for that. We're assuming you have set up a cache like mentioned in :doc:`caching`. Here is an example cache function. It generates the cache key from a specific prefix (actually a format string) and the current path of the request. Notice that we are using a function that first creates the decorator that then decorates the function. Sounds awful? Unfortunately it is a little bit more complex, but the code should still be straightforward to read. The decorated function will then work as follows 1. get the unique cache key for the current request based on the current path. 2. get the value for that key from the cache. If the cache returned something we will return that value. 3. otherwise the original function is called and the return value is stored in the cache for the timeout provided (by default 5 minutes). Here the code:: from functools import wraps from flask import request def cached(timeout=5 * 60, key='view/{}'): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): cache_key = key.format(request.path) rv = cache.get(cache_key) if rv is not None: return rv rv = f(*args, **kwargs) cache.set(cache_key, rv, timeout=timeout) return rv return decorated_function return decorator Notice that this assumes an instantiated ``cache`` object is available, see :doc:`caching`. Templating Decorator -------------------- A common pattern invented by the TurboGears guys a while back is a templating decorator. The idea of that decorator is that you return a dictionary with the values passed to the template from the view function and the template is automatically rendered. With that, the following three examples do exactly the same:: @app.route('/') def index(): return render_template('index.html', value=42) @app.route('/') @templated('index.html') def index(): return dict(value=42) @app.route('/') @templated() def index(): return dict(value=42) As you can see, if no template name is provided it will use the endpoint of the URL map with dots converted to slashes + ``'.html'``. Otherwise the provided template name is used. When the decorated function returns, the dictionary returned is passed to the template rendering function. If ``None`` is returned, an empty dictionary is assumed, if something else than a dictionary is returned we return it from the function unchanged. That way you can still use the redirect function or return simple strings. Here is the code for that decorator:: from functools import wraps from flask import request, render_template def templated(template=None): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): template_name = template if template_name is None: template_name = f"{request.endpoint.replace('.', '/')}.html" ctx = f(*args, **kwargs) if ctx is None: ctx = {} elif not isinstance(ctx, dict): return ctx return render_template(template_name, **ctx) return decorated_function return decorator Endpoint Decorator ------------------ When you want to use the werkzeug routing system for more flexibility you need to map the endpoint as defined in the :class:`~werkzeug.routing.Rule` to a view function. This is possible with this decorator. For example:: from flask import Flask from werkzeug.routing import Rule app = Flask(__name__) app.url_map.add(Rule('/', endpoint='index')) @app.endpoint('index') def my_index(): return "Hello world" flask-3.1.1/docs/patterns/wtforms.rst000066400000000000000000000111451501065614000176370ustar00rootroot00000000000000Form Validation with WTForms ============================ When you have to work with form data submitted by a browser view, code quickly becomes very hard to read. There are libraries out there designed to make this process easier to manage. One of them is `WTForms`_ which we will handle here. If you find yourself in the situation of having many forms, you might want to give it a try. When you are working with WTForms you have to define your forms as classes first. I recommend breaking up the application into multiple modules (:doc:`packages`) for that and adding a separate module for the forms. .. admonition:: Getting the most out of WTForms with an Extension The `Flask-WTF`_ extension expands on this pattern and adds a few little helpers that make working with forms and Flask more fun. You can get it from `PyPI `_. .. _Flask-WTF: https://flask-wtf.readthedocs.io/ The Forms --------- This is an example form for a typical registration page:: from wtforms import Form, BooleanField, StringField, PasswordField, validators class RegistrationForm(Form): username = StringField('Username', [validators.Length(min=4, max=25)]) email = StringField('Email Address', [validators.Length(min=6, max=35)]) password = PasswordField('New Password', [ validators.DataRequired(), validators.EqualTo('confirm', message='Passwords must match') ]) confirm = PasswordField('Repeat Password') accept_tos = BooleanField('I accept the TOS', [validators.DataRequired()]) In the View ----------- In the view function, the usage of this form looks like this:: @app.route('/register', methods=['GET', 'POST']) def register(): form = RegistrationForm(request.form) if request.method == 'POST' and form.validate(): user = User(form.username.data, form.email.data, form.password.data) db_session.add(user) flash('Thanks for registering') return redirect(url_for('login')) return render_template('register.html', form=form) Notice we're implying that the view is using SQLAlchemy here (:doc:`sqlalchemy`), but that's not a requirement, of course. Adapt the code as necessary. Things to remember: 1. create the form from the request :attr:`~flask.request.form` value if the data is submitted via the HTTP ``POST`` method and :attr:`~flask.request.args` if the data is submitted as ``GET``. 2. to validate the data, call the :func:`~wtforms.form.Form.validate` method, which will return ``True`` if the data validates, ``False`` otherwise. 3. to access individual values from the form, access `form..data`. Forms in Templates ------------------ Now to the template side. When you pass the form to the templates, you can easily render them there. Look at the following example template to see how easy this is. WTForms does half the form generation for us already. To make it even nicer, we can write a macro that renders a field with label and a list of errors if there are any. Here's an example :file:`_formhelpers.html` template with such a macro: .. sourcecode:: html+jinja {% macro render_field(field) %}

{{ field.label }}
{{ field(**kwargs)|safe }} {% if field.errors %}
    {% for error in field.errors %}
  • {{ error }}
  • {% endfor %}
{% endif %}
{% endmacro %} This macro accepts a couple of keyword arguments that are forwarded to WTForm's field function, which renders the field for us. The keyword arguments will be inserted as HTML attributes. So, for example, you can call ``render_field(form.username, class='username')`` to add a class to the input element. Note that WTForms returns standard Python strings, so we have to tell Jinja2 that this data is already HTML-escaped with the ``|safe`` filter. Here is the :file:`register.html` template for the function we used above, which takes advantage of the :file:`_formhelpers.html` template: .. sourcecode:: html+jinja {% from "_formhelpers.html" import render_field %}
{{ render_field(form.username) }} {{ render_field(form.email) }} {{ render_field(form.password) }} {{ render_field(form.confirm) }} {{ render_field(form.accept_tos) }}

For more information about WTForms, head over to the `WTForms website`_. .. _WTForms: https://wtforms.readthedocs.io/ .. _WTForms website: https://wtforms.readthedocs.io/ flask-3.1.1/docs/quickstart.rst000066400000000000000000000770411501065614000164770ustar00rootroot00000000000000Quickstart ========== Eager to get started? This page gives a good introduction to Flask. Follow :doc:`installation` to set up a project and install Flask first. A Minimal Application --------------------- A minimal Flask application looks something like this: .. code-block:: python from flask import Flask app = Flask(__name__) @app.route("/") def hello_world(): return "

Hello, World!

" So what did that code do? 1. First we imported the :class:`~flask.Flask` class. An instance of this class will be our WSGI application. 2. Next we create an instance of this class. The first argument is the name of the application's module or package. ``__name__`` is a convenient shortcut for this that is appropriate for most cases. This is needed so that Flask knows where to look for resources such as templates and static files. 3. We then use the :meth:`~flask.Flask.route` decorator to tell Flask what URL should trigger our function. 4. The function returns the message we want to display in the user's browser. The default content type is HTML, so HTML in the string will be rendered by the browser. Save it as :file:`hello.py` or something similar. Make sure to not call your application :file:`flask.py` because this would conflict with Flask itself. To run the application, use the ``flask`` command or ``python -m flask``. You need to tell the Flask where your application is with the ``--app`` option. .. code-block:: text $ flask --app hello run * Serving Flask app 'hello' * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) .. admonition:: Application Discovery Behavior As a shortcut, if the file is named ``app.py`` or ``wsgi.py``, you don't have to use ``--app``. See :doc:`/cli` for more details. This launches a very simple builtin server, which is good enough for testing but probably not what you want to use in production. For deployment options see :doc:`deploying/index`. Now head over to http://127.0.0.1:5000/, and you should see your hello world greeting. If another program is already using port 5000, you'll see ``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the server tries to start. See :ref:`address-already-in-use` for how to handle that. .. _public-server: .. admonition:: Externally Visible Server If you run the server you will notice that the server is only accessible from your own computer, not from any other in the network. This is the default because in debugging mode a user of the application can execute arbitrary Python code on your computer. If you have the debugger disabled or trust the users on your network, you can make the server publicly available simply by adding ``--host=0.0.0.0`` to the command line:: $ flask run --host=0.0.0.0 This tells your operating system to listen on all public IPs. Debug Mode ---------- The ``flask run`` command can do more than just start the development server. By enabling debug mode, the server will automatically reload if code changes, and will show an interactive debugger in the browser if an error occurs during a request. .. image:: _static/debugger.png :align: center :class: screenshot :alt: The interactive debugger in action. .. warning:: The debugger allows executing arbitrary Python code from the browser. It is protected by a pin, but still represents a major security risk. Do not run the development server or debugger in a production environment. To enable debug mode, use the ``--debug`` option. .. code-block:: text $ flask --app hello run --debug * Serving Flask app 'hello' * Debug mode: on * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: nnn-nnn-nnn See also: - :doc:`/server` and :doc:`/cli` for information about running in debug mode. - :doc:`/debugging` for information about using the built-in debugger and other debuggers. - :doc:`/logging` and :doc:`/errorhandling` to log errors and display nice error pages. HTML Escaping ------------- When returning HTML (the default response type in Flask), any user-provided values rendered in the output must be escaped to protect from injection attacks. HTML templates rendered with Jinja, introduced later, will do this automatically. :func:`~markupsafe.escape`, shown here, can be used manually. It is omitted in most examples for brevity, but you should always be aware of how you're using untrusted data. .. code-block:: python from markupsafe import escape @app.route("/") def hello(name): return f"Hello, {escape(name)}!" If a user managed to submit the name ````, escaping causes it to be rendered as text, rather than running the script in the user's browser. ```` in the route captures a value from the URL and passes it to the view function. These variable rules are explained below. Routing ------- Modern web applications use meaningful URLs to help users. Users are more likely to like a page and come back if the page uses a meaningful URL they can remember and use to directly visit a page. Use the :meth:`~flask.Flask.route` decorator to bind a function to a URL. :: @app.route('/') def index(): return 'Index Page' @app.route('/hello') def hello(): return 'Hello, World' You can do more! You can make parts of the URL dynamic and attach multiple rules to a function. Variable Rules `````````````` You can add variable sections to a URL by marking sections with ````. Your function then receives the ```` as a keyword argument. Optionally, you can use a converter to specify the type of the argument like ````. :: from markupsafe import escape @app.route('/user/') def show_user_profile(username): # show the user profile for that user return f'User {escape(username)}' @app.route('/post/') def show_post(post_id): # show the post with the given id, the id is an integer return f'Post {post_id}' @app.route('/path/') def show_subpath(subpath): # show the subpath after /path/ return f'Subpath {escape(subpath)}' Converter types: ========== ========================================== ``string`` (default) accepts any text without a slash ``int`` accepts positive integers ``float`` accepts positive floating point values ``path`` like ``string`` but also accepts slashes ``uuid`` accepts UUID strings ========== ========================================== Unique URLs / Redirection Behavior `````````````````````````````````` The following two rules differ in their use of a trailing slash. :: @app.route('/projects/') def projects(): return 'The project page' @app.route('/about') def about(): return 'The about page' The canonical URL for the ``projects`` endpoint has a trailing slash. It's similar to a folder in a file system. If you access the URL without a trailing slash (``/projects``), Flask redirects you to the canonical URL with the trailing slash (``/projects/``). The canonical URL for the ``about`` endpoint does not have a trailing slash. It's similar to the pathname of a file. Accessing the URL with a trailing slash (``/about/``) produces a 404 "Not Found" error. This helps keep URLs unique for these resources, which helps search engines avoid indexing the same page twice. .. _url-building: URL Building ```````````` To build a URL to a specific function, use the :func:`~flask.url_for` function. It accepts the name of the function as its first argument and any number of keyword arguments, each corresponding to a variable part of the URL rule. Unknown variable parts are appended to the URL as query parameters. Why would you want to build URLs using the URL reversing function :func:`~flask.url_for` instead of hard-coding them into your templates? 1. Reversing is often more descriptive than hard-coding the URLs. 2. You can change your URLs in one go instead of needing to remember to manually change hard-coded URLs. 3. URL building handles escaping of special characters transparently. 4. The generated paths are always absolute, avoiding unexpected behavior of relative paths in browsers. 5. If your application is placed outside the URL root, for example, in ``/myapplication`` instead of ``/``, :func:`~flask.url_for` properly handles that for you. For example, here we use the :meth:`~flask.Flask.test_request_context` method to try out :func:`~flask.url_for`. :meth:`~flask.Flask.test_request_context` tells Flask to behave as though it's handling a request even while we use a Python shell. See :ref:`context-locals`. .. code-block:: python from flask import url_for @app.route('/') def index(): return 'index' @app.route('/login') def login(): return 'login' @app.route('/user/') def profile(username): return f'{username}\'s profile' with app.test_request_context(): print(url_for('index')) print(url_for('login')) print(url_for('login', next='/')) print(url_for('profile', username='John Doe')) .. code-block:: text / /login /login?next=/ /user/John%20Doe HTTP Methods ```````````` Web applications use different HTTP methods when accessing URLs. You should familiarize yourself with the HTTP methods as you work with Flask. By default, a route only answers to ``GET`` requests. You can use the ``methods`` argument of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods. :: from flask import request @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': return do_the_login() else: return show_the_login_form() The example above keeps all methods for the route within one function, which can be useful if each part uses some common data. You can also separate views for different methods into different functions. Flask provides a shortcut for decorating such routes with :meth:`~flask.Flask.get`, :meth:`~flask.Flask.post`, etc. for each common HTTP method. .. code-block:: python @app.get('/login') def login_get(): return show_the_login_form() @app.post('/login') def login_post(): return do_the_login() If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method and handles ``HEAD`` requests according to the `HTTP RFC`_. Likewise, ``OPTIONS`` is automatically implemented for you. .. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt Static Files ------------ Dynamic web applications also need static files. That's usually where the CSS and JavaScript files are coming from. Ideally your web server is configured to serve them for you, but during development Flask can do that as well. Just create a folder called :file:`static` in your package or next to your module and it will be available at ``/static`` on the application. To generate URLs for static files, use the special ``'static'`` endpoint name:: url_for('static', filename='style.css') The file has to be stored on the filesystem as :file:`static/style.css`. Rendering Templates ------------------- Generating HTML from within Python is not fun, and actually pretty cumbersome because you have to do the HTML escaping on your own to keep the application secure. Because of that Flask configures the `Jinja2 `_ template engine for you automatically. Templates can be used to generate any type of text file. For web applications, you'll primarily be generating HTML pages, but you can also generate markdown, plain text for emails, and anything else. For a reference to HTML, CSS, and other web APIs, use the `MDN Web Docs`_. .. _MDN Web Docs: https://developer.mozilla.org/ To render a template you can use the :func:`~flask.render_template` method. All you have to do is provide the name of the template and the variables you want to pass to the template engine as keyword arguments. Here's a simple example of how to render a template:: from flask import render_template @app.route('/hello/') @app.route('/hello/') def hello(name=None): return render_template('hello.html', person=name) Flask will look for templates in the :file:`templates` folder. So if your application is a module, this folder is next to that module, if it's a package it's actually inside your package: **Case 1**: a module:: /application.py /templates /hello.html **Case 2**: a package:: /application /__init__.py /templates /hello.html For templates you can use the full power of Jinja2 templates. Head over to the official `Jinja2 Template Documentation `_ for more information. Here is an example template: .. sourcecode:: html+jinja Hello from Flask {% if person %}

Hello {{ person }}!

{% else %}

Hello, World!

{% endif %} Inside templates you also have access to the :data:`~flask.Flask.config`, :class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` [#]_ objects as well as the :func:`~flask.url_for` and :func:`~flask.get_flashed_messages` functions. Templates are especially useful if inheritance is used. If you want to know how that works, see :doc:`patterns/templateinheritance`. Basically template inheritance makes it possible to keep certain elements on each page (like header, navigation and footer). Automatic escaping is enabled, so if ``person`` contains HTML it will be escaped automatically. If you can trust a variable and you know that it will be safe HTML (for example because it came from a module that converts wiki markup to HTML) you can mark it as safe by using the :class:`~markupsafe.Markup` class or by using the ``|safe`` filter in the template. Head over to the Jinja 2 documentation for more examples. Here is a basic introduction to how the :class:`~markupsafe.Markup` class works:: >>> from markupsafe import Markup >>> Markup('Hello %s!') % 'hacker' Markup('Hello <blink>hacker</blink>!') >>> Markup.escape('hacker') Markup('<blink>hacker</blink>') >>> Markup('Marked up » HTML').striptags() 'Marked up » HTML' .. versionchanged:: 0.5 Autoescaping is no longer enabled for all templates. The following extensions for templates trigger autoescaping: ``.html``, ``.htm``, ``.xml``, ``.xhtml``. Templates loaded from a string will have autoescaping disabled. .. [#] Unsure what that :class:`~flask.g` object is? It's something in which you can store information for your own needs. See the documentation for :class:`flask.g` and :doc:`patterns/sqlite3`. Accessing Request Data ---------------------- For web applications it's crucial to react to the data a client sends to the server. In Flask this information is provided by the global :class:`~flask.request` object. If you have some experience with Python you might be wondering how that object can be global and how Flask manages to still be threadsafe. The answer is context locals: .. _context-locals: Context Locals `````````````` .. admonition:: Insider Information If you want to understand how that works and how you can implement tests with context locals, read this section, otherwise just skip it. Certain objects in Flask are global objects, but not of the usual kind. These objects are actually proxies to objects that are local to a specific context. What a mouthful. But that is actually quite easy to understand. Imagine the context being the handling thread. A request comes in and the web server decides to spawn a new thread (or something else, the underlying object is capable of dealing with concurrency systems other than threads). When Flask starts its internal request handling it figures out that the current thread is the active context and binds the current application and the WSGI environments to that context (thread). It does that in an intelligent way so that one application can invoke another application without breaking. So what does this mean to you? Basically you can completely ignore that this is the case unless you are doing something like unit testing. You will notice that code which depends on a request object will suddenly break because there is no request object. The solution is creating a request object yourself and binding it to the context. The easiest solution for unit testing is to use the :meth:`~flask.Flask.test_request_context` context manager. In combination with the ``with`` statement it will bind a test request so that you can interact with it. Here is an example:: from flask import request with app.test_request_context('/hello', method='POST'): # now you can do something with the request until the # end of the with block, such as basic assertions: assert request.path == '/hello' assert request.method == 'POST' The other possibility is passing a whole WSGI environment to the :meth:`~flask.Flask.request_context` method:: with app.request_context(environ): assert request.method == 'POST' The Request Object `````````````````` The request object is documented in the API section and we will not cover it here in detail (see :class:`~flask.Request`). Here is a broad overview of some of the most common operations. First of all you have to import it from the ``flask`` module:: from flask import request The current request method is available by using the :attr:`~flask.Request.method` attribute. To access form data (data transmitted in a ``POST`` or ``PUT`` request) you can use the :attr:`~flask.Request.form` attribute. Here is a full example of the two attributes mentioned above:: @app.route('/login', methods=['POST', 'GET']) def login(): error = None if request.method == 'POST': if valid_login(request.form['username'], request.form['password']): return log_the_user_in(request.form['username']) else: error = 'Invalid username/password' # the code below is executed if the request method # was GET or the credentials were invalid return render_template('login.html', error=error) What happens if the key does not exist in the ``form`` attribute? In that case a special :exc:`KeyError` is raised. You can catch it like a standard :exc:`KeyError` but if you don't do that, a HTTP 400 Bad Request error page is shown instead. So for many situations you don't have to deal with that problem. To access parameters submitted in the URL (``?key=value``) you can use the :attr:`~flask.Request.args` attribute:: searchword = request.args.get('key', '') We recommend accessing URL parameters with `get` or by catching the :exc:`KeyError` because users might change the URL and presenting them a 400 bad request page in that case is not user friendly. For a full list of methods and attributes of the request object, head over to the :class:`~flask.Request` documentation. File Uploads ```````````` You can handle uploaded files with Flask easily. Just make sure not to forget to set the ``enctype="multipart/form-data"`` attribute on your HTML form, otherwise the browser will not transmit your files at all. Uploaded files are stored in memory or at a temporary location on the filesystem. You can access those files by looking at the :attr:`~flask.request.files` attribute on the request object. Each uploaded file is stored in that dictionary. It behaves just like a standard Python :class:`file` object, but it also has a :meth:`~werkzeug.datastructures.FileStorage.save` method that allows you to store that file on the filesystem of the server. Here is a simple example showing how that works:: from flask import request @app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': f = request.files['the_file'] f.save('/var/www/uploads/uploaded_file.txt') ... If you want to know how the file was named on the client before it was uploaded to your application, you can access the :attr:`~werkzeug.datastructures.FileStorage.filename` attribute. However please keep in mind that this value can be forged so never ever trust that value. If you want to use the filename of the client to store the file on the server, pass it through the :func:`~werkzeug.utils.secure_filename` function that Werkzeug provides for you:: from werkzeug.utils import secure_filename @app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': file = request.files['the_file'] file.save(f"/var/www/uploads/{secure_filename(file.filename)}") ... For some better examples, see :doc:`patterns/fileuploads`. Cookies ``````` To access cookies you can use the :attr:`~flask.Request.cookies` attribute. To set cookies you can use the :attr:`~flask.Response.set_cookie` method of response objects. The :attr:`~flask.Request.cookies` attribute of request objects is a dictionary with all the cookies the client transmits. If you want to use sessions, do not use the cookies directly but instead use the :ref:`sessions` in Flask that add some security on top of cookies for you. Reading cookies:: from flask import request @app.route('/') def index(): username = request.cookies.get('username') # use cookies.get(key) instead of cookies[key] to not get a # KeyError if the cookie is missing. Storing cookies:: from flask import make_response @app.route('/') def index(): resp = make_response(render_template(...)) resp.set_cookie('username', 'the username') return resp Note that cookies are set on response objects. Since you normally just return strings from the view functions Flask will convert them into response objects for you. If you explicitly want to do that you can use the :meth:`~flask.make_response` function and then modify it. Sometimes you might want to set a cookie at a point where the response object does not exist yet. This is possible by utilizing the :doc:`patterns/deferredcallbacks` pattern. For this also see :ref:`about-responses`. Redirects and Errors -------------------- To redirect a user to another endpoint, use the :func:`~flask.redirect` function; to abort a request early with an error code, use the :func:`~flask.abort` function:: from flask import abort, redirect, url_for @app.route('/') def index(): return redirect(url_for('login')) @app.route('/login') def login(): abort(401) this_is_never_executed() This is a rather pointless example because a user will be redirected from the index to a page they cannot access (401 means access denied) but it shows how that works. By default a black and white error page is shown for each error code. If you want to customize the error page, you can use the :meth:`~flask.Flask.errorhandler` decorator:: from flask import render_template @app.errorhandler(404) def page_not_found(error): return render_template('page_not_found.html'), 404 Note the ``404`` after the :func:`~flask.render_template` call. This tells Flask that the status code of that page should be 404 which means not found. By default 200 is assumed which translates to: all went well. See :doc:`errorhandling` for more details. .. _about-responses: About Responses --------------- The return value from a view function is automatically converted into a response object for you. If the return value is a string it's converted into a response object with the string as response body, a ``200 OK`` status code and a :mimetype:`text/html` mimetype. If the return value is a dict or list, :func:`jsonify` is called to produce a response. The logic that Flask applies to converting return values into response objects is as follows: 1. If a response object of the correct type is returned it's directly returned from the view. 2. If it's a string, a response object is created with that data and the default parameters. 3. If it's an iterator or generator returning strings or bytes, it is treated as a streaming response. 4. If it's a dict or list, a response object is created using :func:`~flask.json.jsonify`. 5. If a tuple is returned the items in the tuple can provide extra information. Such tuples have to be in the form ``(response, status)``, ``(response, headers)``, or ``(response, status, headers)``. The ``status`` value will override the status code and ``headers`` can be a list or dictionary of additional header values. 6. If none of that works, Flask will assume the return value is a valid WSGI application and convert that into a response object. If you want to get hold of the resulting response object inside the view you can use the :func:`~flask.make_response` function. Imagine you have a view like this:: from flask import render_template @app.errorhandler(404) def not_found(error): return render_template('error.html'), 404 You just need to wrap the return expression with :func:`~flask.make_response` and get the response object to modify it, then return it:: from flask import make_response @app.errorhandler(404) def not_found(error): resp = make_response(render_template('error.html'), 404) resp.headers['X-Something'] = 'A value' return resp APIs with JSON `````````````` A common response format when writing an API is JSON. It's easy to get started writing such an API with Flask. If you return a ``dict`` or ``list`` from a view, it will be converted to a JSON response. .. code-block:: python @app.route("/me") def me_api(): user = get_current_user() return { "username": user.username, "theme": user.theme, "image": url_for("user_image", filename=user.image), } @app.route("/users") def users_api(): users = get_all_users() return [user.to_json() for user in users] This is a shortcut to passing the data to the :func:`~flask.json.jsonify` function, which will serialize any supported JSON data type. That means that all the data in the dict or list must be JSON serializable. For complex types such as database models, you'll want to use a serialization library to convert the data to valid JSON types first. There are many serialization libraries and Flask API extensions maintained by the community that support more complex applications. .. _sessions: Sessions -------- In addition to the request object there is also a second object called :class:`~flask.session` which allows you to store information specific to a user from one request to the next. This is implemented on top of cookies for you and signs the cookies cryptographically. What this means is that the user could look at the contents of your cookie but not modify it, unless they know the secret key used for signing. In order to use sessions you have to set a secret key. Here is how sessions work:: from flask import session # Set the secret key to some random bytes. Keep this really secret! app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' @app.route('/') def index(): if 'username' in session: return f'Logged in as {session["username"]}' return 'You are not logged in' @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': session['username'] = request.form['username'] return redirect(url_for('index')) return '''

''' @app.route('/logout') def logout(): # remove the username from the session if it's there session.pop('username', None) return redirect(url_for('index')) .. admonition:: How to generate good secret keys A secret key should be as random as possible. Your operating system has ways to generate pretty random data based on a cryptographic random generator. Use the following command to quickly generate a value for :attr:`Flask.secret_key` (or :data:`SECRET_KEY`):: $ python -c 'import secrets; print(secrets.token_hex())' '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' A note on cookie-based sessions: Flask will take the values you put into the session object and serialize them into a cookie. If you are finding some values do not persist across requests, cookies are indeed enabled, and you are not getting a clear error message, check the size of the cookie in your page responses compared to the size supported by web browsers. Besides the default client-side based sessions, if you want to handle sessions on the server-side instead, there are several Flask extensions that support this. Message Flashing ---------------- Good applications and user interfaces are all about feedback. If the user does not get enough feedback they will probably end up hating the application. Flask provides a really simple way to give feedback to a user with the flashing system. The flashing system basically makes it possible to record a message at the end of a request and access it on the next (and only the next) request. This is usually combined with a layout template to expose the message. To flash a message use the :func:`~flask.flash` method, to get hold of the messages you can use :func:`~flask.get_flashed_messages` which is also available in the templates. See :doc:`patterns/flashing` for a full example. Logging ------- .. versionadded:: 0.3 Sometimes you might be in a situation where you deal with data that should be correct, but actually is not. For example you may have some client-side code that sends an HTTP request to the server but it's obviously malformed. This might be caused by a user tampering with the data, or the client code failing. Most of the time it's okay to reply with ``400 Bad Request`` in that situation, but sometimes that won't do and the code has to continue working. You may still want to log that something fishy happened. This is where loggers come in handy. As of Flask 0.3 a logger is preconfigured for you to use. Here are some example log calls:: app.logger.debug('A value for debugging') app.logger.warning('A warning occurred (%d apples)', 42) app.logger.error('An error occurred') The attached :attr:`~flask.Flask.logger` is a standard logging :class:`~logging.Logger`, so head over to the official :mod:`logging` docs for more information. See :doc:`errorhandling`. Hooking in WSGI Middleware -------------------------- To add WSGI middleware to your Flask application, wrap the application's ``wsgi_app`` attribute. For example, to apply Werkzeug's :class:`~werkzeug.middleware.proxy_fix.ProxyFix` middleware for running behind Nginx: .. code-block:: python from werkzeug.middleware.proxy_fix import ProxyFix app.wsgi_app = ProxyFix(app.wsgi_app) Wrapping ``app.wsgi_app`` instead of ``app`` means that ``app`` still points at your Flask application, not at the middleware, so you can continue to use and configure ``app`` directly. Using Flask Extensions ---------------------- Extensions are packages that help you accomplish common tasks. For example, Flask-SQLAlchemy provides SQLAlchemy support that makes it simple and easy to use with Flask. For more on Flask extensions, see :doc:`extensions`. Deploying to a Web Server ------------------------- Ready to deploy your new Flask app? See :doc:`deploying/index`. flask-3.1.1/docs/reqcontext.rst000066400000000000000000000221661501065614000164770ustar00rootroot00000000000000.. currentmodule:: flask The Request Context =================== The request context keeps track of the request-level data during a request. Rather than passing the request object to each function that runs during a request, the :data:`request` and :data:`session` proxies are accessed instead. This is similar to :doc:`/appcontext`, which keeps track of the application-level data independent of a request. A corresponding application context is pushed when a request context is pushed. Purpose of the Context ---------------------- When the :class:`Flask` application handles a request, it creates a :class:`Request` object based on the environment it received from the WSGI server. Because a *worker* (thread, process, or coroutine depending on the server) handles only one request at a time, the request data can be considered global to that worker during that request. Flask uses the term *context local* for this. Flask automatically *pushes* a request context when handling a request. View functions, error handlers, and other functions that run during a request will have access to the :data:`request` proxy, which points to the request object for the current request. Lifetime of the Context ----------------------- When a Flask application begins handling a request, it pushes a request context, which also pushes an :doc:`app context `. When the request ends it pops the request context then the application context. The context is unique to each thread (or other worker type). :data:`request` cannot be passed to another thread, the other thread has a different context space and will not know about the request the parent thread was pointing to. Context locals are implemented using Python's :mod:`contextvars` and Werkzeug's :class:`~werkzeug.local.LocalProxy`. Python manages the lifetime of context vars automatically, and local proxy wraps that low-level interface to make the data easier to work with. Manually Push a Context ----------------------- If you try to access :data:`request`, or anything that uses it, outside a request context, you'll get this error message: .. code-block:: pytb RuntimeError: Working outside of request context. This typically means that you attempted to use functionality that needed an active HTTP request. Consult the documentation on testing for information about how to avoid this problem. This should typically only happen when testing code that expects an active request. One option is to use the :meth:`test client ` to simulate a full request. Or you can use :meth:`~Flask.test_request_context` in a ``with`` block, and everything that runs in the block will have access to :data:`request`, populated with your test data. :: def generate_report(year): format = request.args.get("format") ... with app.test_request_context( "/make_report/2017", query_string={"format": "short"} ): generate_report() If you see that error somewhere else in your code not related to testing, it most likely indicates that you should move that code into a view function. For information on how to use the request context from the interactive Python shell, see :doc:`/shell`. How the Context Works --------------------- The :meth:`Flask.wsgi_app` method is called to handle each request. It manages the contexts during the request. Internally, the request and application contexts work like stacks. When contexts are pushed, the proxies that depend on them are available and point at information from the top item. When the request starts, a :class:`~ctx.RequestContext` is created and pushed, which creates and pushes an :class:`~ctx.AppContext` first if a context for that application is not already the top context. While these contexts are pushed, the :data:`current_app`, :data:`g`, :data:`request`, and :data:`session` proxies are available to the original thread handling the request. Other contexts may be pushed to change the proxies during a request. While this is not a common pattern, it can be used in advanced applications to, for example, do internal redirects or chain different applications together. After the request is dispatched and a response is generated and sent, the request context is popped, which then pops the application context. Immediately before they are popped, the :meth:`~Flask.teardown_request` and :meth:`~Flask.teardown_appcontext` functions are executed. These execute even if an unhandled exception occurred during dispatch. .. _callbacks-and-errors: Callbacks and Errors -------------------- Flask dispatches a request in multiple stages which can affect the request, response, and how errors are handled. The contexts are active during all of these stages. A :class:`Blueprint` can add handlers for these events that are specific to the blueprint. The handlers for a blueprint will run if the blueprint owns the route that matches the request. #. Before each request, :meth:`~Flask.before_request` functions are called. If one of these functions return a value, the other functions are skipped. The return value is treated as the response and the view function is not called. #. If the :meth:`~Flask.before_request` functions did not return a response, the view function for the matched route is called and returns a response. #. The return value of the view is converted into an actual response object and passed to the :meth:`~Flask.after_request` functions. Each function returns a modified or new response object. #. After the response is returned, the contexts are popped, which calls the :meth:`~Flask.teardown_request` and :meth:`~Flask.teardown_appcontext` functions. These functions are called even if an unhandled exception was raised at any point above. If an exception is raised before the teardown functions, Flask tries to match it with an :meth:`~Flask.errorhandler` function to handle the exception and return a response. If no error handler is found, or the handler itself raises an exception, Flask returns a generic ``500 Internal Server Error`` response. The teardown functions are still called, and are passed the exception object. If debug mode is enabled, unhandled exceptions are not converted to a ``500`` response and instead are propagated to the WSGI server. This allows the development server to present the interactive debugger with the traceback. Teardown Callbacks ~~~~~~~~~~~~~~~~~~ The teardown callbacks are independent of the request dispatch, and are instead called by the contexts when they are popped. The functions are called even if there is an unhandled exception during dispatch, and for manually pushed contexts. This means there is no guarantee that any other parts of the request dispatch have run first. Be sure to write these functions in a way that does not depend on other callbacks and will not fail. During testing, it can be useful to defer popping the contexts after the request ends, so that their data can be accessed in the test function. Use the :meth:`~Flask.test_client` as a ``with`` block to preserve the contexts until the ``with`` block exits. .. code-block:: python from flask import Flask, request app = Flask(__name__) @app.route('/') def hello(): print('during view') return 'Hello, World!' @app.teardown_request def show_teardown(exception): print('after with block') with app.test_request_context(): print('during with block') # teardown functions are called after the context with block exits with app.test_client() as client: client.get('/') # the contexts are not popped even though the request ended print(request.path) # the contexts are popped and teardown functions are called after # the client with block exits Signals ~~~~~~~ The following signals are sent: #. :data:`request_started` is sent before the :meth:`~Flask.before_request` functions are called. #. :data:`request_finished` is sent after the :meth:`~Flask.after_request` functions are called. #. :data:`got_request_exception` is sent when an exception begins to be handled, but before an :meth:`~Flask.errorhandler` is looked up or called. #. :data:`request_tearing_down` is sent after the :meth:`~Flask.teardown_request` functions are called. .. _notes-on-proxies: Notes On Proxies ---------------- Some of the objects provided by Flask are proxies to other objects. The proxies are accessed in the same way for each worker thread, but point to the unique object bound to each worker behind the scenes as described on this page. Most of the time you don't have to care about that, but there are some exceptions where it is good to know that this object is actually a proxy: - The proxy objects cannot fake their type as the actual object types. If you want to perform instance checks, you have to do that on the object being proxied. - The reference to the proxied object is needed in some situations, such as sending :doc:`signals` or passing data to a background thread. If you need to access the underlying object that is proxied, use the :meth:`~werkzeug.local.LocalProxy._get_current_object` method:: app = current_app._get_current_object() my_signal.send(app) flask-3.1.1/docs/server.rst000066400000000000000000000072031501065614000156040ustar00rootroot00000000000000.. currentmodule:: flask Development Server ================== Flask provides a ``run`` command to run the application with a development server. In debug mode, this server provides an interactive debugger and will reload when code is changed. .. warning:: Do not use the development server when deploying to production. It is intended for use only during local development. It is not designed to be particularly efficient, stable, or secure. See :doc:`/deploying/index` for deployment options. Command Line ------------ The ``flask run`` CLI command is the recommended way to run the development server. Use the ``--app`` option to point to your application, and the ``--debug`` option to enable debug mode. .. code-block:: text $ flask --app hello run --debug This enables debug mode, including the interactive debugger and reloader, and then starts the server on http://localhost:5000/. Use ``flask run --help`` to see the available options, and :doc:`/cli` for detailed instructions about configuring and using the CLI. .. _address-already-in-use: Address already in use ~~~~~~~~~~~~~~~~~~~~~~ If another program is already using port 5000, you'll see an ``OSError`` when the server tries to start. It may have one of the following messages: - ``OSError: [Errno 98] Address already in use`` - ``OSError: [WinError 10013] An attempt was made to access a socket in a way forbidden by its access permissions`` Either identify and stop the other program, or use ``flask run --port 5001`` to pick a different port. You can use ``netstat`` or ``lsof`` to identify what process id is using a port, then use other operating system tools stop that process. The following example shows that process id 6847 is using port 5000. .. tabs:: .. tab:: ``netstat`` (Linux) .. code-block:: text $ netstat -nlp | grep 5000 tcp 0 0 127.0.0.1:5000 0.0.0.0:* LISTEN 6847/python .. tab:: ``lsof`` (macOS / Linux) .. code-block:: text $ lsof -P -i :5000 Python 6847 IPv4 TCP localhost:5000 (LISTEN) .. tab:: ``netstat`` (Windows) .. code-block:: text > netstat -ano | findstr 5000 TCP 127.0.0.1:5000 0.0.0.0:0 LISTENING 6847 macOS Monterey and later automatically starts a service that uses port 5000. You can choose to disable this service instead of using a different port by searching for "AirPlay Receiver" in System Preferences and toggling it off. Deferred Errors on Reload ~~~~~~~~~~~~~~~~~~~~~~~~~ When using the ``flask run`` command with the reloader, the server will continue to run even if you introduce syntax errors or other initialization errors into the code. Accessing the site will show the interactive debugger for the error, rather than crashing the server. If a syntax error is already present when calling ``flask run``, it will fail immediately and show the traceback rather than waiting until the site is accessed. This is intended to make errors more visible initially while still allowing the server to handle errors on reload. In Code ------- The development server can also be started from Python with the :meth:`Flask.run` method. This method takes arguments similar to the CLI options to control the server. The main difference from the CLI command is that the server will crash if there are errors when reloading. ``debug=True`` can be passed to enable debug mode. Place the call in a main block, otherwise it will interfere when trying to import and run the application with a production server later. .. code-block:: python if __name__ == "__main__": app.run(debug=True) .. code-block:: text $ python hello.py flask-3.1.1/docs/shell.rst000066400000000000000000000070761501065614000154150ustar00rootroot00000000000000Working with the Shell ====================== .. versionadded:: 0.3 One of the reasons everybody loves Python is the interactive shell. It basically allows you to execute Python commands in real time and immediately get results back. Flask itself does not come with an interactive shell, because it does not require any specific setup upfront, just import your application and start playing around. There are however some handy helpers to make playing around in the shell a more pleasant experience. The main issue with interactive console sessions is that you're not triggering a request like a browser does which means that :data:`~flask.g`, :data:`~flask.request` and others are not available. But the code you want to test might depend on them, so what can you do? This is where some helper functions come in handy. Keep in mind however that these functions are not only there for interactive shell usage, but also for unit testing and other situations that require a faked request context. Generally it's recommended that you read :doc:`reqcontext` first. Command Line Interface ---------------------- Starting with Flask 0.11 the recommended way to work with the shell is the ``flask shell`` command which does a lot of this automatically for you. For instance the shell is automatically initialized with a loaded application context. For more information see :doc:`/cli`. Creating a Request Context -------------------------- The easiest way to create a proper request context from the shell is by using the :attr:`~flask.Flask.test_request_context` method which creates us a :class:`~flask.ctx.RequestContext`: >>> ctx = app.test_request_context() Normally you would use the ``with`` statement to make this request object active, but in the shell it's easier to use the :meth:`~flask.ctx.RequestContext.push` and :meth:`~flask.ctx.RequestContext.pop` methods by hand: >>> ctx.push() From that point onwards you can work with the request object until you call `pop`: >>> ctx.pop() Firing Before/After Request --------------------------- By just creating a request context, you still don't have run the code that is normally run before a request. This might result in your database being unavailable if you are connecting to the database in a before-request callback or the current user not being stored on the :data:`~flask.g` object etc. This however can easily be done yourself. Just call :meth:`~flask.Flask.preprocess_request`: >>> ctx = app.test_request_context() >>> ctx.push() >>> app.preprocess_request() Keep in mind that the :meth:`~flask.Flask.preprocess_request` function might return a response object, in that case just ignore it. To shutdown a request, you need to trick a bit before the after request functions (triggered by :meth:`~flask.Flask.process_response`) operate on a response object: >>> app.process_response(app.response_class()) >>> ctx.pop() The functions registered as :meth:`~flask.Flask.teardown_request` are automatically called when the context is popped. So this is the perfect place to automatically tear down resources that were needed by the request context (such as database connections). Further Improving the Shell Experience -------------------------------------- If you like the idea of experimenting in a shell, create yourself a module with stuff you want to star import into your interactive session. There you could also define some more helper methods for common things such as initializing the database, dropping tables etc. Just put them into a module (like `shelltools`) and import from there: >>> from shelltools import * flask-3.1.1/docs/signals.rst000066400000000000000000000140531501065614000157370ustar00rootroot00000000000000Signals ======= Signals are a lightweight way to notify subscribers of certain events during the lifecycle of the application and each request. When an event occurs, it emits the signal, which calls each subscriber. Signals are implemented by the `Blinker`_ library. See its documentation for detailed information. Flask provides some built-in signals. Extensions may provide their own. Many signals mirror Flask's decorator-based callbacks with similar names. For example, the :data:`.request_started` signal is similar to the :meth:`~.Flask.before_request` decorator. The advantage of signals over handlers is that they can be subscribed to temporarily, and can't directly affect the application. This is useful for testing, metrics, auditing, and more. For example, if you want to know what templates were rendered at what parts of what requests, there is a signal that will notify you of that information. Core Signals ------------ See :ref:`core-signals-list` for a list of all built-in signals. The :doc:`lifecycle` page also describes the order that signals and decorators execute. Subscribing to Signals ---------------------- To subscribe to a signal, you can use the :meth:`~blinker.base.Signal.connect` method of a signal. The first argument is the function that should be called when the signal is emitted, the optional second argument specifies a sender. To unsubscribe from a signal, you can use the :meth:`~blinker.base.Signal.disconnect` method. For all core Flask signals, the sender is the application that issued the signal. When you subscribe to a signal, be sure to also provide a sender unless you really want to listen for signals from all applications. This is especially true if you are developing an extension. For example, here is a helper context manager that can be used in a unit test to determine which templates were rendered and what variables were passed to the template:: from flask import template_rendered from contextlib import contextmanager @contextmanager def captured_templates(app): recorded = [] def record(sender, template, context, **extra): recorded.append((template, context)) template_rendered.connect(record, app) try: yield recorded finally: template_rendered.disconnect(record, app) This can now easily be paired with a test client:: with captured_templates(app) as templates: rv = app.test_client().get('/') assert rv.status_code == 200 assert len(templates) == 1 template, context = templates[0] assert template.name == 'index.html' assert len(context['items']) == 10 Make sure to subscribe with an extra ``**extra`` argument so that your calls don't fail if Flask introduces new arguments to the signals. All the template rendering in the code issued by the application `app` in the body of the ``with`` block will now be recorded in the `templates` variable. Whenever a template is rendered, the template object as well as context are appended to it. Additionally there is a convenient helper method (:meth:`~blinker.base.Signal.connected_to`) that allows you to temporarily subscribe a function to a signal with a context manager on its own. Because the return value of the context manager cannot be specified that way, you have to pass the list in as an argument:: from flask import template_rendered def captured_templates(app, recorded, **extra): def record(sender, template, context): recorded.append((template, context)) return template_rendered.connected_to(record, app) The example above would then look like this:: templates = [] with captured_templates(app, templates, **extra): ... template, context = templates[0] Creating Signals ---------------- If you want to use signals in your own application, you can use the blinker library directly. The most common use case are named signals in a custom :class:`~blinker.base.Namespace`. This is what is recommended most of the time:: from blinker import Namespace my_signals = Namespace() Now you can create new signals like this:: model_saved = my_signals.signal('model-saved') The name for the signal here makes it unique and also simplifies debugging. You can access the name of the signal with the :attr:`~blinker.base.NamedSignal.name` attribute. .. _signals-sending: Sending Signals --------------- If you want to emit a signal, you can do so by calling the :meth:`~blinker.base.Signal.send` method. It accepts a sender as first argument and optionally some keyword arguments that are forwarded to the signal subscribers:: class Model(object): ... def save(self): model_saved.send(self) Try to always pick a good sender. If you have a class that is emitting a signal, pass ``self`` as sender. If you are emitting a signal from a random function, you can pass ``current_app._get_current_object()`` as sender. .. admonition:: Passing Proxies as Senders Never pass :data:`~flask.current_app` as sender to a signal. Use ``current_app._get_current_object()`` instead. The reason for this is that :data:`~flask.current_app` is a proxy and not the real application object. Signals and Flask's Request Context ----------------------------------- Signals fully support :doc:`reqcontext` when receiving signals. Context-local variables are consistently available between :data:`~flask.request_started` and :data:`~flask.request_finished`, so you can rely on :class:`flask.g` and others as needed. Note the limitations described in :ref:`signals-sending` and the :data:`~flask.request_tearing_down` signal. Decorator Based Signal Subscriptions ------------------------------------ You can also easily subscribe to signals by using the :meth:`~blinker.base.NamedSignal.connect_via` decorator:: from flask import template_rendered @template_rendered.connect_via(app) def when_template_rendered(sender, template, context, **extra): print(f'Template {template.name} is rendered with {context}') .. _blinker: https://pypi.org/project/blinker/ flask-3.1.1/docs/templating.rst000066400000000000000000000171531501065614000164470ustar00rootroot00000000000000Templates ========= Flask leverages Jinja2 as its template engine. You are obviously free to use a different template engine, but you still have to install Jinja2 to run Flask itself. This requirement is necessary to enable rich extensions. An extension can depend on Jinja2 being present. This section only gives a very quick introduction into how Jinja2 is integrated into Flask. If you want information on the template engine's syntax itself, head over to the official `Jinja2 Template Documentation `_ for more information. Jinja Setup ----------- Unless customized, Jinja2 is configured by Flask as follows: - autoescaping is enabled for all templates ending in ``.html``, ``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using :func:`~flask.templating.render_template`. - autoescaping is enabled for all strings when using :func:`~flask.templating.render_template_string`. - a template has the ability to opt in/out autoescaping with the ``{% autoescape %}`` tag. - Flask inserts a couple of global functions and helpers into the Jinja2 context, additionally to the values that are present by default. Standard Context ---------------- The following global variables are available within Jinja2 templates by default: .. data:: config :noindex: The current configuration object (:data:`flask.Flask.config`) .. versionadded:: 0.6 .. versionchanged:: 0.10 This is now always available, even in imported templates. .. data:: request :noindex: The current request object (:class:`flask.request`). This variable is unavailable if the template was rendered without an active request context. .. data:: session :noindex: The current session object (:class:`flask.session`). This variable is unavailable if the template was rendered without an active request context. .. data:: g :noindex: The request-bound object for global variables (:data:`flask.g`). This variable is unavailable if the template was rendered without an active request context. .. function:: url_for :noindex: The :func:`flask.url_for` function. .. function:: get_flashed_messages :noindex: The :func:`flask.get_flashed_messages` function. .. admonition:: The Jinja Context Behavior These variables are added to the context of variables, they are not global variables. The difference is that by default these will not show up in the context of imported templates. This is partially caused by performance considerations, partially to keep things explicit. What does this mean for you? If you have a macro you want to import, that needs to access the request object you have two possibilities: 1. you explicitly pass the request to the macro as parameter, or the attribute of the request object you are interested in. 2. you import the macro "with context". Importing with context looks like this: .. sourcecode:: jinja {% from '_helpers.html' import my_macro with context %} Controlling Autoescaping ------------------------ Autoescaping is the concept of automatically escaping special characters for you. Special characters in the sense of HTML (or XML, and thus XHTML) are ``&``, ``>``, ``<``, ``"`` as well as ``'``. Because these characters carry specific meanings in documents on their own you have to replace them by so called "entities" if you want to use them for text. Not doing so would not only cause user frustration by the inability to use these characters in text, but can also lead to security problems. (see :ref:`security-xss`) Sometimes however you will need to disable autoescaping in templates. This can be the case if you want to explicitly inject HTML into pages, for example if they come from a system that generates secure HTML like a markdown to HTML converter. There are three ways to accomplish that: - In the Python code, wrap the HTML string in a :class:`~markupsafe.Markup` object before passing it to the template. This is in general the recommended way. - Inside the template, use the ``|safe`` filter to explicitly mark a string as safe HTML (``{{ myvariable|safe }}``) - Temporarily disable the autoescape system altogether. To disable the autoescape system in templates, you can use the ``{% autoescape %}`` block: .. sourcecode:: html+jinja {% autoescape false %}

autoescaping is disabled here

{{ will_not_be_escaped }} {% endautoescape %} Whenever you do this, please be very cautious about the variables you are using in this block. .. _registering-filters: Registering Filters ------------------- If you want to register your own filters in Jinja2 you have two ways to do that. You can either put them by hand into the :attr:`~flask.Flask.jinja_env` of the application or use the :meth:`~flask.Flask.template_filter` decorator. The two following examples work the same and both reverse an object:: @app.template_filter('reverse') def reverse_filter(s): return s[::-1] def reverse_filter(s): return s[::-1] app.jinja_env.filters['reverse'] = reverse_filter In case of the decorator the argument is optional if you want to use the function name as name of the filter. Once registered, you can use the filter in your templates in the same way as Jinja2's builtin filters, for example if you have a Python list in context called `mylist`:: {% for x in mylist | reverse %} {% endfor %} Context Processors ------------------ To inject new variables automatically into the context of a template, context processors exist in Flask. Context processors run before the template is rendered and have the ability to inject new values into the template context. A context processor is a function that returns a dictionary. The keys and values of this dictionary are then merged with the template context, for all templates in the app:: @app.context_processor def inject_user(): return dict(user=g.user) The context processor above makes a variable called `user` available in the template with the value of `g.user`. This example is not very interesting because `g` is available in templates anyways, but it gives an idea how this works. Variables are not limited to values; a context processor can also make functions available to templates (since Python allows passing around functions):: @app.context_processor def utility_processor(): def format_price(amount, currency="€"): return f"{amount:.2f}{currency}" return dict(format_price=format_price) The context processor above makes the `format_price` function available to all templates:: {{ format_price(0.33) }} You could also build `format_price` as a template filter (see :ref:`registering-filters`), but this demonstrates how to pass functions in a context processor. Streaming --------- It can be useful to not render the whole template as one complete string, instead render it as a stream, yielding smaller incremental strings. This can be used for streaming HTML in chunks to speed up initial page load, or to save memory when rendering a very large template. The Jinja2 template engine supports rendering a template piece by piece, returning an iterator of strings. Flask provides the :func:`~flask.stream_template` and :func:`~flask.stream_template_string` functions to make this easier to use. .. code-block:: python from flask import stream_template @app.get("/timeline") def timeline(): return stream_template("timeline.html") These functions automatically apply the :func:`~flask.stream_with_context` wrapper if a request is active, so that it remains available in the template. flask-3.1.1/docs/testing.rst000066400000000000000000000243201501065614000157520ustar00rootroot00000000000000Testing Flask Applications ========================== Flask provides utilities for testing an application. This documentation goes over techniques for working with different parts of the application in tests. We will use the `pytest`_ framework to set up and run our tests. .. code-block:: text $ pip install pytest .. _pytest: https://docs.pytest.org/ The :doc:`tutorial ` goes over how to write tests for 100% coverage of the sample Flaskr blog application. See :doc:`the tutorial on tests ` for a detailed explanation of specific tests for an application. Identifying Tests ----------------- Tests are typically located in the ``tests`` folder. Tests are functions that start with ``test_``, in Python modules that start with ``test_``. Tests can also be further grouped in classes that start with ``Test``. It can be difficult to know what to test. Generally, try to test the code that you write, not the code of libraries that you use, since they are already tested. Try to extract complex behaviors as separate functions to test individually. Fixtures -------- Pytest *fixtures* allow writing pieces of code that are reusable across tests. A simple fixture returns a value, but a fixture can also do setup, yield a value, then do teardown. Fixtures for the application, test client, and CLI runner are shown below, they can be placed in ``tests/conftest.py``. If you're using an :doc:`application factory `, define an ``app`` fixture to create and configure an app instance. You can add code before and after the ``yield`` to set up and tear down other resources, such as creating and clearing a database. If you're not using a factory, you already have an app object you can import and configure directly. You can still use an ``app`` fixture to set up and tear down resources. .. code-block:: python import pytest from my_project import create_app @pytest.fixture() def app(): app = create_app() app.config.update({ "TESTING": True, }) # other setup can go here yield app # clean up / reset resources here @pytest.fixture() def client(app): return app.test_client() @pytest.fixture() def runner(app): return app.test_cli_runner() Sending Requests with the Test Client ------------------------------------- The test client makes requests to the application without running a live server. Flask's client extends :doc:`Werkzeug's client `, see those docs for additional information. The ``client`` has methods that match the common HTTP request methods, such as ``client.get()`` and ``client.post()``. They take many arguments for building the request; you can find the full documentation in :class:`~werkzeug.test.EnvironBuilder`. Typically you'll use ``path``, ``query_string``, ``headers``, and ``data`` or ``json``. To make a request, call the method the request should use with the path to the route to test. A :class:`~werkzeug.test.TestResponse` is returned to examine the response data. It has all the usual properties of a response object. You'll usually look at ``response.data``, which is the bytes returned by the view. If you want to use text, Werkzeug 2.1 provides ``response.text``, or use ``response.get_data(as_text=True)``. .. code-block:: python def test_request_example(client): response = client.get("/posts") assert b"

Hello, World!

" in response.data Pass a dict ``query_string={"key": "value", ...}`` to set arguments in the query string (after the ``?`` in the URL). Pass a dict ``headers={}`` to set request headers. To send a request body in a POST or PUT request, pass a value to ``data``. If raw bytes are passed, that exact body is used. Usually, you'll pass a dict to set form data. Form Data ~~~~~~~~~ To send form data, pass a dict to ``data``. The ``Content-Type`` header will be set to ``multipart/form-data`` or ``application/x-www-form-urlencoded`` automatically. If a value is a file object opened for reading bytes (``"rb"`` mode), it will be treated as an uploaded file. To change the detected filename and content type, pass a ``(file, filename, content_type)`` tuple. File objects will be closed after making the request, so they do not need to use the usual ``with open() as f:`` pattern. It can be useful to store files in a ``tests/resources`` folder, then use ``pathlib.Path`` to get files relative to the current test file. .. code-block:: python from pathlib import Path # get the resources folder in the tests folder resources = Path(__file__).parent / "resources" def test_edit_user(client): response = client.post("/user/2/edit", data={ "name": "Flask", "theme": "dark", "picture": (resources / "picture.png").open("rb"), }) assert response.status_code == 200 JSON Data ~~~~~~~~~ To send JSON data, pass an object to ``json``. The ``Content-Type`` header will be set to ``application/json`` automatically. Similarly, if the response contains JSON data, the ``response.json`` attribute will contain the deserialized object. .. code-block:: python def test_json_data(client): response = client.post("/graphql", json={ "query": """ query User($id: String!) { user(id: $id) { name theme picture_url } } """, variables={"id": 2}, }) assert response.json["data"]["user"]["name"] == "Flask" Following Redirects ------------------- By default, the client does not make additional requests if the response is a redirect. By passing ``follow_redirects=True`` to a request method, the client will continue to make requests until a non-redirect response is returned. :attr:`TestResponse.history ` is a tuple of the responses that led up to the final response. Each response has a :attr:`~werkzeug.test.TestResponse.request` attribute which records the request that produced that response. .. code-block:: python def test_logout_redirect(client): response = client.get("/logout", follow_redirects=True) # Check that there was one redirect response. assert len(response.history) == 1 # Check that the second request was to the index page. assert response.request.path == "/index" Accessing and Modifying the Session ----------------------------------- To access Flask's context variables, mainly :data:`~flask.session`, use the client in a ``with`` statement. The app and request context will remain active *after* making a request, until the ``with`` block ends. .. code-block:: python from flask import session def test_access_session(client): with client: client.post("/auth/login", data={"username": "flask"}) # session is still accessible assert session["user_id"] == 1 # session is no longer accessible If you want to access or set a value in the session *before* making a request, use the client's :meth:`~flask.testing.FlaskClient.session_transaction` method in a ``with`` statement. It returns a session object, and will save the session once the block ends. .. code-block:: python from flask import session def test_modify_session(client): with client.session_transaction() as session: # set a user id without going through the login route session["user_id"] = 1 # session is saved now response = client.get("/users/me") assert response.json["username"] == "flask" .. _testing-cli: Running Commands with the CLI Runner ------------------------------------ Flask provides :meth:`~flask.Flask.test_cli_runner` to create a :class:`~flask.testing.FlaskCliRunner`, which runs CLI commands in isolation and captures the output in a :class:`~click.testing.Result` object. Flask's runner extends :doc:`Click's runner `, see those docs for additional information. Use the runner's :meth:`~flask.testing.FlaskCliRunner.invoke` method to call commands in the same way they would be called with the ``flask`` command from the command line. .. code-block:: python import click @app.cli.command("hello") @click.option("--name", default="World") def hello_command(name): click.echo(f"Hello, {name}!") def test_hello_command(runner): result = runner.invoke(args="hello") assert "World" in result.output result = runner.invoke(args=["hello", "--name", "Flask"]) assert "Flask" in result.output Tests that depend on an Active Context -------------------------------------- You may have functions that are called from views or commands, that expect an active :doc:`application context ` or :doc:`request context ` because they access ``request``, ``session``, or ``current_app``. Rather than testing them by making a request or invoking the command, you can create and activate a context directly. Use ``with app.app_context()`` to push an application context. For example, database extensions usually require an active app context to make queries. .. code-block:: python def test_db_post_model(app): with app.app_context(): post = db.session.query(Post).get(1) Use ``with app.test_request_context()`` to push a request context. It takes the same arguments as the test client's request methods. .. code-block:: python def test_validate_user_edit(app): with app.test_request_context( "/user/2/edit", method="POST", data={"name": ""} ): # call a function that accesses `request` messages = validate_edit_user() assert messages["name"][0] == "Name cannot be empty." Creating a test request context doesn't run any of the Flask dispatching code, so ``before_request`` functions are not called. If you need to call these, usually it's better to make a full request instead. However, it's possible to call them manually. .. code-block:: python def test_auth_token(app): with app.test_request_context("/user/2/edit", headers={"X-Auth-Token": "1"}): app.preprocess_request() assert g.user.name == "Flask" flask-3.1.1/docs/tutorial/000077500000000000000000000000001501065614000154055ustar00rootroot00000000000000flask-3.1.1/docs/tutorial/blog.rst000066400000000000000000000261731501065614000170730ustar00rootroot00000000000000.. currentmodule:: flask Blog Blueprint ============== You'll use the same techniques you learned about when writing the authentication blueprint to write the blog blueprint. The blog should list all posts, allow logged in users to create posts, and allow the author of a post to edit or delete it. As you implement each view, keep the development server running. As you save your changes, try going to the URL in your browser and testing them out. The Blueprint ------------- Define the blueprint and register it in the application factory. .. code-block:: python :caption: ``flaskr/blog.py`` from flask import ( Blueprint, flash, g, redirect, render_template, request, url_for ) from werkzeug.exceptions import abort from flaskr.auth import login_required from flaskr.db import get_db bp = Blueprint('blog', __name__) Import and register the blueprint from the factory using :meth:`app.register_blueprint() `. Place the new code at the end of the factory function before returning the app. .. code-block:: python :caption: ``flaskr/__init__.py`` def create_app(): app = ... # existing code omitted from . import blog app.register_blueprint(blog.bp) app.add_url_rule('/', endpoint='index') return app Unlike the auth blueprint, the blog blueprint does not have a ``url_prefix``. So the ``index`` view will be at ``/``, the ``create`` view at ``/create``, and so on. The blog is the main feature of Flaskr, so it makes sense that the blog index will be the main index. However, the endpoint for the ``index`` view defined below will be ``blog.index``. Some of the authentication views referred to a plain ``index`` endpoint. :meth:`app.add_url_rule() ` associates the endpoint name ``'index'`` with the ``/`` url so that ``url_for('index')`` or ``url_for('blog.index')`` will both work, generating the same ``/`` URL either way. In another application you might give the blog blueprint a ``url_prefix`` and define a separate ``index`` view in the application factory, similar to the ``hello`` view. Then the ``index`` and ``blog.index`` endpoints and URLs would be different. Index ----- The index will show all of the posts, most recent first. A ``JOIN`` is used so that the author information from the ``user`` table is available in the result. .. code-block:: python :caption: ``flaskr/blog.py`` @bp.route('/') def index(): db = get_db() posts = db.execute( 'SELECT p.id, title, body, created, author_id, username' ' FROM post p JOIN user u ON p.author_id = u.id' ' ORDER BY created DESC' ).fetchall() return render_template('blog/index.html', posts=posts) .. code-block:: html+jinja :caption: ``flaskr/templates/blog/index.html`` {% extends 'base.html' %} {% block header %}

{% block title %}Posts{% endblock %}

{% if g.user %} New {% endif %} {% endblock %} {% block content %} {% for post in posts %}

{{ post['title'] }}

by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
{% if g.user['id'] == post['author_id'] %} Edit {% endif %}

{{ post['body'] }}

{% if not loop.last %}
{% endif %} {% endfor %} {% endblock %} When a user is logged in, the ``header`` block adds a link to the ``create`` view. When the user is the author of a post, they'll see an "Edit" link to the ``update`` view for that post. ``loop.last`` is a special variable available inside `Jinja for loops`_. It's used to display a line after each post except the last one, to visually separate them. .. _Jinja for loops: https://jinja.palletsprojects.com/templates/#for Create ------ The ``create`` view works the same as the auth ``register`` view. Either the form is displayed, or the posted data is validated and the post is added to the database or an error is shown. The ``login_required`` decorator you wrote earlier is used on the blog views. A user must be logged in to visit these views, otherwise they will be redirected to the login page. .. code-block:: python :caption: ``flaskr/blog.py`` @bp.route('/create', methods=('GET', 'POST')) @login_required def create(): if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) else: db = get_db() db.execute( 'INSERT INTO post (title, body, author_id)' ' VALUES (?, ?, ?)', (title, body, g.user['id']) ) db.commit() return redirect(url_for('blog.index')) return render_template('blog/create.html') .. code-block:: html+jinja :caption: ``flaskr/templates/blog/create.html`` {% extends 'base.html' %} {% block header %}

{% block title %}New Post{% endblock %}

{% endblock %} {% block content %}
{% endblock %} Update ------ Both the ``update`` and ``delete`` views will need to fetch a ``post`` by ``id`` and check if the author matches the logged in user. To avoid duplicating code, you can write a function to get the ``post`` and call it from each view. .. code-block:: python :caption: ``flaskr/blog.py`` def get_post(id, check_author=True): post = get_db().execute( 'SELECT p.id, title, body, created, author_id, username' ' FROM post p JOIN user u ON p.author_id = u.id' ' WHERE p.id = ?', (id,) ).fetchone() if post is None: abort(404, f"Post id {id} doesn't exist.") if check_author and post['author_id'] != g.user['id']: abort(403) return post :func:`abort` will raise a special exception that returns an HTTP status code. It takes an optional message to show with the error, otherwise a default message is used. ``404`` means "Not Found", and ``403`` means "Forbidden". (``401`` means "Unauthorized", but you redirect to the login page instead of returning that status.) The ``check_author`` argument is defined so that the function can be used to get a ``post`` without checking the author. This would be useful if you wrote a view to show an individual post on a page, where the user doesn't matter because they're not modifying the post. .. code-block:: python :caption: ``flaskr/blog.py`` @bp.route('//update', methods=('GET', 'POST')) @login_required def update(id): post = get_post(id) if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) else: db = get_db() db.execute( 'UPDATE post SET title = ?, body = ?' ' WHERE id = ?', (title, body, id) ) db.commit() return redirect(url_for('blog.index')) return render_template('blog/update.html', post=post) Unlike the views you've written so far, the ``update`` function takes an argument, ``id``. That corresponds to the ```` in the route. A real URL will look like ``/1/update``. Flask will capture the ``1``, ensure it's an :class:`int`, and pass it as the ``id`` argument. If you don't specify ``int:`` and instead do ````, it will be a string. To generate a URL to the update page, :func:`url_for` needs to be passed the ``id`` so it knows what to fill in: ``url_for('blog.update', id=post['id'])``. This is also in the ``index.html`` file above. The ``create`` and ``update`` views look very similar. The main difference is that the ``update`` view uses a ``post`` object and an ``UPDATE`` query instead of an ``INSERT``. With some clever refactoring, you could use one view and template for both actions, but for the tutorial it's clearer to keep them separate. .. code-block:: html+jinja :caption: ``flaskr/templates/blog/update.html`` {% extends 'base.html' %} {% block header %}

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

{% endblock %} {% block content %}

{% endblock %} This template has two forms. The first posts the edited data to the current page (``//update``). The other form contains only a button and specifies an ``action`` attribute that posts to the delete view instead. The button uses some JavaScript to show a confirmation dialog before submitting. The pattern ``{{ request.form['title'] or post['title'] }}`` is used to choose what data appears in the form. When the form hasn't been submitted, the original ``post`` data appears, but if invalid form data was posted you want to display that so the user can fix the error, so ``request.form`` is used instead. :data:`request` is another variable that's automatically available in templates. Delete ------ The delete view doesn't have its own template, the delete button is part of ``update.html`` and posts to the ``//delete`` URL. Since there is no template, it will only handle the ``POST`` method and then redirect to the ``index`` view. .. code-block:: python :caption: ``flaskr/blog.py`` @bp.route('//delete', methods=('POST',)) @login_required def delete(id): get_post(id) db = get_db() db.execute('DELETE FROM post WHERE id = ?', (id,)) db.commit() return redirect(url_for('blog.index')) Congratulations, you've now finished writing your application! Take some time to try out everything in the browser. However, there's still more to do before the project is complete. Continue to :doc:`install`. flask-3.1.1/docs/tutorial/database.rst000066400000000000000000000155611501065614000177130ustar00rootroot00000000000000.. currentmodule:: flask Define and Access the Database ============================== The application will use a `SQLite`_ database to store users and posts. Python comes with built-in support for SQLite in the :mod:`sqlite3` module. SQLite is convenient because it doesn't require setting up a separate database server and is built-in to Python. However, if concurrent requests try to write to the database at the same time, they will slow down as each write happens sequentially. Small applications won't notice this. Once you become big, you may want to switch to a different database. The tutorial doesn't go into detail about SQL. If you are not familiar with it, the SQLite docs describe the `language`_. .. _SQLite: https://sqlite.org/about.html .. _language: https://sqlite.org/lang.html Connect to the Database ----------------------- The first thing to do when working with a SQLite database (and most other Python database libraries) is to create a connection to it. Any queries and operations are performed using the connection, which is closed after the work is finished. In web applications this connection is typically tied to the request. It is created at some point when handling a request, and closed before the response is sent. .. code-block:: python :caption: ``flaskr/db.py`` import sqlite3 from datetime import datetime import click from flask import current_app, g def get_db(): if 'db' not in g: g.db = sqlite3.connect( current_app.config['DATABASE'], detect_types=sqlite3.PARSE_DECLTYPES ) g.db.row_factory = sqlite3.Row return g.db def close_db(e=None): db = g.pop('db', None) if db is not None: db.close() :data:`g` is a special object that is unique for each request. It is used to store data that might be accessed by multiple functions during the request. The connection is stored and reused instead of creating a new connection if ``get_db`` is called a second time in the same request. :data:`current_app` is another special object that points to the Flask application handling the request. Since you used an application factory, there is no application object when writing the rest of your code. ``get_db`` will be called when the application has been created and is handling a request, so :data:`current_app` can be used. :func:`sqlite3.connect` establishes a connection to the file pointed at by the ``DATABASE`` configuration key. This file doesn't have to exist yet, and won't until you initialize the database later. :class:`sqlite3.Row` tells the connection to return rows that behave like dicts. This allows accessing the columns by name. ``close_db`` checks if a connection was created by checking if ``g.db`` was set. If the connection exists, it is closed. Further down you will tell your application about the ``close_db`` function in the application factory so that it is called after each request. Create the Tables ----------------- In SQLite, data is stored in *tables* and *columns*. These need to be created before you can store and retrieve data. Flaskr will store users in the ``user`` table, and posts in the ``post`` table. Create a file with the SQL commands needed to create empty tables: .. code-block:: sql :caption: ``flaskr/schema.sql`` DROP TABLE IF EXISTS user; DROP TABLE IF EXISTS post; CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL ); CREATE TABLE post ( id INTEGER PRIMARY KEY AUTOINCREMENT, author_id INTEGER NOT NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title TEXT NOT NULL, body TEXT NOT NULL, FOREIGN KEY (author_id) REFERENCES user (id) ); Add the Python functions that will run these SQL commands to the ``db.py`` file: .. code-block:: python :caption: ``flaskr/db.py`` def init_db(): db = get_db() with current_app.open_resource('schema.sql') as f: db.executescript(f.read().decode('utf8')) @click.command('init-db') def init_db_command(): """Clear the existing data and create new tables.""" init_db() click.echo('Initialized the database.') sqlite3.register_converter( "timestamp", lambda v: datetime.fromisoformat(v.decode()) ) :meth:`open_resource() ` opens a file relative to the ``flaskr`` package, which is useful since you won't necessarily know where that location is when deploying the application later. ``get_db`` returns a database connection, which is used to execute the commands read from the file. :func:`click.command` defines a command line command called ``init-db`` that calls the ``init_db`` function and shows a success message to the user. You can read :doc:`/cli` to learn more about writing commands. The call to :func:`sqlite3.register_converter` tells Python how to interpret timestamp values in the database. We convert the value to a :class:`datetime.datetime`. Register with the Application ----------------------------- The ``close_db`` and ``init_db_command`` functions need to be registered with the application instance; otherwise, they won't be used by the application. However, since you're using a factory function, that instance isn't available when writing the functions. Instead, write a function that takes an application and does the registration. .. code-block:: python :caption: ``flaskr/db.py`` def init_app(app): app.teardown_appcontext(close_db) app.cli.add_command(init_db_command) :meth:`app.teardown_appcontext() ` tells Flask to call that function when cleaning up after returning the response. :meth:`app.cli.add_command() ` adds a new command that can be called with the ``flask`` command. Import and call this function from the factory. Place the new code at the end of the factory function before returning the app. .. code-block:: python :caption: ``flaskr/__init__.py`` def create_app(): app = ... # existing code omitted from . import db db.init_app(app) return app Initialize the Database File ---------------------------- Now that ``init-db`` has been registered with the app, it can be called using the ``flask`` command, similar to the ``run`` command from the previous page. .. note:: If you're still running the server from the previous page, you can either stop the server, or run this command in a new terminal. If you use a new terminal, remember to change to your project directory and activate the env as described in :doc:`/installation`. Run the ``init-db`` command: .. code-block:: none $ flask --app flaskr init-db Initialized the database. There will now be a ``flaskr.sqlite`` file in the ``instance`` folder in your project. Continue to :doc:`views`. flask-3.1.1/docs/tutorial/deploy.rst000066400000000000000000000071631501065614000174420ustar00rootroot00000000000000Deploy to Production ==================== This part of the tutorial assumes you have a server that you want to deploy your application to. It gives an overview of how to create the distribution file and install it, but won't go into specifics about what server or software to use. You can set up a new environment on your development computer to try out the instructions below, but probably shouldn't use it for hosting a real public application. See :doc:`/deploying/index` for a list of many different ways to host your application. Build and Install ----------------- When you want to deploy your application elsewhere, you build a *wheel* (``.whl``) file. Install and use the ``build`` tool to do this. .. code-block:: none $ pip install build $ python -m build --wheel You can find the file in ``dist/flaskr-1.0.0-py3-none-any.whl``. The file name is in the format of {project name}-{version}-{python tag} -{abi tag}-{platform tag}. Copy this file to another machine, :ref:`set up a new virtualenv `, then install the file with ``pip``. .. code-block:: none $ pip install flaskr-1.0.0-py3-none-any.whl Pip will install your project along with its dependencies. Since this is a different machine, you need to run ``init-db`` again to create the database in the instance folder. .. code-block:: text $ flask --app flaskr init-db When Flask detects that it's installed (not in editable mode), it uses a different directory for the instance folder. You can find it at ``.venv/var/flaskr-instance`` instead. Configure the Secret Key ------------------------ In the beginning of the tutorial that you gave a default value for :data:`SECRET_KEY`. This should be changed to some random bytes in production. Otherwise, attackers could use the public ``'dev'`` key to modify the session cookie, or anything else that uses the secret key. You can use the following command to output a random secret key: .. code-block:: none $ python -c 'import secrets; print(secrets.token_hex())' '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' Create the ``config.py`` file in the instance folder, which the factory will read from if it exists. Copy the generated value into it. .. code-block:: python :caption: ``.venv/var/flaskr-instance/config.py`` SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' You can also set any other necessary configuration here, although ``SECRET_KEY`` is the only one needed for Flaskr. Run with a Production Server ---------------------------- When running publicly rather than in development, you should not use the built-in development server (``flask run``). The development server is provided by Werkzeug for convenience, but is not designed to be particularly efficient, stable, or secure. Instead, use a production WSGI server. For example, to use `Waitress`_, first install it in the virtual environment: .. code-block:: none $ pip install waitress You need to tell Waitress about your application, but it doesn't use ``--app`` like ``flask run`` does. You need to tell it to import and call the application factory to get an application object. .. code-block:: none $ waitress-serve --call 'flaskr:create_app' Serving on http://0.0.0.0:8080 See :doc:`/deploying/index` for a list of many different ways to host your application. Waitress is just an example, chosen for the tutorial because it supports both Windows and Linux. There are many more WSGI servers and deployment options that you may choose for your project. .. _Waitress: https://docs.pylonsproject.org/projects/waitress/en/stable/ Continue to :doc:`next`. flask-3.1.1/docs/tutorial/factory.rst000066400000000000000000000134411501065614000176110ustar00rootroot00000000000000.. currentmodule:: flask Application Setup ================= A Flask application is an instance of the :class:`Flask` class. Everything about the application, such as configuration and URLs, will be registered with this class. The most straightforward way to create a Flask application is to create a global :class:`Flask` instance directly at the top of your code, like how the "Hello, World!" example did on the previous page. While this is simple and useful in some cases, it can cause some tricky issues as the project grows. Instead of creating a :class:`Flask` instance globally, you will create it inside a function. This function is known as the *application factory*. Any configuration, registration, and other setup the application needs will happen inside the function, then the application will be returned. The Application Factory ----------------------- It's time to start coding! Create the ``flaskr`` directory and add the ``__init__.py`` file. The ``__init__.py`` serves double duty: it will contain the application factory, and it tells Python that the ``flaskr`` directory should be treated as a package. .. code-block:: none $ mkdir flaskr .. code-block:: python :caption: ``flaskr/__init__.py`` import os from flask import Flask def create_app(test_config=None): # create and configure the app app = Flask(__name__, instance_relative_config=True) app.config.from_mapping( SECRET_KEY='dev', DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), ) if test_config is None: # load the instance config, if it exists, when not testing app.config.from_pyfile('config.py', silent=True) else: # load the test config if passed in app.config.from_mapping(test_config) # ensure the instance folder exists try: os.makedirs(app.instance_path) except OSError: pass # a simple page that says hello @app.route('/hello') def hello(): return 'Hello, World!' return app ``create_app`` is the application factory function. You'll add to it later in the tutorial, but it already does a lot. #. ``app = Flask(__name__, instance_relative_config=True)`` creates the :class:`Flask` instance. * ``__name__`` is the name of the current Python module. The app needs to know where it's located to set up some paths, and ``__name__`` is a convenient way to tell it that. * ``instance_relative_config=True`` tells the app that configuration files are relative to the :ref:`instance folder `. The instance folder is located outside the ``flaskr`` package and can hold local data that shouldn't be committed to version control, such as configuration secrets and the database file. #. :meth:`app.config.from_mapping() ` sets some default configuration that the app will use: * :data:`SECRET_KEY` is used by Flask and extensions to keep data safe. It's set to ``'dev'`` to provide a convenient value during development, but it should be overridden with a random value when deploying. * ``DATABASE`` is the path where the SQLite database file will be saved. It's under :attr:`app.instance_path `, which is the path that Flask has chosen for the instance folder. You'll learn more about the database in the next section. #. :meth:`app.config.from_pyfile() ` overrides the default configuration with values taken from the ``config.py`` file in the instance folder if it exists. For example, when deploying, this can be used to set a real ``SECRET_KEY``. * ``test_config`` can also be passed to the factory, and will be used instead of the instance configuration. This is so the tests you'll write later in the tutorial can be configured independently of any development values you have configured. #. :func:`os.makedirs` ensures that :attr:`app.instance_path ` exists. Flask doesn't create the instance folder automatically, but it needs to be created because your project will create the SQLite database file there. #. :meth:`@app.route() ` creates a simple route so you can see the application working before getting into the rest of the tutorial. It creates a connection between the URL ``/hello`` and a function that returns a response, the string ``'Hello, World!'`` in this case. Run The Application ------------------- Now you can run your application using the ``flask`` command. From the terminal, tell Flask where to find your application, then run it in debug mode. Remember, you should still be in the top-level ``flask-tutorial`` directory, not the ``flaskr`` package. Debug mode shows an interactive debugger whenever a page raises an exception, and restarts the server whenever you make changes to the code. You can leave it running and just reload the browser page as you follow the tutorial. .. code-block:: text $ flask --app flaskr run --debug You'll see output similar to this: .. code-block:: text * Serving Flask app "flaskr" * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: nnn-nnn-nnn Visit http://127.0.0.1:5000/hello in a browser and you should see the "Hello, World!" message. Congratulations, you're now running your Flask web application! If another program is already using port 5000, you'll see ``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the server tries to start. See :ref:`address-already-in-use` for how to handle that. Continue to :doc:`database`. flask-3.1.1/docs/tutorial/flaskr_edit.png000066400000000000000000000317131501065614000204070ustar00rootroot00000000000000‰PNG  IHDR ¯)Ì IDATxœíÝ]LTwâÿq®êUÓ‹½0Ù{ç]½é†dIiùB; ?HÜÒ%%%1¸ùûÐÖÖj‰Êâ5Jë.ºûsÛZÉòP)±âCeA‘ʈÅ-}u;.-ÈîXÔðù_ÌóüðåÜ÷+ù$-œ™9çëÌùpÎ÷H¹uë–!„x“²Ð+@!dq†!„b „BˆQ(B!F¡@!„…!„b „BˆQ(B!F¡@!„…!„b „BˆQ(B!F¡@!„…!„b „BˆQæ\ ýýý„Ba(B!F¡@!„…!„b „BˆQ(B!F¡@!„…!„b”$(•–×)µÔ0ïwÈÙuZŽß;« ý”ÈëF±u’ @fóÉŸv²e'Ôvyoñ¼ß!§÷kšZv'ÈëF±g( „BŒbã N"ï¤yÝ ÌÅò4¥¤dèýX—°i=¦ÔÒF•u%Àºxs¡«SejQnyƒÒJë”VÖ¨‚êÚ×î\ðu  Ä÷/wÒ‰¼n„†‰?í­Çä(­×ožÕ‘®>9û/ëB÷ýåP‹ž+mÐ+GzfΖěÎ#Ÿ+uK›ŽX¼ÎˆOy'ÈëFH`(8Óõ7å–Ö)÷Ð…%q²þs¥–ÑÆöËFÏOÄX ä“ê:¥nùBµ!ËNúB×9¨iSñžfån?¢´-uJ-­×seÊÝÓª×k:ÔÜ~Ýç´ïÐ1½ò^£²Êê•ZZ§´²#rìü\¯TŸPYS—š{‚ß$‘×í²¼ê*3ïvÿ"ÌhgÕÙ}N»Î2§–Ö)µ<¾â%öNû‡¯é…å¿Ðc)éñ_>¥Üòu…+¶ý*~á)ýòñÇ”’ò˜òi½Tþ©.N®Où¥Š[‚_§«2M))OiK»Eëc\è8«×ßÿLY[ꔺ¥AŽ=Ç´ëdoÐr_þM%ïQZi½žÛþ¹^o¹¨ -­J-=¦O¢¬Kퟔºå˜>é ·ÌmôÎÙNî|Ø ÔS{àú¶´ø½fó¡#!>ÿ-:öµbâ;ðÑ ¤¯C¯”Ö)uK³6¶^P{ïeõ÷õ©½½Sß«Ÿ}Ü–f•~ÙôüÎöÓÊÝ2ýü *®?§“=—åìëSgÇ9íÚ×v}Ýz´çý¥–·j_WèŸR‚žc{³~SÞ¬Òãu¡ï².tœUqBbÏÅO‹õdÊcZþR¥>m¿¨‹íŸªò¥§õBÚSÁÒV®§OÑ/36êÖv]¼Ø¥–÷‹õÔãé©-ÞåjôÒ/Rô‹—j^«][žJQJZ¹º¬Zÿ ÄÙqZ¹¥õÊ=Ø¡cÝ}rö^TmÍçz®´A%Çgç%œí'=§Ÿuy>Ó==úˇ-Z³ïó ä‚6–Õ)u_GÄSTµëýž+Öéïç$¦H¸Ì±@~SüÓG÷ýÆ÷±A;a§öìñÝyŸÖÉ uèQévƒé½ ²=õJ{ï„j#ý=Gi šü'çŽj¤@HŒéRùÓ)JY¾Qm~_oÓkËS ä¢*3SÊ“Åú4àyÚ·<­”Ç2ôþEÏÿZüK¥<ž«}—k{MO¦<¦Œ÷/Z·þQ ¤OûÞ¯SêÎÀÏjŸþ²¯^©å'uÌw¹=gÔé·\¯v½WCtª¸´NÏ}Øq}OÖ|¦ÔÒfíñ~Æ)Åv1Ýz}‹ïë¦]]¾_vLŸô?Oó¡#ñHÏ9mÜY¯´=ÓÉ(‡¤ÁÒª¿XpKìš÷•‘’¢å¯µ}¯åµ' d¿^x,E¿,þ4øyÚ6jyÊãz©fúÿƒËÂS*/h¿•ëµ@ºTRZ§ÜšàÒr¶¶z>ãݳ˅úÁòdýgâ“Þ‹ú¤þ„Š÷|¦¬²úÙyƒ Qi‡ïc½G~G)ŸkÍ¡3:pü‚N†˜÷ˆºnÝ]Z3ýœå_¨6D!E|ŽGA„Ę®r=’¢´òàl×–§ý ä¢gÙ”yaÿôãÛµñ©¥<=}º*Üi­9&ZôzÎ8¼Ò|:ºÿË6¥MƽË·„›»äVÒH¤D-îûþÚ”²íù²×»L·^/ (¿É¾Ëž‰²§ÖžÛÙªGΩ=ÄQAðÎÿs½ð+\žÛ×på9öp%™Kâ<IIÑòÁˆJWùÓJIY®íýº¸ÿ=–òKjñú'ÌH¿>ùS}Ü“èÇj•º}ú4šÏkÖ¿&CmxÏ{ú|ÿ•ßùƒhÒ¯þî³*ؾ@¦“öÞ߂ޡæ/ru¨Ìwò¾´^¯4…˜Ÿ‰iû‰7ñÌt©2í1¥,-`Ù0¹X©´ÇRôäk5žÇ=ããâIÂÌôG¿Œ÷ÈçJ+=¢_Îåx®ðjÕ_z£¿æ…––°g^æ Äw#î`ÏißÎ?ðCô÷ËÙÕ©ï7F8õåIIëåÈë6}èTJÍs/±n!ñçbM¡÷*¬÷ÕÇUXû[ÚÕuñ¢ºÚZTSùš^xºÐÒ¼ÿ¢Þá1¥üâúEJŠžÚØnýúÇrV»ÏUX=ÓWaynì+i ¾ «Àï*¬Ö¯Âò¤½õ˜²¦o$ì¿_º{ÂßHØ×¥’-urü©S'{/ëBOþr°U¯W‡xÍö“Ê*mÔÆ/Æ7#† â“È;XÏaªß$y·ïã &°@<¿{kz¢ÌÙÓ£#Ç;´ëP›JöJnýŘ׭³¥Åÿñ;Oª9–Ó`± í¾¦ ¿û@> Hû‡Ú˜û´žüÅcÞåŸÔS/«òÓ®àçþ0W§¤(%åi•?Š›ýZEø!nöó}¡=à>÷Ž©,Æû@:›Z4s?V ¹ÐÑ©²?}®Üòú™_eò›}á•É¿×lÑÆã½!ç@úûª=Ԣܲé3Ü”G[ ½ÚµÓ÷ùëõúIŸÇýÚõÐn²ûȇ ~Ï]r<ʈߺ9õ—}þó+YÏý¤A2ŸñÎ{†˜èN–P >‰¶ƒílmÕs¾s{NëHÏe]è>§²=þó#a ¤´NϽצ_^Tg_¿œ}N;~Z>§¿ž{ÿlð•Ñvþ½]*ñ;…æ£SLÏA1L·Êªÿ¦OÚ{=7åö\Tí‘Ve•6¨¸51¢I‚‰ñJU‡8DžN¸?Ú䣦ǻìeµŸü›Jö|¦¬2Ï‘Âs埩 ú„öœìК²PŸ=”töô¨¶åŒJÿÔ¢‚³¿Ê$uKƒ²¶{žgWkÿN=ʺ­9Þ¯þž€›ýÒ¢‘Ÿ#Ös´„$K¢î/l’¹Žc!„…B!Ä(!„£P „BŒBB1 B!Ä(!„£P „BŒBB1Ê‚`q¢@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„I‚¹¥=;ê”Zj˜÷œú:†W¹zöXÈÇvýlÁ6ÌÅCµœ^Ÿ/Õz×óÕþæfïךµçºïò±ŒW›j½ÿ4î _FÃçŒÌ}3~ìS^¨ç?øÜñ,³®;õbı:¥Æ ýY‡öú,³oP®ÀEBnw£v^\ð²`ìãû ÝÒž÷ü—û]ïdˆåâ}ÿÃJIP ³ÎÔ¼9wD*‡»ªÝ{xðW¼Ü ¼ä2É $΄,»7ôV¹Ïr~çó~ÒÞÑ­Þ€+Š,)».•úþôøïxÊhG—Î<ŠK­¿îšý)¾´N~÷n¸¿Q±ï)© £WŸËyo *¿´Ùÿ¾ ¶1Ô=>§Ù¾nm‰¯@,ú yüWØ;ý^ s4TrHœG çbܸóš}C'eXpÒßÚqÛ¿nóÿ~áYÿñ³¢@_#uÿ?Nãx/áöYæÅÖà+åæìþ5­³é£wï¥îíÓÕ™…ƒOyMŒ^84±nÅ6ÈŒr‰p´±·ê3ä1=_Âi¨DEÌEЯ©A—SNªñ`ô7¿} ä¿úCàÖùaÐPÀyý¹Hð*øßQ:p©u|÷Å*xn#uŸgG¦¦!h‡øžðÌ<ÔÉëôŒß8Z³Á§°| -´ÈcoÝghZos£BßÁD`» Ï¥Ò€dðÄ]lWØ·@¢áDÐsþ?õν@bùw”ÎiXx.\ˆ]ðE_ªõîˆÞ-¾£<èöònõÞ÷ÌøŸÂ±fƒÆ1èh!ØÜÇ>¾+=“áîÎÇ‚¢@fP ¢@¢Çÿãy+ºù)Ýú§ÿœ@iÞjîÖ‹¥!î(šhÖγ_éù #fk¶Ñä’\ ¾(Ù¡àÖœ $x Ÿ© wùehÉu +Ìú„ÝYübÅÒ:Ï]êAóèÖœ ÄúSXHlˆ Ë.cøàÈ‘d,©·9à2̓߇þÕ.ïMbï76 5‰4þ~÷JÌíè$è‚;ͯC-{6xY+¶Ñú±î3$I×ÿ©õ{õ̶f¹â,4[HЄúÿœ·Ëx=¯=»ÌìïËòNDÇ0¡9Wä>÷÷úïå£ÛÎèdÐ采:w¤Y©¥Íz÷{ÿË3­ºŒ÷Ý8/q=°Ž~ó¡~µH<¾ÿÊÿW•”¶èáî=ù¾;¶e-ØÆGQ V}†BM¸_2‰…Cœ_{Ô]ko$œÜQìÔÕû5úõWÊ+­SÞ£¸¬4À£º‘Ðý}ŸŠ}îy~ŸS']?{nN»uK'[OéùÒFŸý)èèÄÊ Kƒn²“â&»“!vÐ~¿Ë*†ÉåÈfoˆóR¨_¬8-pÇaÙ9nã#)Yó¢@‡$(9þA©—ô†þ5 _êÝ®à_Ãà‰÷:õp¿Êdúîw'ñC}ÝëÔúý-z±|z'РçßkÓ[­òp}žþ ÔÝ[:y¶[ë÷›Ý¾m zñ½6­¯ëSëõ€Aeìâ_)è×|LqÔ_eâÿÛtCBŠç·îÎìÐ#Î ùO2Gÿc]ÛeCž²‹sì?C>üOa]ãVJ‚¬åþÇßgCž‚ Q €÷õ>ΜBjÐï.p…ø½|Æ{ôÑ ÂÖ‘Äû5ñ@¡@?wÕZsJ¥½·ô/ÿ‹0BŒP #À0BŒ,úéïï'„bÛ`aP #À0BŒP #À0BŒP #À0BŒP #À0BŒP #ÀÈ"/v=‘¢””²üm]‘Ô”;ýµåÚq5¶W1y $; DÒøGÏÎ,÷ìþñ W¡@ Ø"/÷>Ò³3¥ñ²ŽÇø0 âg»ñ/ƒ›Ú»"ÜQËõ˜Ùç¿T«õ¹©Z¶t‰RRR´ä‰eJÍ}[MWîY»]€(Ã?^¢eáN—-Y¡·/Q"’›Í ÄóµøOaWÉRï×–©öʸîéžn^Ú«g—x¿žºW7­Ý:H(ˆ äÒz-:qŸ­Zk· "ƒ9^¤%Q äY}üT4(I÷>Ê6?É­³ìˆ‘¤¦—g dYI{P!DœIY¦ÜýçuóÞ=Ý¿©+—Žkÿú\­(i²vÛ ÁP ’tu¯V„:u/ücÆ…¿ Ë{dÉŒñU8Öª©£Z9Åš~ù¡…J[}TwS‡ ³´æÀ U¬.VNf–6XßÑ>U¿Y¬œL‡r 7¨ª¹ZE`H̃kÜÖ©wò”óf£œ®IMLÜÔùJ”Q\«aŸåƒ dLM«Êy³Q£“šÔáuyÊxóÄÌÎ?š³ee”uzþg¨Z9…5¬mÍÜ cnŸu­¹Ãëy $-o³šÆ4bÝ›V;”óÎ ¹'åvõ¨ªØ¡4¿9o ĸ $æAðHZzèÌÈh£ç§p¿½~¶fªzX [ ®F¥—¨ÞwU†ª è¹Â›øb³Òòª4 ÉUS¢ìÝ}žõ~Ó¡­’&NhMz¾ªÃëy ¤¨æ¦Ïë0aî¬R¶ß×:µ)=_U¾3’ó Äx2P¥ì0%³µ{vù ¨Rvú6õ}ê‰v½Ïx´QEéÅ:<|[ÇÖyKC’«Á[&ÝÊȬÐù™õŒôzžyãôdжάïôsM *•>Udë0·ÙI‰‰yb,ÑF¥OŸ2 %ʈïÑFœG Ò5UçgiUÃQmÊôY‡¡jåäTÓî|¥½yÂs:*êëE)˜Ž@$3 $æA0™¹­‰‰Ûr uêð;ÕÞŸö£Í ;âª) ºT7swžÒòò•]\«Ù­êS…#_9yYZÕ06³Þ‘_/JhLõ«³fæ@&\}Ás L¢I‰yâ¼ ëƒÍ*ÊÏSFfžVoÓáî›!'¢ý¯jÔᲟ«¢5àóÓ¼swžr¶÷(â¸ÊHÏRξë9©Sof)-}zF1¼^´‘4Ú£ê7‹•ÍUX€-Q  £÷­ª ã9bÑ €ÝP  Ø bÑ €ÝP  Ø bÑ €ÝP ’†FtÿþÃ9?ØÅýû5042ççYôâqë»ëc”Äàþý‡úîú˜\#sÿµ‹¾@¦¦¦äqk`hD—]„B"d`hD®·¦¦¦(À @F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `$) äÒúeJII È=±t¹Rs׫öÒ¸%¯3ëž>Êž~Ú{Óâ§€E ‰ Ä'KRµÿª%/åE@ÒÈòW<_¿¤½Ï. þº%(HÞ‘tsoêÌ×—–œ÷{Ìø¥´>7UËžð”Ì’'–)5w½> :ÝuOW>*ѳ˞В%OhéòTå¾]«·Ÿõ/ö’¥Þÿ_¦·/ù>þªv,÷.ûD‘Ú-ÙbXxÉ[ ãW|Ž@–(»v¶ÆiYØS^ËTt|vÙ«=«%‘NM\Ù¡å¡Êêêì×—­÷kXÔ’®@BM¦//jÒìY¦ó*Y:[/×^Ñø½q]©}y¶T––ÈSí*z§Xš<Ë^ÚŸ­¥¢q}4}TòÄË:î}µ«;Vx—[®W-Ù\H6(-Yš­ý—îM/<»óOÝ/ŸãíO~ÌR­¿$éÒÛ³¥â·ì½½<°@¤{Ç_ÖÞÒÊmº'éªv¬ð.÷ì~1U ™$]̜º7®+åΖÅòº*IÇ‹fNI-yù¸ßóyö”WÑqIí³Ë¦¼Üä³ä½Ù£ ¿IôKZ¿lº0>ÒøÕ½Z1](µ÷,ÙVHÉ[ ’¤ãzyæH$[µ÷ôH@$éêÌÄý •”¬:¥É"y $ðdÉôNÜŠ9Üs ^ãµÊ^â méúó€d“t.ËÖŸ×ôI¤Gr–ö¢¥>ß_.KoA€‘Ô²dÉR-KÍVÉþó ¼»#Ô} +ÂÜré£"¥.{BK–,ÑÒe¡ïñãsIoJ*“ç’SRH¢?ÿöìäy“Õ¿‡ b¡ñžõ? Zþ¶¸u@²¢@,4S KžÐògKÔtu¡× `„¡@F(€ `„¡@F(€ `„YôòíÕ„B BX0`7ˆEƒvCX4þz´5½Dõ£?í¢`Á¶»OhMz–ÒÒ³”¶ú¨Ü­Ùùí¸fѳY)Þ1»©Ã…U8å:%ªDÚöDZ—…AÌ Â¤Ž­óî´“Y¡øþ"ù£.kª. \ÏÍ:5ñ¨^/±oûÙ²à±^yàÛÙ:¶Q q®)Vöö¾¹?Ñ"dçm_(HÈAèÑÖô|U ˜>ã|tjSz¾ª†õëÄ#¾ÉÙ=~ $nvÞ‰ÚyÛ rˆ«S•ë •éPNáf¸=󭉡£ÚT˜¯ŒÌ|­:P­7|v£Z™™¥´t‡²}7P­G•|_f`w~䪟x ä[UåûnWŸ*Å:<ì³}«½ÛW\¡¦¡ÉÙ‡ºuøå8ÊÎß ªŽÙ¿ÒiÛ#1-°ãeü dø¨V9 UåœÔ\„_ÏXWÔÔš|‡2…ÚÔ<[^¦c6| Ø{´pêd JÙAGÏt,âQé *ó U=øõmÍ\«¦Qó±ŽÈÕ®Šâ|edæ©hwµ69 U=$E{o{„÷nx‘ÿý®‹$iRC_TjU~¾2ùZµ½]®éo­Kâ¢@BB¨Sýj‡ v÷htⶆOW(DZMg'$ϛ͡5 ßÊ=1¦ó»‹•6³C˜Ôù/ÚåÙ7LÊÕQ©•ŽJyÞsߪ*?Ïç 8¨Ê¼xŽ|¬-óe­ÜޣщIµ«ò@ç̶7­s¨`{§\“r5j•󃉼푙H¤ñŒ´ >âîÑÖü<­ižëÑH´Û,e¿Ù¨!÷¤F»+•3s*Ô|Ì<Ÿ{÷§ðSïdiUó˜ÿ‡ª•“W­¡9Œu¤u¯/v¨èƒ>NÜÖÀµÊH­@̶=Ò{7’Hÿ~‘×ÅݱMÙy›Õ40¦‰‰1 >¨cCsY—ÄE„„â>ªUétlfovMÕùmí–4\«ß7—û¨V…Ý! ªÒçC1t XÙÛ{<ÿ3P¥œüjÅ~FÊâÙîPÎ;:?tÓ§íjTQútYz—-shÍ·ãÜvÁs Å:ìòY ¦SXþãv¼ß+ø ]ÕÅžŸU§ÆB¯‹§$¶vO¯O™Þq™Ã˜yXW ®†¥½Ó.¹Ûµ5¯XUÓ_ U±uXŸ£‰v½ñ( $Ò{7¢ÿ~×eRÇØ¼A~†IDATÞÌÒª†€RžÓº$. $ä „(áƒZ™îûÈm5­Ëòüã;«”ç»ãïÔ&ŸÂðé*­ÉÏSÆÌŽrú#ÏÎÄáyÞÝùþ“ÈQY| Ë=¨úÝ›µ*?Oi™>‡ì!Ody~š²í‘˜žÂŠ8žá¶Až^Zf¾ròòµ©Ãšmøu‰0Ös3ë DUÊvTé웵éÀA­ÙÞé·4ë°‚>G}ªˆñ–ѶGzïFdº.7u¸0K[;B<¥ñº$. $ä ÄxR˜ýdô¨Ve®UõÀ˜ç´]*ÊÜ ¦áIMLŒi fƒ²vú®†eä*»°VñíÓâ-IzÇ¡‚Ý}rOÜÖPÍeÌ|@oëÔîJ59oÊ=1)×é Ÿ þ15­ÎÓªzärOjbô[:°M‡¢l{Q d¸VŽÍ:åû\Ç3Ò6øO¢žÞ¬œÂj øN2{ï? š'âºDÚ™™GøèÄ›•V|PÃ1_Ò}S‡‹³”–W¥MêÔ›YJ›>Í2‡±Žüz”;h$ÒûÓtÛ#½w#1? ž©‰¶.®Z¤giÓéhë—(ƒá*¬Õ³WaU;gw4¾WÕ¬©9èsUÍm9k6h¥Ã¡4G¡Þ¨iTE^ÀN´QEéY*ª‰õŠ óû@&†õF~ž22 õFM­¶ú|(&†OÌ\e–°}íÓáwJ´ÒáPF^‰6ÕôÍ„ßöÈ¢ˆnËùÁZådz¯ªq{¿a<#mƒÿe¼·u¶,_9e³§È†je˜#²°ëv]"ï€ÌƬG[3ƒO¬iö971¨êu…Êž³ÞÎÝùžyyç?ЧúÌÇ:’‰áÚê½ kÕZmuÌž ÿþœÃ¶Gxï†éß/ÚºÜÖPs¥Våçy®²ÛÝ9ûzÑÖ…I†™gÚ”Y¢ú¹žRÁœ ×kå±^B k ªÒ§@°¸P  ‚¹I ׬UFà¹~À(ÅŒ±hÌô©Â‘¥ìüͪõ&, ©P ‹bÑ €ÝP  Ø bÑ €ÝP  Ø bÑ €ÝP  Ø bÑ €ÝP  Ø bÑ €ÝP  Ø bÑ €ÝP  Ø bÑ €ÝP  Ø bÑ €ÝP  Ø bÑ €ÝP V ‚û„Ö¤g)-=Ki«Êr¡›:\èP…Óÿ«ç·;TpàÚÜ^_=Úš^¢ú˜ÿ&nèu™W1€DFø Â5Uzwj3Ù¬S1>QǶ¸w† S á ×+{{ßÜŸ(Vc&-ÀzB„„NmJÏWU¼§™‰,ZHÈA°¶@†{f¢œÂ>ªUŽBU9'=ÿïÔáwJ”ãp(;ƒª:nÎ%Á°ð(ƒ0ÏòA»ª‹Z¹½gö±®F¥ûï4Ï—9´æ‹ÛÒp­ 2+t~úî£Zµ`hP•ŽbŽqñPcæ>ªUétlæ‹×TïÐÖn+×À\Q !a~ $-3_9yùÚÔásê'䩚,ÏÑŠ³JÙyÕš]½NmZÀ>]¥5ùyʘYÏBUÇ:v¡Ælø V¦û¤n«i]–§<ç°ž¬E„Ãé®PÚêF…Þ‡/•»5á:ªUyÔäò~ÃÕ¨¢p§‚ራ¦$¾óèQ­Ê\«ê1y–úTáˆcìBY¨#Â,¿#¸×€å(ƒ`X õ*plÖ©;òè÷ŒžÞ¬œÂj LHÒ˜šVçiÕ=r¹'51ú­NئÃRàˆóëæ@&¾Ø¬´âƒŽuRÚÕ¨¢Ì jžÔÄĘj6(;ž± 9f7U_0’é:/âzºjUž¥M§c\F(YxˆnËùÁZådz¯ rKR¶fŸŠZÓì9ãïm-ËWNY§ç”ÎhŸ¿S¢•‡2òJ´©¦oæ'uß«°ÖÔŒñ*¬ÈëâyâAU¯+Tvf¬W7Ý–³fƒV:JsêšFUäÅS¾¡ÆLž«°VÏ^…UíóX¤õ¤@€yAX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvCX4`7ˆEƒvrçÎ D¢@ £££jkk£@$ b566¦¶¶6?~œ‘(ˆÅØØ˜Nœ8¡¶¶6ýôÓOˆD@4å!1‰nÙ @² UL¢{Q Z¨ò`ÝÁB•“è(ð®<˜D@À¬XÊCbݲA€dky0‰îE@ìåÁ$º €ÝÅZL¢ @ØY<åÁ$z €]ݹsgæˆbtt4®¯Q ÞA „l_ €…AŒP #À0BŒP #À0BŒP #ÀÈ¢/©©)ý0âÖÀпuiÐElš¡ý0âÖÔÔ” ±XôòÈ[ß]ÓÝ»“zðà!±iîÞÔw×Çäq[ð±‹E_ C#”уž±àc ‹¾@. º|ÇE'—]|,Ä‚!I ˜?IªP Àü¡@HR…æ’h¹Ñ¡½›K´òE‡2^ÌWц½j¼üã¯×" Ì $¡òµö¾”¥•[ëÊÈ]ݹó£¾ûê¸vmø³¾Zðu[¡@€ùC$RÆ?תôßêWÂ/Ó¾5KiéždäþV«*>ו;žï|¶Vi¯þU7|sãS¥—¨ö†÷ÿï\Õ‰Ê7T›§ŒçóU°áÏêI€m§@€E‡I¨xŽ@r6üUí—ÐøÈËßéWã†|åT8uçÁC=ÿR¯ÐwÕ¯*míçðPüG'6ç)gçúêÆ]ݹ󃺪J”ñê_õÝ‚o;,6H¢eÄ©ÆÊ­ZõRž2ÒZYR®?výà)ˆP¹ñW¼¸W—<ÔƒwÕ¾Ù¡œÊþÙBÊuèí“ÿñ>÷§*J_«F¿#Žnmyþ·úãw °í°¨P ‰œ;?èòg[•“ž¯]—=_éú³Þ~õ·Ê~Þ1s*+-}“NL­tíRö‹»ùaæUyûUŒßõ|ÿ»¿ª ý 9ÝÕ¯]¹miÿº¶æ)§Â9û\#Ÿª(ý _èm¢@€d@$RFŽkˆ?êèWž‚¸sçG]n-WNzžv}õPF>תôWµ«ëÝypWã7œ:´6Oi~òPWª«Œ’MúÏçk¯ß„¼ïÈtçÎtãJ‡mþ£ÚzÛ)`Ñ¡@*wu£ëS•¯}U9/:”ñ|žV¾ºI{O^õÎÜÕ•ÏÊU”ëPZºCÙ/½¡]u{=G¾î7þª‚ô,¥ýöÏÁ“ãw®êDÕ&½”ç}þ­:iŽe‘…æB’*0(’T¡@€ùC¤ Ì „$U(`þ,úá/’éð ùµè ÄÅßD'ø›èÀBXô255%׈[C#º4è"6ÍÀЈ\#nMMMYð±‹E_ €…AŒP #À0BŒP #À0BŒP #À0BŒP #À0BŒP #À0BŒP #À0BŒP #À0BŒ,úéïï'„bÛ`aP #À0BŒP #À0BŒP #À0BŒP #À0²è djjJ?Œ¸50ôo]t›f`hD?Œ¸555eÁÇ@,}ü0âÖw×Çt÷î¤O€uYøP Àü¡@}( XÈ‚åŽFŠŸÒÅÞüêi9_ø®¼µG?ÄSñÈuý륧ôõgw`û)`±£@¸@ú~ïœùÿ»×œúqç«r®Èз§þMP @B£@¦@f¿>Vš¡‹ÿó{ýgúk?£ÿªúÿç×êýU†úWïÑØÍ0aÙÑu>G<+žÒÅ—énÔç_\¡@€ùC$\<Ôƒîß˹âõ¯oêÁƒqý¸î×ê[}Hã×îhòçëÛùõ¾ôºT Ñ– uí1‹+0(D,kÿ§þv>Ôƒ›‡4°â·ºéwDСo•­ëßHÔeCHÔÇ,®P Àü¡@±@¦@®=Ôçïå\pêÉ›oÏHÔeCHÔÇ,®P Àü¡@®@æ@nÒÀŠW5r+ÜóD_6ø$ÒcW(`þP S w4ò*,ß9ŠqMþ<®;§ô¯u;5X Q—½£W?¥Ë;šœYhY\¡@€ùC,pÌž2zZÎþ7ô} ?£w–hà…_«÷W¿Öå—Öé_g¯{K ÄUXa—}¨IçÿéÊÿþZ½WaExÌb Ì „$U(`þP $©Bó‡!I ˜?IªP ÀüYôÂ_$$Óá/ókш‹¿‰Nð7Ñ…°è djjJ®·†FtiÐElš¡¹FÜššš²àc ‹¾@ ƒ¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `dÁ „Bˆ=CB1 B!Ä(!„£P „BŒBB1 B!Ä(!„£P „BŒBB1 B!Ä(!„£P „BŒBB1 B!Ä(!„£üjPMø6UTŸIEND®B`‚flask-3.1.1/docs/tutorial/flaskr_index.png000066400000000000000000000266331501065614000205760ustar00rootroot00000000000000‰PNG  IHDR+ð²Õz IDATxœíML×úÆg¯*/º°Ô ݱ+›J^D‘[EÍÝ8‹{‹*EEbƒ*] *‘ÉBqc)Nƒ'r§))¡¡²ÃG Ä„\j'á#ðU”ˆ+¬”¿àæÓ‚F˜HÏaæËöø`˜ç'=R‚gæ¼sì9Ïœ÷œ3#½zõ EQ•«¤÷EQµ3E¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„D¡(Š¢„´a™šš¢(Š¢v h EQ”h EQ”h EQ”h EQ”h EQ”h EQ”h EQ” À@Fàp`uêìmŒ üÿÐýì†ß÷—´c£(jW« d]¿þ¤jd7Йvû¤ñœ½±ä߆;‚Û¶‘ÞαQµ;E¡PE iˆVÛ¹‘ÞαQ”Z÷]{!IŸãì6ˆ%­ººau´Ã9° bIjx Ο‚°»Z°×À^g;¾>wÞðØ{MO4ù—·éíE©EÉ]á®nüÃqÿl¸…¶ ŒMMbøî0.6ñ™£ß´¬eKrUÛï°Öõ -Ï1Ó@dÚÎôvŽ¢Ô¢ä¨?`w`oÖ5‰Þ+¿ÃêhÑð¤Ðñi dÄ@~=€µî:ü:ûi¤‡q¾¹§;a?ц½uXWð™³öÓ]ø®ù6:GÒÇ8vwÞ¦n|Sߎ/œW`u°×Ù†œüßœ»gÇ:G´?’̱MâüY½YfÉó ]O3íúï⇆õ.³Õ€Õ•›ñR»[á_ªñeñ‡Ø#íÁ}»+ˆtÒãCÅ—Ÿà£ö@’öàƒ?Å¿\­¸ŸúÜgÇÒG¨jË8µ’ô êÂyŠÝ  ß¾…ïÎ^ÅuXëZðÓÝø¡wT³]ߨ¬oÃ^Ç|vâw|¼á`¬Žnüš%ÿO-°Öuã׉tÛ ãHrÌ6uÝ·ýÒëÉ?VÇ *ÊìljÓ¹þƒ8Ÿ¶,ã¢È+>›LÜÆ7ެu8Ò5Œðè$¦&&÷ãHý•õýê:áè›Ð,üØëRÇoAÅ•AôŽLblbý·ñƒ·=mž=¶œ>Û«« Þý»Í1Nt⟮N8B÷1<1‰áÛ·Pá¤PÆu¿µK{Pü¯Sh ßÇýp+NýëS|¹÷­ô¸ðé>úü~ †qÿþ‚g+ðÉ{ðÉ‘`r»füëC þ«YUVuŸHöº0¯ø ÈØíÿÀî¸{ÃmtßÀØè}ø›ÇgŽT†ÖÇ%ÆÂ½‰ôSÓ@âšÁÅ_‚ø·÷w2Œ#άÞÛSTþ†+Šc5©)ö@ Ic é´AùçíÝÇÔÝþ)ßWÓáôiyãýôjbã„€ŒÃyú öÖ߀?CïGs G_w(纛Úi ”A Àõ©©øzïAu±¤2û8õùHW Uuœpݧö|޳÷ÿo­øÒvü"ß®§K{ðùÙûù‹?«LÀ{6ëIõµ:‹Þ+°ºzÑ-ßîtýŠíFñC}À€ô£ÂÀg¿ÜÍooóUX8¼Æi ;­’Qwñ]¼ü«øa ÃçÎnü:ª=NgS[n22ˆ#'¯`ïé?Л¥Kª5.\ÌC7–Ú­:‹Ï% ÅÕ=šÏ‚Õ« ć/÷Hø¨¢U{œž#(–>À¿šSÿךEÂT¾„/Ÿñg5T:°7kMk¬«+qß]ßNïÆ²÷ÊU D¦ÑûøõÊ Tœ¾Š/œWÖÇ 4jƒã¶|ßdïBÑKùÿnŠà|h½:ãYc»;€§Žéº¿Ž!e<†n/ˆ¢ jÀ…O% {]Úv îS¥ÜOl+eЗ¾ÔþaùD‚ôi*]•.­µAe3ÑDÆá›m:zª¯{S×xr»Š`º±K¦° Æ@2)«Ü½ ùcSœAœîMnsß9U¢ì›L ”eH­}v² GÚÖéhÿßñê.ŸyûU]è,Ç8Í™\ÔF”cD’P|D»­ž\ŸB’Šq$<…û¾/±Gú­yŽÛô@¦ðëOWrDïnn‡õD*&+³Y[& Ä€6×@’yOÙçßåãÙ d Swoáëºô’ÒÞú?4? ½ñ {Óm8åƒ÷Ž+ø¦Cg|ÆÐùQT®Êe d§öîT\­Ú6îŸÂÞ=>®nNì÷±ÁýrѶ™Ê>·íwìu´áHßz/'1ë G³—9 ¦Í¼lD4y%gl`ñoEã¯þá0©)Œ ôãÈÙö ©¯„*»&3Ç–êºjL©S5öbôü(*wÝo>”œ…uÁfaù‚a Ü¿ž šOUãËO)ͧîãì—{ }ø!>”$|r$œÿøÌ Ëfa¤fa%öUviga}­˜…ÕepVBá®n|‘ZHxwcSS¾;’~!áÄ*ëøÇOýèÄðÈ.6tá»s:e†{ñ…£GúÆ„#ê‰"Sæ6ÑMU ’ß•ï¯6µ$ž½•(A[è6~hêAåi­¡Ø¯Ü7[0¨Üÿd/:¤Áh Tþ¥Ÿ+Ö´¦_þGìŸâã÷$·ÿŸ|YS­ÚcÿbÇ’Iú®ÍXì×Õá&nýú«ÖÔwÃipHGkë± høv?œ?ý»ëÊÚ£LþéMÿ(“aE™A êŽLMÁ߄ݙÊXpˆF›k £øá¤üøWð]¯lÍc×õ $Ý`wÛ/-ŠcW†²ô@±á¢W9¾òEàæNƒBQ[©ä¸§Î@w¡ˆ"S¶¶¿« ŸÉÇ*Nÿm#“¾;çiåøHZqðY}Î÷ÝGÿÄÆ&ÆÐú¾–¥¿>;{K;³"[ã?:€JE M¹ÐÉÐ1(ŠÔ]8Ïý_ã‰E¹#÷áoëÂŽTtmÏ!æC` _(uN§‹œRº—6%õæ‘ä¶“÷þÊÓWñ…3ÑSøÌu_Ÿ»Ó½·ño§Þþë]ɱ‘øƒ8~ âë“ë2±Öµà‹‰ãüÐ5¢lÔ³ÄöïЦFT‹ âüíÌÇ0𣥍BQÖöb—h£õXBQE½Ñ@(Š¢(!Ñ@(Š¢(!Ñ@(Š¢(!Ñ@(Š¢(!Ñ@(Š¢(!Ñ@(Š¢(!Ñ@(Š¢(!Ñ@(Š¢(!½w!„²3¡B‚B!D!„!h „B„ B‚B!D!„!h „B„ B‚B!D!„!h „B„ B‚B!D!„!h „B„ B‚B!D!„!h „B„(y…Óî¬#jÁ~w7*.ÝEÓä+,å¡tBÙ­€¬ ¨ Ã=†Ç°úÑÙG8\¯ü|¿o£Ëy Bv »Ã@R<{ˆ¯T½’} 3x‘×(rg0Тˆé`S{„íÏî2¼Cï%uZ«§çóFÎÐ@!;‘]f Àã® fläÛÑ7y#Wh „“w×ñ±zuÒã½—°z½÷¬G¾ Ð@!ù"e Ž´=ÅŠêcÈŽ6wèÒ¤°‚øQ‘ÂZÆÔäC¿t_¹Û±ïXÖcë³·~}žöË_y5ö®;øÖÄWÂö¹:qÐsß6ßÃÃs˜Zz·¶ý‹á›fŽÝAdeB¶É;°:ºQu©VGšž)?¦ìdÑDŸ]ÿ2_=ÁqÏzoà`ÛD—ßaey½mÝëûxÐ¥úa¬ü5¯Ž¥ŽÛŽª[óˆ.½ÃÊ꼘ŸGÓ¥dÏÇ}Sª¸ö@6R!d ˜¼«£þù'¨:À¾KOËÒÈ«yü¸‰ƒ®–ÄrON޾ZïÁ¼zˆ¯8ý—lŸg8èÀêSo0ЫofK–)ìI?w*•§\ž‡C¾žäØDVe_Ãácòc ²–ã\†ß'û¬~QmUãtýF dceB¶€”¼¢·z`utâäìzF@×@^Íâ[W›aôÙ2VVß ú`®T ,¦6Âiw_õ,®íöbà&¬ÇZ°Ïч®µ¶è9NºøêÖ"¶‚Â6>îQøÔw ªÜÀ®çÉÏžâ¸Ü\\wdæ²ÎTWç dceB¶™`õ)Ž»°z"•%×È;ô6·Àê™ÐŒ×¾îSÜȶµÈö}ƒ®†tNห‡$MêÙª{*›HaˆÎ º>‹øÑ£Ú·yN{üfÕ6õ©ã'ïüeß„£ëº1Cbg"¹²ô £fÑÔu‡}ÚÆ^=CBK;²2![€®x6±¾„ Ã,¬ÁgËXZ}‡¥W‹˜š|GÃFåÛ>H^ëÊõk£‰c»¶v $Åò|bÆDj_Óx×zŠÉGÊ+þ¶Îh§|ìB6c"ÉTW§"î©Ù]‰;‹Ä”Ý•AÙÒÞa°­Sß@`é)üm}øÊÝ‚}Žì¯ïFE`½óª§d¬>A•žQ$e_@›9ÙLh ŠÚx‡f!!…„½Šnâú;Iö{ï¢kv/V•Õe<~0 —l »aFÓU]™½‹ýò¸½]}‡ïá #€ƒ]Ï7\!„ä›0ƒ/”º4kpf‚êQ&É;úl2YYz…ÁÑG8èC…gý1#Öcí8Pߊæ{^bõGÇPå“íçhÁþúîš[3ƒ•A!ù¥ „BÞ?Yob߃6!„!h „B„ B‚B!D!„!h „B„ B‚B!D!„!v¼LMMQEQÚõB!äý@!„" „Bˆ4B!BÐ@!„A!„" „Bˆ4B!BÐ@!„A!„" „Bˆ4B!BÐ@!„A!„" „Bˆ4B!BÐ@!„A!„" „Bˆ4B!BÐ@!„±ó dyß:°¦QÅÀ2 ÚÓv«ã"¢å?Ðo_Û¼ñÏ·Šg8˜îüf°$ßVQ§ô®Ê?|&jÿK³XÙŒ˜…ëî9NºÔçÙ‡®UåV+“ÙçÝhâý !9±ó $ÅüÈŒÏu7‹äJüy a?Ögjä²}¾u ¶µËοŽï4Û(Ö8ëè¯1pô¡ky³#¯»Ã73HâòpAÈ.…²Ë dåÁöÉïèêXÞ¡·¹Eq÷¾¿í©æ8ÑžnX}ªžË¦°‰²<ƒŠÔçõcÐóIBHzh »Ì@°úUòÔŽë.FŸÏáð1UúÇ}SŠƒ,Ãï àÐÀë-x OqÄñK=8èjÁ>GûŽµã ·Žž'xœÇ±eꬫÁ@ ¬—f±4{ûeÛºµÞÛX™Œ¨R>ÆcW—ªÂá bªG°–3PwKÏáoëÃWîöDÙ® *š¢·+›‘æÄç©É„㬓ެ>ǾÔ@s;.biy]Í©¿qüñÕN4ÕEøRwü-¨¸õ/–ßàÅìCT¤TWüózæ%À«G8¤HQ¥Ì`Wß¿Iü[žÊò=‹äî‘@˺éÅ®ªúnjxˆ©¥wXYzŠ“ž dé »šòçá+E*Nß@wau´à8@É™‚5ÑÈh§,u#¿Ëž—Mƒ=A¯â†uc2Õ”I(Çñ¸ïaJ§ÌEü¨˜Š›LQÍÞÅ~GüK@b0]Õ/ÀSwµãøìFbWÖ‡Õqí²Ñø7 È;Däþª±šH Ù de´œÂKˆ4Eª»îKOdŸ)Ÿ¿êY”}¶Q•©Z“±4Ü'‹·‡u¦ÝŠðXÞð'Ïg´³VÏC¤¢[íSlS5ù.1õõØ7»Ê@d½-™ên‡éËŸ¿Õ“Õ@ðx@fŽ„\ ÈCP§v.Í`þ¯'ðw àÛúvEcªl¬6` ê2›Ÿ(w¼£ièóÂ_÷”)?OUîvÉ*dI6ÍÕÀ¾À÷tc_ó“ÄŒ%áØUÒ<—!PñºË>ˆ!€’É@ìswãPC޶1ü80ƒÞó˜zöKŠÆhó D¹¨/‚§8®Y­ÝޓɷYŸ¥”ËøÖ@Õè› ÆžË̪M6Bˆ04L),õ#>Ò²óRX@rÆ•¼V¯ Þ#`n¢}ín=?),aÙp k헂جßö<ç:Br„¢áåëtTÌÞÃ~Í#@6s]–:S D/ÃïKþ}mL"7VÜQ­J×I%ý¥šÝæUŽWˆÅž/ÙØ úŠ"Å–¯5A„ìh ê–Ÿâ¤'Õð´àPç<^z‡•Õ7˜ŸÁáú\š…²ÉÛŒi¼ï²O…MͤòL *RgªUéU“z½eªë«[ªšhìy1h¦ñ~{ë9^¬&¦ñÊ2—BÈÆØù²Oã]}Á{¨ò±ÿXj!a_ùîàôðSåŒ!'Æ®¥z– |¾Æ<žœÀñ†Äb¼DÏ"µoV»PÖøìÒ7Ëì¼A{ƒ¬M3¼žêR­H‰=m}èŒï­»¥§É…„‰² 'T “uÕ#ÿÍ%RXûŽu¢¢ç)SXdÛ (o¢z/½ß§(ì|Ù¥Ì$Çc7ÑÎj$dG¡L)K6!³¼C×¥ôÙ—Í€²YšM­²îÔYOÙî Z„žf­6%4b€¥Ù Txï ý/š!;C²ºˆ®¶›8p¬ûÜݨh›EÓ¥ô=m¯fóxJ!„-ƈLua­@ïü¬,¿F¤³'ñ°P¦°h „ÝKº15sXÃácêu_ÏqÒM¡Bv5Y{ Ïâ+uj½RX4BÙˆh ›e ÿwó"ª`³Ÿ‰·|¯Õž~üo“Ë!„.Y du‡5.Ê–Âz›i ¼Åðå3ø>´ùïXé8‡jÿ7½œÍ`qî®úø¾Þ‹÷œò÷cæoõV+˜ºçj\çð}cþŒ©6‰¿ÄŸC}8ï=‡jçE\[Ð)ìï9D:Zq¢>qœ:_®Íydöò‡àù ÇMˆq ¢wæ:ˆLuvÂê{„ù-zphȇšË’c1+èo<ƒ£×äO¹]Áèe/޶FõË}>+yÌ·o±‹â7Ï|’­>¹Ž§uüù|«o_ãÏÖ ¨öê4æiYÁðe/ª½©ñœ _>ƒšË”q=ïÇ÷΋ÊF=YßÔtDµ‡þûλ¼ðÞ™ÇâÛ·XÍ#rùêü¾ï–kùéÎÃ(:û ÅMHn¤&_þµÙ„$î$[1,Ïg§zÉÔÃâP@1ø½¸?Á÷rR°ˆˆïŒÆÔÔ±ÿïNªÝAü)‹}uªÕõüŸÞa5¼Åÿ µ¢Æy-©^ËÛG8ï<ƒóê4`ôŽ:›ÑÄE“ë ¼÷ôz=+ˆÞé@ó ªgPíôâÔ͹Ìr.?Íy"Ý~q²‹)ù³Õ‹šÖ¨ò±1œ’§§æ"¨s¶b8ÏpÕs§†ÒÜWþ=†S΋š´Å̵s¨n|hÜÿ鵆F%YÏgu¼Ca:‘ŸU½ EÌú)µÅ¡€Â½)åù&f¡Mhì^ãa(€W3®ÎÈRk ÔéÜé¿jFµ{Ý×x>‚Î ¸ªy…È"†/_@Ýå1ÌĽ£è½ŽDj*S(×òÓGVÒí'7!»˜0—¸æÕšÁêôuÔ¸®ãaêoÿ‹§¿E“’§/ýS/g®ëäÔ)›µ™^3×Ó4l*Ökuæ:Žªz ¦õÊMö|ÖR; óSÞù'f¡Õ¥3¦ñgˆ\¾€OÃϵwúšüÒðŽê¤©V§‚¨vv`Tu˜Õé j\Aü©ø{â;Z‹o!"»Ë?ƒw^æV~¦óÐ;¶ý ÅMQ°ó $™úPÈkôÿœHÉÿñÁ©ký8ï:‡†é )™ëÚÆq!‚ï²™^37P£Ó€êÇw-Q}£SÕ9f<‘—o˜’§šÔwþ‰IšôŒÕçÐäñâèÏý˜Ñ3°…~|¯šm¤;¨ 5tAwÜáåfT{Ô©´9üæ–5æÓ×=¶óão —Ÿõ<ôŽm`?CqBì|™‹ ÎéE÷þŒ½Åjü%þ 5£¦þ:ªŠèµ ¨vžAÍÏ“Y¦|>€×y§B©ôI4x¼¨–§—âÿEƒ;1åóÿ’S>ÿ7÷×:ÆT=›DáÄÏÔÔ÷a&“á¼¢Éí…çfbwùy×~>§Ä‹ Nc2“ðdh^œ‰à”Ë‹2œw2ÎkQ,¾}‹Å¹1xÝ^œºùLg°?19@9)Aß4Ü{–8‡Ø<ú[/¢ÚÝÑŒk5²—oì<Ï_8nBv/;Þ@+Ã#øsºžz/ª]çpÂ?¢{—¹:Õ‘~ᛊ—S7à©÷¢Æu'.Gðç*%`ya¿56­M·õøû1ú\Û³õ'dÏgÏÕ/Ï¡ÉwG]^õ4£áNT1Hw%|ôŽ¦ÆšèùèŽÕøÆÇ^]˜\/ÛÛŠ–q•y¨ÒCkR,¬|‹ÿMEpÞ›¬m¿«´çg°nr¹V„êÌàuSH¼ÄÇkQ,™P\Þˆñh ñØ4BµV˜%3ìë?ÏÔv%•~Œ/Ä_‡¿Ò†²RkÖ‹4¾0P­ f“¾hê¯Ó¨-–`.Wß;Gá.‘`²wdþaåÝ@à³™`¶¹ŽÆDZ0=„ƪRÔÊCŒ6Âf–Pd÷ <½€x<†éŽZXÍ&X=ëÁl¤¾r9?#=ýú7v¾J‰cºÑ‹d‚µ6Œ˜º ôcK•[в2;Ü¡iÄâ1DÃnXÍŠUv­ë§cÆQe‘P,û²û›`’lh\«¨!TZ$»Ó©ù®t—CÝlÄ@ÖÏ9M¼n 7|6½”V þR$KÆ“ÿo´IJ<ªW¢±7–&Hl+¿x|VH¦Røå-ÑP,’•Ùr&y7Ê$ ¥þL¶‡ßn‚T\ uÇiÁgƒd²£#䥾Ò¢—B¢ñ–ºþœ¯Ü@b×Za–,°7Nçù®QûÛˆºK If”…”%WA²È{D¯ë|äóCå&H%©ÞF"&Ke-ÊÍ&Ø;’ç0]‹"uOE±ïJKnu³ybôº)< Ü@:`—”wNk[úK!IÅpG×·+IüGÁ´»Xç"]@ØW‰Ò’"XÌ&e]ÞãˆwÀn’`õ¤z:q„ÊÌÆÆhòn É‹ªÈ·?Œé˜Þ/:„2“„"½)kQJ$sÒør­/6Ô1RÿFÎw½'PYVÉT‚Ê‘¤Iƈ ý6¢SO~»ÊD7^×ù0Äõ’ìmÄ;`7™PJ4œæòpò˜%L›÷]iÉ­n6Ï@Œ^7…GaH,1Í׿ÓIF„Ëa’,‰1Šäv¥:Ià˜Oûãª*‚d¶¢64Ž…µ{ >«©Ly9‡+-ŠRñøQj’`óh¤6c $B­=‘o—$ fužÙÀ´è²r®¯œŸÞ…l¸þ³/’ ¹É ³I‚ÉêÑÈ£±¥¼•A’¬Xû‰ä¡®ó2£hÁ«”ìm„ËaNš\¬Ñ–ìÉ'{¥~Eï-Ÿß•¶r«›M3£×MRز)=!Tšõò¼‰ü¯Ú@0íF±dFyˆzJŒwg 6°ãµEŠª4]çéÚLw§1D‡Bh¬-…E’çÜ©„O4Kpï³’cýH¾ë yh(1þ`¶y0.l"Æc3l Û¤"Óª²@²%Ê£”HÅp«{Û@¾¿+-¹ÕM.×JÎ=C×MáQà²c ‰‹Bý£‡Ëa‘ô.Šø¬Lö*”I°T†ŠÁ61ÛD5Î’C~Ü_*­7©z)ÑÞ¥©JÍÓˆ¸¯U±ŠóU6äñqlf &k-†„LÄxlÆ dsÆ@b6tSf†*-,¥(-–Ÿã4j‹$XKKaÒ|Ÿùý®´äV7¹\+¹Õ™Ñë¦ð(pâC²YX ©YX¶Ä,,¿lÖPUr6G¦3Îæˆ#Tnd±Ã7žœmrÃVb‡½Dÿ¢ˆwØaJ­ˆ7:!Ãè:ä8KQ™Ó±8b ã𗗢ܮŠ;Öˆ2[%|¡qDâ@<†è¥f %ò;DÙl’Ðôbñ8bÑi ùÝ(³U­Ý}¯¯ŸöB6XÿÏWÝǧQj‘`*©BXÞÐÄý(U­’×bü·aÜ@6^׺ãHCU°HE¨ /Ÿ0*[ÿËnãÕÉ)®šóÉïw¥GNucôZ©3ƒ×M¡Qð±!Õ:;jÓ¬±+æ“ëϵ£±<1ßÛd2£¸´ ÑdoG÷®*Œr³vÎ|FrXHSÄmCeGT'¯G4ìCeiIbžz¦¹ö áäœ|Sò˜Å°–ÕÂ?Smf°¾6p~º²¡ú7v¾º yÔ{‘Sq%ÖÆÔc>X¤) þ6r1`cun"B¨Ê†âµí ë@ÖÎ-a¢š¸“ÆbÒûíçñ»J{~9Ô±kE°Î ^7…DÈ6%5é/ÜRÁÓQ“ÉŽ~…;‚÷ö‰] d³‰O£ÑnѼ#;‡¡J‹îd ²=¡l 4MÄo— I&XJìð‰Oí!„ä dk B‚B!D!„!h „B„ØuÂWUBH~ Bb×!„üP’ËëAÓõ@â©×QZLë;éP½Xh¾²’BÒQ@bìõ º2íƒÕ$ÁRêF(ù:ÊèxÜöÊõûìÒWVBH: Ç@ ½TÏ@’bÎø¨‘ÝûÊJBIGˆ‘׃êHâe0º¯£”o³K_YI!é(1øhl$_GiÍôšÙ]üÊJBI $SïB¾Í.}e%!„¤ƒbh d÷¾²’BÒA€¨6³ñYX_Y9^‹$ÁÖÈ7B H’¸|ÇÚ:¨r‡‘WVÒ@!»„‚0B![ „Bˆ4B!BÐ@!„A!„" „Bˆ4B!BÐ@!„A!„" „Bˆ4B!BÐ@!„A!„" „Bˆ4B!BÐ@!„A!„" „Bˆ4B!BÐ@!„A!„" „Bˆ4B!BÐ@!„A!„" „Bˆ…a ~;$©µÓ™7[ðY!Ieeù[Ô]É\‰¡üD§K¼±’dGG–íôâÛJÞwù„í dÈ‚Ç ÉTŽð&Æ—©|Ãûnaœ„­g׈ÛÉ@ô B¶»Þ@Œö@BU(1I(*ó#º¶aµeV™M$ÌÅ6T6Ž#®.7ìFY‰&És‘åÓˆ ö@†ª,$I%Õ…Êé(Slo²¡ÄV‰Æ¡X–ÒÓHl܇r[1,& ’ÉŒ"«îÐZ͈ÅIÙQÐ@²HÓvX$¬µa¬5¹ÑFØÌŠì„§Ç0ÝQ «Ù«g=øx-Š%J*ý_ˆ#¾0¥ e¥Ö÷–Š/L#TkƒÙd…/š{ù©s*.oÄx4†xl¡Z+Ì’öŽ…¼ÅIÙÞÐ@2H áZ+Ì’öÆiYÏ"¿Ý©¸ãš2lLvtÄ †F›©Ä³ÞkI”w‰ôžÇ@1»3Wš¶ü|6 Rq-”{Æà/5A²T­Õ „†’Ö@JQYVÉT‚ÊЂêH!”™$Õªí@ԃɌÊ!è€]’PâŽj6›vo¡, ì«DiI,f“2UžytC[~✊u*;î/…$#uº4B H:1™a6I0Y=˜Vçíã°iòûJ•…ÄÛ•ê$þc>Û–ÈPU$³µ¡q,ÄR±Äà³JÊ2G )?yN6ŸÎøI¸&É‚ªq±8 !; H†VhÈ «Y‚ÙæÁ¸ÂB(“$”x¢YÛ=!TšõRUC¨´{ „$4,ƒèñqlf &k-†ÖL$™ï/q«Æ6ÔlÎH¬ÑÉdtjmÂ@Ô&—Ã"‰ˆñ1Üâ$„ì4h ¦ñƧQj‘`*©B8•¹‘Í M/ #Æß2[ÕÚ]w|¨*9 «Óyš……¡*X¤"T†4S†µÄ*·@²ØáOÎ ¹a+±Ã^"b @|H6 k!5 Ë–˜…å—eŠs¼ I‚­1ûTbBÈö„bt%zÔ{‘Sq%ÖÆÔÂð”ÛPl1%×xÃZV ÿ¸²Q\»aW¬^’ü+BU6¯ ˆgY_Gcyb½ŠÉdFqi:¢Éž„€@lHµ¤ÄŽZÙ:¬qÒ@Ùñ†BÙrh „B„ B‚B!D!„!h „B„ B‚B!D!„!h „B„ B‚B!D!„!h „B„ B‚B!D!„!h „B„ B‚B!D!„!h „B„ B‚B!D!„!h „B„ B‚B!D!„!h „B„ B‚B!D!„!h „B„ B‚B!Dˆ÷n EQÔî „¢(Š „¢(Š „¢(Š „¢(Š „¢(Š „¢(Š „¢(Š „¢(Š „¢(Š „¢(Š „¢(Š „¢(Š „¢(Š „¢(Š „¢(ŠÒÿ“v# –j)ïIEND®B`‚flask-3.1.1/docs/tutorial/flaskr_login.png000066400000000000000000000164371501065614000206000ustar00rootroot00000000000000‰PNG  IHDRRÜŸ‰sæIDATxœíÝ]L[g‚ÆqßùÒ—\rÉ%—¾ˆZ2ª"”H•+m·(ê(¨‘VÖjR¡Ìh‚VVZÔH%™HXQÚЮڡ©¢5d & §LˆS&|6`Ùx†U5Q`ÜÝÆ;h‹§4âÙ pü˜'Ø>ÿŸôH3öññ9oÉûø|Ïž=!„²×8z!„Tg(B!F¡@!„…!„b „BˆQ(B!F¡@!„…!„b „BˆQ(B!F¡@!„…!„b „BˆQ(B!F¡@!„eßF !„Ta(B!F¡@!„…!„b „BˆQ(B!F¡@!„…!„b”(ùÎõÊí3Ì¥1Íßѱ¢Ï}­éƒþTÉÛF±uj @¶òå§y“lÇWÜvùtñ\Ó\ú±éàPÅNÒ•¼m„{†¡@!Ä(6.ÂTò$]ÉÛF*8÷ N:wC'>»§þ™ÿþ÷ƒ7åöýAƒ‡2®ê·÷¶P –Tò$]ÉÛF*8ù“Ëü¼Â£_Ë{®WîGöôïÃ$¦r¿ÿ¦Ügo«ÿ Ç¯Ôqµé¶P –Tò$]ÉÛF*8ÛL.sÃÃrûútz¤¶±H(êØÛÈ—Wzå>[üQ)“ôôø„>¹v['»åù°_‡ÎöÊíëÓáŽyº†uúÚ˜w85075¡ËWoéÄÅéèKRèè×±ó7uâÊWêŽkpfaÛ¶ O.»Ë,½Ÿ¡?lsÚ׺?5¡ Ÿ Ésîºe?··â%œí&—ðˆŽøzubh>÷ñ© u|zSÇ:úR?×ç‡tfh&{0“û£w彨¯C¾>þð¦N=Ðôаܾ[úÒº\±#™iue~æÎ^בóC:Ý?¥ûéç¯öùyÒ'ó{ÛÎpß`j˜V×§ƒ:ÒÑ+w×½ìû¼qÍËôØ×:}醎œí•ûìu뺥 #³Ë•:ž¥nKvßçgtùÓA9Û«C:þÙ˜Fæ÷¸¯;„±þÇÞ­@æÇtÂ×+÷ÙAžVxv!uJ |_g.öm½îì |£óëŸ ß‘çlfý×u²oB#3 š›Ÿ×ý± ]¸<°í¾û¶Í¨ëÒu¹Ï ëòxa]LJƒzóÜ |¡šž_ÐôØ×:ÙAÔT¶™èR“SŸNZŸÓ‰Ž^»rOýãsš›Ÿ×H莎wôéxÿƒìrsáóõéͫ㩟ߙ}þÛ!º|³„™ÓåK}:|é®ú§æ57¿ ûãÓúäê°:,GC;”¸©ItHÞ+ú0ú@Óeœ8K)¹±;òøúäùlL·¦æ57û@k7uØw]ÞМÑx–º-©}Ö©OoéÂÈMÏÏ+4(ÙiutõéÐůØáè§`¾^Îå,sëêRKÙæÈÉs½:ÔuÏò3¸ Ï¯ôÉ}þŽnå­ã~pHî³·ôå|TÑè¼._*öI~V.ö–P :åëÕÉáâr²¯Û¶@JÝÎô$êë“7´ó{•e\ ’§óùÿÎçõùå>¹Ï¤·oãY궤öýºNäî{¹ÿ}×v¼€SXÛgJ§ÏZßÿ†.Œïð|Ç-}9[¸žÁ«ý{+™ 9ß§C]ww=4-,a}^ÎOe¤òRä.,·¯WG®|÷ó2¡Sg{u¬Ø§Ó©{zÓw]gÂQE£ãònó!j¤ïF ’ž?¼¥ ÃS™->¹o_ ¥ngf}A?ã»Hjœ<×<—ºþtC¦ö>ž¥nKjß‹¼vøVYÇ„±¤¤™} /û¾ÒÉ®:ÒÑ·uÝ  ýòY_›>ºÈ9J¹©SWïé“дFŠ\÷ØuÛ¦Æu*³ÎsP H!í¸Ž¢GA¤¦Rp2§[C·åñ]×ñ~Ë$œ9=»CNDM-wrh»ët%\™šPÇ•Ôyy·¯W‡ó®D£;H©ÛÍœÆyAâw+ô8žÊŽŽÞÖ¡Ìü°Çñ,u[²×@ò—ùƒÜ¾›º\¦[¸mW ;e×™ÓIë¯MéR×èlz™)îÈ+°uý |z}ÇüÃç‡u¦Bá"Ÿ 'ÿ›:‘÷+\_¾¿ã‚utq'WÍg›‰n¤Pn߀|áÌÄ•:µôf‘Ó³9Ùïˆ5ó ‡'ôɵañåž›ßñ¤¤íÜa}ãš?NzBì-/¶@Òç.-ÏŸ²^?Ø­@¢ŠN}­ãg·/L]¼[p~·Øõ ÏÕ1uX/Þûút"¸ý?,n¶a¶è¦uæ\êCDêCGúçûâ]…w\ç~¯Ïç—s§‡†¶9SPêvtTÀ5 doy±’:÷ºõ|þO jnü¾Î\ØáÔW*Þ¼‹ŒÛv>ý¨ ”ó®½”º¤&³ÃDwhX‡|ý:3šþY³ÜÝŸKÝ™7õ@ƒÃwuêÒHöh s×Ðñœ»††K» kvL§.}¥Ë#3 Ïdîb¼§“½z³Ïòi=<¢#¾+¸…¸Ôí<ØIßu™¹ k&sÖPê.¬á»°JÏR·…1È‹-Ô¡æÖó™CÐLò &¿@R¿{ëðo§R?433êéÂÕÛòvЧïAÉÛvh(÷õçG4XÊi0 ¤ö³ãD—¾.—= ‰*:3•þŽF_ú; 7tüÓ;ú|,÷\~±ï-Üm,(<úµÎ\L}‡£È÷@R™Sàê<™#ì¼ï”°/¾@¶û¸57L‡ó¾rñ–:JüH±ñ,õ¿1b[ ³ºpÞºþ¼oñüÚõâ²ÝÅîþß^ÏYwþ­‡;oÛœ>¿œ{}åÈgŸÜ(òâ’¾Æw~÷SK¤vÆ“±d· öþð°[¯UtÝQÿÌ‚¦§&ÔÑ•{}dÛñõêðÅÛúdôîÏG57?§[¡;:n9ýuøÒ×?4»Nþ³ãòæœBËý²RIë ¤¤L©ãÊ]}žM>šy @ÿ°Žø®ëäðÜo[5¦zdz ¤Ä?(ue|ûul÷G›ÒÙºç|Aá‘»òvÝHýZ_ŸŸ»¡ãW¾R×ȘNu{ýÖ9̹™†îÉ÷鎟ßúU&î³×uäÃÔz. ÏäNê»lÛ©PTÑ™¼/1ædHŸŒí¼Ž’γ’ªË®ÿ&lƵ|cQB!ä BB1 B!Ä(!„£P „BŒBB1 B!Ä(!„£P „BŒrà¨NÀ0BŒP #À0BŒP #À0BŒP #À0BŒP #À0BŒP #5P ÏÔÕÙ+·o§ÜV ê»Ê.û  ZÔ@l¹×›7¡vÎé/e}‡Ê`—ýPÙ(*d—ýPÙ(*d—ýPÙ(*d—ýPÙ(*d—ýPÙ(ëŠ.<Ô_üQouè•÷{å~ÿº^ë¼¥“_Lé£ÙÿÖÿìð굕oõQïhêµ¾^¹ßЗïéüôŠ®~QìΩAu=.ó~~7¯7ŠÝ¥õÙŸýχúà‹ÛzãÜõ­íëþ“>úÏ¿ë{+vFäŒÆ·úÀ=ûú7ú¿Õòúsýcýïé¿•}üÿ¸†¿+|ùÊŸtÔòþoõ?Ñòºôµgøb°`Bÿùøÿ½Ðý\›-xÏ׺ç5ñìÇÔ>õYžPÛ¬Ùö°' $c}E>ë÷,Þÿ“îmXžßx¢_¿o]÷¸î­[žÿî¡~îÛáõëÿ¥“^ ÔÀZî6¿Å©0†(´¿ÜÊ}m÷ŸóNU­+лþ£Ãÿ}v¢ ïõÿ¥µœ×ÿ]ù¸@.ÎkÙºÀÆ_õ¯93ªáõ"+€"(IÅ&w÷µ'…ë¿–·ÌÅÌúŸ©ëân¯ÿQŸpä—ÚÆ·jË)Û ¬YAH’VäË;½ôJÿJÁRý×s×ïû“î•üúçþ†ä³ „_… Dˆ¤’ ¤—€ D’mNaQ ÊȾòø½æëÕ¯Rÿ×ä"úÃ[Û?[p=ÿõpPFHº@´¾¢öxïDÎm¼®üÛx)eD,X{ö­|_$”Tä‹„#{ù"áz…|‘PF5P ¥ü¡¥í“S ’ ~•‰¯Wnß~•ɨ>/üU&'Ç÷ò¥‹=üA©í~•Iz_>ø«.,´lÎ}£èžÿ;°›(jQx½máùAo£@^šü;µ8] ºQ eów}t±W¯\û¶øoµ]É=­t´w…ß~  ªQ e“*·o@ÿ:ü­¢Ï~LÄÆº–ÿògýúâÖ5ˆ·zÿªÂ¯)@u¡@ÊèkÏ41ûPç¯êçþ!=—º£ë•szãòõëÁ‡yÌo+P(€ `„¡@F(€‘ª/h4J!Ä ¶/ÀÁ @F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€‘š(H[½‡‡:c9ÏÅ:²ÏÕ·EÊò~ `ˆ±o,Õæq«Þå”Ãᔫ®AnWþ`Lɼõ'"Ô²uN99]õr{ÚŒå.ô8ÒïÕ ÎåUÛšÔàrÊÑЮXÞóÞžnµ67ªÞå”ÓU¯Æ¿&[ëšl«Ënw*©mljiWp9÷}–õ¶ºÕênPÓ)§«AMm-'—ló¨±Î)§Ó¥zw«zbù{Yú~€dÛ‰¨­Þ‘7AgR¯vKÏ$B^Õ]Î!‡³Qí‘­ÉÕZÏÖû:êó ¤x\ž`¶¼ Ä—GAKÙv[¯ËUøxW“I³ýÉ®²êWcú±ÆöˆI)¹º¬H¨[Þæfù³«˜”·.S­ ÄJ*©Õˆ_MÎôãn¿VÓKçD}‹‘Õœ£™œç¼ -'”ˆõ¨Ù•yÜ£àv;™\Õd§;ûú¦ž­±ˆÓÝ©ÉDR«¹²%àR“?¢DrY=ÍÎìMkØl?@²k(¤–ôÄèr{åï *4Ójþ‡ìH›êvødŸJ³éÅ· ©–`á'öâÏ'ÕÓœy¼QþÌ,½–¿µIõ.9‹¼o½å0)`Yok(ý`¢GMÙ#–V…3w7e×ÑÜ“4ÚOj¤@bí[%ÑÐ+é¹ÕP›šêy“¤SõÍí g>܇Z‹NÞ¹iRæ` çHîfìø| çÚ‰$MÊ»í)¶TêÚ&‹¿>³Þd@Í™å;µœ~8ѳU MÝ £ý©F Äú©ÚÙÊy.Øâ,œ0­¯]i2RÀïÉ^pyé'­ŸÌ=‚‹ëù‚EPÚóöfO?9ÝŠd‚žÝ $³ÞœÙ:ýd«ìxìq?@ª‘ÑòÖ5 ‡³Qíáe%“I-‡ÛÕ˜9‡ïh”9ó‚ ¼îVùƒ“Š-§Î÷'"~¹3Ë6õ¤'Q˵G½<Ý“ZM&•L¬* ©»Í£FïÖU‹²Ȥ¥@š»µœL*¹’·Ñùb dû R­ˆ’šl³ÜõT$ m“[Ÿ¬­“k‘4ZÎ/%B­Ûß”þÄžQ¶I†-úOaeÖ»§ÙÛ~€T3"IIEíjijP+ó=†:54µ¨=PøÝŽÕH@­Íjl¨“ËéÃéR}c“Zýá‚»‘€Ú[ÜjÈ|?¢®^ n¼þ "–³be+IÉX@Þ¦†ô¶Õ©¡É«žNOöZE¹ d/û RMàe¢@F(€ `„¡@F(€ `„¡@F(€ `¤ê dssSOãkZ\ú›"W!„ìÅ¥¸žÆ×´¹¹I<¯éÑãïµ±ñ|ßë€Z·±ñ\¯•øÚ¾×Uõ²¸§<`66žkq)¾ïõT}D®ì{`7å˜;)°! ¤LƒvC”iÀn(2 Ø R¦A»¡@Ê4`7H™ì†)Ó €ÝP e° ¤LƒvC”iÀn(2 Ø R¦A»¡@Ê4`7H™ì†)Ó €ÝP e° ¤LƒvC”iÀn(2 Ø R¦A»¡@$-.ŵ±ñ|ßë»ØØx®Å¥ø¾×Sõ²_Ó£ÇßS"P‚çzôø{­Ä×ö½®ª/ÍÍM­Ä×´¸Wäá !„²¸×J|M›››à`P #À0BŒP #À0BŒP #À0BŒP #À0Ri«—Ãá(ˆÓU¯Æf¯º'eyŸÚ—TOsfüå_=èíPÉjº@¶R¯Ö%²; @éj®@:c©1[-ÅÒЩ岼[-£@”®v D’V»åÎ…4+L=<ÙV—w„┫®AM-í .'sW¾T›Ç­z—3»œÛã•?Sr¯Ë[äL¿gSOæˆ(¢¶ºôvxÙeí®ô¶¹Õm™È‘Ëû¤NÓ¹=mê‰äa=™}kPçòª‚mMjp9åhhWj„’ŠõxÕTï’ÓéR]ƒ[žö€Ú›(¥©ññ«±¤±ÄåQ0;GÔV¿ýi±öÈ—Kô¨)s}¦%”zlÙ².¯&%I1µgÖWŸ™ð¥D¨Uõ%ž¦³ˆÇÓ°µ\z}Ë=MÙ2+ ÀÎj·@ŠœÂŠm·‚äª&;ÝÙe³G–jl(‘”’«ËŠ„ºåmn–?³ÂR—Óª:3y›"’’fˤݠΘ¤ÄÖ‘“ËN¿vRÞº­²h Ä”H& ´l•J]¦€¬⣾EȪåˆ)¬V—¥x‚©uEº›UG(QÍHñÔå^D_ ËßÚ¤ÆzWÑOáõÙC†Zœé‰Üí•¿'¨ÐdL«Éü-(u9붦NM¥NUÕÉãqgË+ôd·ÅLf^¸5¹»»µµ7 u»·ö³-½é[âTK0oC"í[¥“³®˜Ú(¥©é)~祿۞nJ¥®m2»ôj¨MMõ΂k&õÍí [;©ÄåjÍ––'IªrzœL„³%¤°7sŠ­IÙK%–×eOežjqf߯5ýTÎ5üC¯ðÖº-AËIõp @‰j®@ fË1ٗʼ<H¥HæÄz4ðŽ@(¦(J-ì5›%]ùÁðÀRrèÕü¼­=×Oñ¨®¾çµÜ…õ;-äÝ…å·Þ…Õë×ñW©ßÿ@xñ(ZÊø5Í».b³P ÀËCTqá×Çá¿(þúÆtîí#zý7s¾]`H5'Uàß¼zýè15{ÞÑ/~3ª'EO_Ù'ðòP ¤¦B/OÕ‘dÂ_$^®ª/þ&:ù‰¿‰„ª/ÍÍM­Ä×´¸Wäá ±i—âZ‰¯iss³ ÿ,”¢ê p0(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F(€ `„¡@F¼@!„Ø3!„£P „BŒBB1 B!Ä(!„£P „BŒBB1 B!Ä(!„£P „BŒBB1 B!Ä(!„£P „BŒBB1ÊÿØlÎÇQØIEND®B`‚flask-3.1.1/docs/tutorial/index.rst000066400000000000000000000040151501065614000172460ustar00rootroot00000000000000Tutorial ======== .. toctree:: :caption: Contents: :maxdepth: 1 layout factory database views templates static blog install tests deploy next This tutorial will walk you through creating a basic blog application called Flaskr. Users will be able to register, log in, create posts, and edit or delete their own posts. You will be able to package and install the application on other computers. .. image:: flaskr_index.png :align: center :class: screenshot :alt: screenshot of index page It's assumed that you're already familiar with Python. The `official tutorial`_ in the Python docs is a great way to learn or review first. .. _official tutorial: https://docs.python.org/3/tutorial/ While it's designed to give a good starting point, the tutorial doesn't cover all of Flask's features. Check out the :doc:`/quickstart` for an overview of what Flask can do, then dive into the docs to find out more. The tutorial only uses what's provided by Flask and Python. In another project, you might decide to use :doc:`/extensions` or other libraries to make some tasks simpler. .. image:: flaskr_login.png :align: center :class: screenshot :alt: screenshot of login page Flask is flexible. It doesn't require you to use any particular project or code layout. However, when first starting, it's helpful to use a more structured approach. This means that the tutorial will require a bit of boilerplate up front, but it's done to avoid many common pitfalls that new developers encounter, and it creates a project that's easy to expand on. Once you become more comfortable with Flask, you can step out of this structure and take full advantage of Flask's flexibility. .. image:: flaskr_edit.png :align: center :class: screenshot :alt: screenshot of edit page :gh:`The tutorial project is available as an example in the Flask repository `, if you want to compare your project with the final product as you follow the tutorial. Continue to :doc:`layout`. flask-3.1.1/docs/tutorial/install.rst000066400000000000000000000054261501065614000176140ustar00rootroot00000000000000Make the Project Installable ============================ Making your project installable means that you can build a *wheel* file and install that in another environment, just like you installed Flask in your project's environment. This makes deploying your project the same as installing any other library, so you're using all the standard Python tools to manage everything. Installing also comes with other benefits that might not be obvious from the tutorial or as a new Python user, including: * Currently, Python and Flask understand how to use the ``flaskr`` package only because you're running from your project's directory. Installing means you can import it no matter where you run from. * You can manage your project's dependencies just like other packages do, so ``pip install yourproject.whl`` installs them. * Test tools can isolate your test environment from your development environment. .. note:: This is being introduced late in the tutorial, but in your future projects you should always start with this. Describe the Project -------------------- The ``pyproject.toml`` file describes your project and how to build it. .. code-block:: toml :caption: ``pyproject.toml`` [project] name = "flaskr" version = "1.0.0" description = "The basic blog app built in the Flask tutorial." dependencies = [ "flask", ] [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" See the official `Packaging tutorial `_ for more explanation of the files and options used. .. _packaging tutorial: https://packaging.python.org/tutorials/packaging-projects/ Install the Project ------------------- Use ``pip`` to install your project in the virtual environment. .. code-block:: none $ pip install -e . This tells pip to find ``pyproject.toml`` in the current directory and install the project in *editable* or *development* mode. Editable mode means that as you make changes to your local code, you'll only need to re-install if you change the metadata about the project, such as its dependencies. You can observe that the project is now installed with ``pip list``. .. code-block:: none $ pip list Package Version Location -------------- --------- ---------------------------------- click 6.7 Flask 1.0 flaskr 1.0.0 /home/user/Projects/flask-tutorial itsdangerous 0.24 Jinja2 2.10 MarkupSafe 1.0 pip 9.0.3 Werkzeug 0.14.1 Nothing changes from how you've been running your project so far. ``--app`` is still set to ``flaskr`` and ``flask run`` still runs the application, but you can call it from anywhere, not just the ``flask-tutorial`` directory. Continue to :doc:`tests`. flask-3.1.1/docs/tutorial/layout.rst000066400000000000000000000055761501065614000174710ustar00rootroot00000000000000Project Layout ============== Create a project directory and enter it: .. code-block:: none $ mkdir flask-tutorial $ cd flask-tutorial Then follow the :doc:`installation instructions ` to set up a Python virtual environment and install Flask for your project. The tutorial will assume you're working from the ``flask-tutorial`` directory from now on. The file names at the top of each code block are relative to this directory. ---- A Flask application can be as simple as a single file. .. code-block:: python :caption: ``hello.py`` from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return 'Hello, World!' However, as a project gets bigger, it becomes overwhelming to keep all the code in one file. Python projects use *packages* to organize code into multiple modules that can be imported where needed, and the tutorial will do this as well. The project directory will contain: * ``flaskr/``, a Python package containing your application code and files. * ``tests/``, a directory containing test modules. * ``.venv/``, a Python virtual environment where Flask and other dependencies are installed. * Installation files telling Python how to install your project. * Version control config, such as `git`_. You should make a habit of using some type of version control for all your projects, no matter the size. * Any other project files you might add in the future. .. _git: https://git-scm.com/ By the end, your project layout will look like this: .. code-block:: none /home/user/Projects/flask-tutorial ├── flaskr/ │ ├── __init__.py │ ├── db.py │ ├── schema.sql │ ├── auth.py │ ├── blog.py │ ├── templates/ │ │ ├── base.html │ │ ├── auth/ │ │ │ ├── login.html │ │ │ └── register.html │ │ └── blog/ │ │ ├── create.html │ │ ├── index.html │ │ └── update.html │ └── static/ │ └── style.css ├── tests/ │ ├── conftest.py │ ├── data.sql │ ├── test_factory.py │ ├── test_db.py │ ├── test_auth.py │ └── test_blog.py ├── .venv/ ├── pyproject.toml └── MANIFEST.in If you're using version control, the following files that are generated while running your project should be ignored. There may be other files based on the editor you use. In general, ignore files that you didn't write. For example, with git: .. code-block:: none :caption: ``.gitignore`` .venv/ *.pyc __pycache__/ instance/ .pytest_cache/ .coverage htmlcov/ dist/ build/ *.egg-info/ Continue to :doc:`factory`. flask-3.1.1/docs/tutorial/next.rst000066400000000000000000000031371501065614000171210ustar00rootroot00000000000000Keep Developing! ================ You've learned about quite a few Flask and Python concepts throughout the tutorial. Go back and review the tutorial and compare your code with the steps you took to get there. Compare your project to the :gh:`example project `, which might look a bit different due to the step-by-step nature of the tutorial. There's a lot more to Flask than what you've seen so far. Even so, you're now equipped to start developing your own web applications. Check out the :doc:`/quickstart` for an overview of what Flask can do, then dive into the docs to keep learning. Flask uses `Jinja`_, `Click`_, `Werkzeug`_, and `ItsDangerous`_ behind the scenes, and they all have their own documentation too. You'll also be interested in :doc:`/extensions` which make tasks like working with the database or validating form data easier and more powerful. If you want to keep developing your Flaskr project, here are some ideas for what to try next: * A detail view to show a single post. Click a post's title to go to its page. * Like / unlike a post. * Comments. * Tags. Clicking a tag shows all the posts with that tag. * A search box that filters the index page by name. * Paged display. Only show 5 posts per page. * Upload an image to go along with a post. * Format posts using Markdown. * An RSS feed of new posts. Have fun and make awesome applications! .. _Jinja: https://palletsprojects.com/p/jinja/ .. _Click: https://palletsprojects.com/p/click/ .. _Werkzeug: https://palletsprojects.com/p/werkzeug/ .. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ flask-3.1.1/docs/tutorial/static.rst000066400000000000000000000061421501065614000174310ustar00rootroot00000000000000Static Files ============ The authentication views and templates work, but they look very plain right now. Some `CSS`_ can be added to add style to the HTML layout you constructed. The style won't change, so it's a *static* file rather than a template. Flask automatically adds a ``static`` view that takes a path relative to the ``flaskr/static`` directory and serves it. The ``base.html`` template already has a link to the ``style.css`` file: .. code-block:: html+jinja {{ url_for('static', filename='style.css') }} Besides CSS, other types of static files might be files with JavaScript functions, or a logo image. They are all placed under the ``flaskr/static`` directory and referenced with ``url_for('static', filename='...')``. This tutorial isn't focused on how to write CSS, so you can just copy the following into the ``flaskr/static/style.css`` file: .. code-block:: css :caption: ``flaskr/static/style.css`` html { font-family: sans-serif; background: #eee; padding: 1rem; } body { max-width: 960px; margin: 0 auto; background: white; } h1 { font-family: serif; color: #377ba8; margin: 1rem 0; } a { color: #377ba8; } hr { border: none; border-top: 1px solid lightgray; } nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } nav h1 { flex: auto; margin: 0; } nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } nav ul { display: flex; list-style: none; margin: 0; padding: 0; } nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } .content { padding: 0 1rem 1rem; } .content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } .content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } .flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } .post > header { display: flex; align-items: flex-end; font-size: 0.85em; } .post > header > div:first-of-type { flex: auto; } .post > header h1 { font-size: 1.5em; margin-bottom: 0; } .post .about { color: slategray; font-style: italic; } .post .body { white-space: pre-line; } .content:last-child { margin-bottom: 0; } .content form { margin: 1em 0; display: flex; flex-direction: column; } .content label { font-weight: bold; margin-bottom: 0.5em; } .content input, .content textarea { margin-bottom: 1em; } .content textarea { min-height: 12em; resize: vertical; } input.danger { color: #cc2f2e; } input[type=submit] { align-self: start; min-width: 10em; } You can find a less compact version of ``style.css`` in the :gh:`example code `. Go to http://127.0.0.1:5000/auth/login and the page should look like the screenshot below. .. image:: flaskr_login.png :align: center :class: screenshot :alt: screenshot of login page You can read more about CSS from `Mozilla's documentation `_. If you change a static file, refresh the browser page. If the change doesn't show up, try clearing your browser's cache. .. _CSS: https://developer.mozilla.org/docs/Web/CSS Continue to :doc:`blog`. flask-3.1.1/docs/tutorial/templates.rst000066400000000000000000000154611501065614000201440ustar00rootroot00000000000000.. currentmodule:: flask Templates ========= You've written the authentication views for your application, but if you're running the server and try to go to any of the URLs, you'll see a ``TemplateNotFound`` error. That's because the views are calling :func:`render_template`, but you haven't written the templates yet. The template files will be stored in the ``templates`` directory inside the ``flaskr`` package. Templates are files that contain static data as well as placeholders for dynamic data. A template is rendered with specific data to produce a final document. Flask uses the `Jinja`_ template library to render templates. In your application, you will use templates to render `HTML`_ which will display in the user's browser. In Flask, Jinja is configured to *autoescape* any data that is rendered in HTML templates. This means that it's safe to render user input; any characters they've entered that could mess with the HTML, such as ``<`` and ``>`` will be *escaped* with *safe* values that look the same in the browser but don't cause unwanted effects. Jinja looks and behaves mostly like Python. Special delimiters are used to distinguish Jinja syntax from the static data in the template. Anything between ``{{`` and ``}}`` is an expression that will be output to the final document. ``{%`` and ``%}`` denotes a control flow statement like ``if`` and ``for``. Unlike Python, blocks are denoted by start and end tags rather than indentation since static text within a block could change indentation. .. _Jinja: https://jinja.palletsprojects.com/templates/ .. _HTML: https://developer.mozilla.org/docs/Web/HTML The Base Layout --------------- Each page in the application will have the same basic layout around a different body. Instead of writing the entire HTML structure in each template, each template will *extend* a base template and override specific sections. .. code-block:: html+jinja :caption: ``flaskr/templates/base.html`` {% block title %}{% endblock %} - Flaskr
{% block header %}{% endblock %}
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %} {% block content %}{% endblock %}
:data:`g` is automatically available in templates. Based on if ``g.user`` is set (from ``load_logged_in_user``), either the username and a log out link are displayed, or links to register and log in are displayed. :func:`url_for` is also automatically available, and is used to generate URLs to views instead of writing them out manually. After the page title, and before the content, the template loops over each message returned by :func:`get_flashed_messages`. You used :func:`flash` in the views to show error messages, and this is the code that will display them. There are three blocks defined here that will be overridden in the other templates: #. ``{% block title %}`` will change the title displayed in the browser's tab and window title. #. ``{% block header %}`` is similar to ``title`` but will change the title displayed on the page. #. ``{% block content %}`` is where the content of each page goes, such as the login form or a blog post. The base template is directly in the ``templates`` directory. To keep the others organized, the templates for a blueprint will be placed in a directory with the same name as the blueprint. Register -------- .. code-block:: html+jinja :caption: ``flaskr/templates/auth/register.html`` {% extends 'base.html' %} {% block header %}

{% block title %}Register{% endblock %}

{% endblock %} {% block content %}
{% endblock %} ``{% extends 'base.html' %}`` tells Jinja that this template should replace the blocks from the base template. All the rendered content must appear inside ``{% block %}`` tags that override blocks from the base template. A useful pattern used here is to place ``{% block title %}`` inside ``{% block header %}``. This will set the title block and then output the value of it into the header block, so that both the window and page share the same title without writing it twice. The ``input`` tags are using the ``required`` attribute here. This tells the browser not to submit the form until those fields are filled in. If the user is using an older browser that doesn't support that attribute, or if they are using something besides a browser to make requests, you still want to validate the data in the Flask view. It's important to always fully validate the data on the server, even if the client does some validation as well. Log In ------ This is identical to the register template except for the title and submit button. .. code-block:: html+jinja :caption: ``flaskr/templates/auth/login.html`` {% extends 'base.html' %} {% block header %}

{% block title %}Log In{% endblock %}

{% endblock %} {% block content %}
{% endblock %} Register A User --------------- Now that the authentication templates are written, you can register a user. Make sure the server is still running (``flask run`` if it's not), then go to http://127.0.0.1:5000/auth/register. Try clicking the "Register" button without filling out the form and see that the browser shows an error message. Try removing the ``required`` attributes from the ``register.html`` template and click "Register" again. Instead of the browser showing an error, the page will reload and the error from :func:`flash` in the view will be shown. Fill out a username and password and you'll be redirected to the login page. Try entering an incorrect username, or the correct username and incorrect password. If you log in you'll get an error because there's no ``index`` view to redirect to yet. Continue to :doc:`static`. flask-3.1.1/docs/tutorial/tests.rst000066400000000000000000000435311501065614000173070ustar00rootroot00000000000000.. currentmodule:: flask Test Coverage ============= Writing unit tests for your application lets you check that the code you wrote works the way you expect. Flask provides a test client that simulates requests to the application and returns the response data. You should test as much of your code as possible. Code in functions only runs when the function is called, and code in branches, such as ``if`` blocks, only runs when the condition is met. You want to make sure that each function is tested with data that covers each branch. The closer you get to 100% coverage, the more comfortable you can be that making a change won't unexpectedly change other behavior. However, 100% coverage doesn't guarantee that your application doesn't have bugs. In particular, it doesn't test how the user interacts with the application in the browser. Despite this, test coverage is an important tool to use during development. .. note:: This is being introduced late in the tutorial, but in your future projects you should test as you develop. You'll use `pytest`_ and `coverage`_ to test and measure your code. Install them both: .. code-block:: none $ pip install pytest coverage .. _pytest: https://pytest.readthedocs.io/ .. _coverage: https://coverage.readthedocs.io/ Setup and Fixtures ------------------ The test code is located in the ``tests`` directory. This directory is *next to* the ``flaskr`` package, not inside it. The ``tests/conftest.py`` file contains setup functions called *fixtures* that each test will use. Tests are in Python modules that start with ``test_``, and each test function in those modules also starts with ``test_``. Each test will create a new temporary database file and populate some data that will be used in the tests. Write a SQL file to insert that data. .. code-block:: sql :caption: ``tests/data.sql`` INSERT INTO user (username, password) VALUES ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); INSERT INTO post (title, body, author_id, created) VALUES ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); The ``app`` fixture will call the factory and pass ``test_config`` to configure the application and database for testing instead of using your local development configuration. .. code-block:: python :caption: ``tests/conftest.py`` import os import tempfile import pytest from flaskr import create_app from flaskr.db import get_db, init_db with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: _data_sql = f.read().decode('utf8') @pytest.fixture def app(): db_fd, db_path = tempfile.mkstemp() app = create_app({ 'TESTING': True, 'DATABASE': db_path, }) with app.app_context(): init_db() get_db().executescript(_data_sql) yield app os.close(db_fd) os.unlink(db_path) @pytest.fixture def client(app): return app.test_client() @pytest.fixture def runner(app): return app.test_cli_runner() :func:`tempfile.mkstemp` creates and opens a temporary file, returning the file descriptor and the path to it. The ``DATABASE`` path is overridden so it points to this temporary path instead of the instance folder. After setting the path, the database tables are created and the test data is inserted. After the test is over, the temporary file is closed and removed. :data:`TESTING` tells Flask that the app is in test mode. Flask changes some internal behavior so it's easier to test, and other extensions can also use the flag to make testing them easier. The ``client`` fixture calls :meth:`app.test_client() ` with the application object created by the ``app`` fixture. Tests will use the client to make requests to the application without running the server. The ``runner`` fixture is similar to ``client``. :meth:`app.test_cli_runner() ` creates a runner that can call the Click commands registered with the application. Pytest uses fixtures by matching their function names with the names of arguments in the test functions. For example, the ``test_hello`` function you'll write next takes a ``client`` argument. Pytest matches that with the ``client`` fixture function, calls it, and passes the returned value to the test function. Factory ------- There's not much to test about the factory itself. Most of the code will be executed for each test already, so if something fails the other tests will notice. The only behavior that can change is passing test config. If config is not passed, there should be some default configuration, otherwise the configuration should be overridden. .. code-block:: python :caption: ``tests/test_factory.py`` from flaskr import create_app def test_config(): assert not create_app().testing assert create_app({'TESTING': True}).testing def test_hello(client): response = client.get('/hello') assert response.data == b'Hello, World!' You added the ``hello`` route as an example when writing the factory at the beginning of the tutorial. It returns "Hello, World!", so the test checks that the response data matches. Database -------- Within an application context, ``get_db`` should return the same connection each time it's called. After the context, the connection should be closed. .. code-block:: python :caption: ``tests/test_db.py`` import sqlite3 import pytest from flaskr.db import get_db def test_get_close_db(app): with app.app_context(): db = get_db() assert db is get_db() with pytest.raises(sqlite3.ProgrammingError) as e: db.execute('SELECT 1') assert 'closed' in str(e.value) The ``init-db`` command should call the ``init_db`` function and output a message. .. code-block:: python :caption: ``tests/test_db.py`` def test_init_db_command(runner, monkeypatch): class Recorder(object): called = False def fake_init_db(): Recorder.called = True monkeypatch.setattr('flaskr.db.init_db', fake_init_db) result = runner.invoke(args=['init-db']) assert 'Initialized' in result.output assert Recorder.called This test uses Pytest's ``monkeypatch`` fixture to replace the ``init_db`` function with one that records that it's been called. The ``runner`` fixture you wrote above is used to call the ``init-db`` command by name. Authentication -------------- For most of the views, a user needs to be logged in. The easiest way to do this in tests is to make a ``POST`` request to the ``login`` view with the client. Rather than writing that out every time, you can write a class with methods to do that, and use a fixture to pass it the client for each test. .. code-block:: python :caption: ``tests/conftest.py`` class AuthActions(object): def __init__(self, client): self._client = client def login(self, username='test', password='test'): return self._client.post( '/auth/login', data={'username': username, 'password': password} ) def logout(self): return self._client.get('/auth/logout') @pytest.fixture def auth(client): return AuthActions(client) With the ``auth`` fixture, you can call ``auth.login()`` in a test to log in as the ``test`` user, which was inserted as part of the test data in the ``app`` fixture. The ``register`` view should render successfully on ``GET``. On ``POST`` with valid form data, it should redirect to the login URL and the user's data should be in the database. Invalid data should display error messages. .. code-block:: python :caption: ``tests/test_auth.py`` import pytest from flask import g, session from flaskr.db import get_db def test_register(client, app): assert client.get('/auth/register').status_code == 200 response = client.post( '/auth/register', data={'username': 'a', 'password': 'a'} ) assert response.headers["Location"] == "/auth/login" with app.app_context(): assert get_db().execute( "SELECT * FROM user WHERE username = 'a'", ).fetchone() is not None @pytest.mark.parametrize(('username', 'password', 'message'), ( ('', '', b'Username is required.'), ('a', '', b'Password is required.'), ('test', 'test', b'already registered'), )) def test_register_validate_input(client, username, password, message): response = client.post( '/auth/register', data={'username': username, 'password': password} ) assert message in response.data :meth:`client.get() ` makes a ``GET`` request and returns the :class:`Response` object returned by Flask. Similarly, :meth:`client.post() ` makes a ``POST`` request, converting the ``data`` dict into form data. To test that the page renders successfully, a simple request is made and checked for a ``200 OK`` :attr:`~Response.status_code`. If rendering failed, Flask would return a ``500 Internal Server Error`` code. :attr:`~Response.headers` will have a ``Location`` header with the login URL when the register view redirects to the login view. :attr:`~Response.data` contains the body of the response as bytes. If you expect a certain value to render on the page, check that it's in ``data``. Bytes must be compared to bytes. If you want to compare text, use :meth:`get_data(as_text=True) ` instead. ``pytest.mark.parametrize`` tells Pytest to run the same test function with different arguments. You use it here to test different invalid input and error messages without writing the same code three times. The tests for the ``login`` view are very similar to those for ``register``. Rather than testing the data in the database, :data:`session` should have ``user_id`` set after logging in. .. code-block:: python :caption: ``tests/test_auth.py`` def test_login(client, auth): assert client.get('/auth/login').status_code == 200 response = auth.login() assert response.headers["Location"] == "/" with client: client.get('/') assert session['user_id'] == 1 assert g.user['username'] == 'test' @pytest.mark.parametrize(('username', 'password', 'message'), ( ('a', 'test', b'Incorrect username.'), ('test', 'a', b'Incorrect password.'), )) def test_login_validate_input(auth, username, password, message): response = auth.login(username, password) assert message in response.data Using ``client`` in a ``with`` block allows accessing context variables such as :data:`session` after the response is returned. Normally, accessing ``session`` outside of a request would raise an error. Testing ``logout`` is the opposite of ``login``. :data:`session` should not contain ``user_id`` after logging out. .. code-block:: python :caption: ``tests/test_auth.py`` def test_logout(client, auth): auth.login() with client: auth.logout() assert 'user_id' not in session Blog ---- All the blog views use the ``auth`` fixture you wrote earlier. Call ``auth.login()`` and subsequent requests from the client will be logged in as the ``test`` user. The ``index`` view should display information about the post that was added with the test data. When logged in as the author, there should be a link to edit the post. You can also test some more authentication behavior while testing the ``index`` view. When not logged in, each page shows links to log in or register. When logged in, there's a link to log out. .. code-block:: python :caption: ``tests/test_blog.py`` import pytest from flaskr.db import get_db def test_index(client, auth): response = client.get('/') assert b"Log In" in response.data assert b"Register" in response.data auth.login() response = client.get('/') assert b'Log Out' in response.data assert b'test title' in response.data assert b'by test on 2018-01-01' in response.data assert b'test\nbody' in response.data assert b'href="/1/update"' in response.data A user must be logged in to access the ``create``, ``update``, and ``delete`` views. The logged in user must be the author of the post to access ``update`` and ``delete``, otherwise a ``403 Forbidden`` status is returned. If a ``post`` with the given ``id`` doesn't exist, ``update`` and ``delete`` should return ``404 Not Found``. .. code-block:: python :caption: ``tests/test_blog.py`` @pytest.mark.parametrize('path', ( '/create', '/1/update', '/1/delete', )) def test_login_required(client, path): response = client.post(path) assert response.headers["Location"] == "/auth/login" def test_author_required(app, client, auth): # change the post author to another user with app.app_context(): db = get_db() db.execute('UPDATE post SET author_id = 2 WHERE id = 1') db.commit() auth.login() # current user can't modify other user's post assert client.post('/1/update').status_code == 403 assert client.post('/1/delete').status_code == 403 # current user doesn't see edit link assert b'href="/1/update"' not in client.get('/').data @pytest.mark.parametrize('path', ( '/2/update', '/2/delete', )) def test_exists_required(client, auth, path): auth.login() assert client.post(path).status_code == 404 The ``create`` and ``update`` views should render and return a ``200 OK`` status for a ``GET`` request. When valid data is sent in a ``POST`` request, ``create`` should insert the new post data into the database, and ``update`` should modify the existing data. Both pages should show an error message on invalid data. .. code-block:: python :caption: ``tests/test_blog.py`` def test_create(client, auth, app): auth.login() assert client.get('/create').status_code == 200 client.post('/create', data={'title': 'created', 'body': ''}) with app.app_context(): db = get_db() count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0] assert count == 2 def test_update(client, auth, app): auth.login() assert client.get('/1/update').status_code == 200 client.post('/1/update', data={'title': 'updated', 'body': ''}) with app.app_context(): db = get_db() post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() assert post['title'] == 'updated' @pytest.mark.parametrize('path', ( '/create', '/1/update', )) def test_create_update_validate(client, auth, path): auth.login() response = client.post(path, data={'title': '', 'body': ''}) assert b'Title is required.' in response.data The ``delete`` view should redirect to the index URL and the post should no longer exist in the database. .. code-block:: python :caption: ``tests/test_blog.py`` def test_delete(client, auth, app): auth.login() response = client.post('/1/delete') assert response.headers["Location"] == "/" with app.app_context(): db = get_db() post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() assert post is None Running the Tests ----------------- Some extra configuration, which is not required but makes running tests with coverage less verbose, can be added to the project's ``pyproject.toml`` file. .. code-block:: toml :caption: ``pyproject.toml`` [tool.pytest.ini_options] testpaths = ["tests"] [tool.coverage.run] branch = true source = ["flaskr"] To run the tests, use the ``pytest`` command. It will find and run all the test functions you've written. .. code-block:: none $ pytest ========================= test session starts ========================== platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 rootdir: /home/user/Projects/flask-tutorial collected 23 items tests/test_auth.py ........ [ 34%] tests/test_blog.py ............ [ 86%] tests/test_db.py .. [ 95%] tests/test_factory.py .. [100%] ====================== 24 passed in 0.64 seconds ======================= If any tests fail, pytest will show the error that was raised. You can run ``pytest -v`` to get a list of each test function rather than dots. To measure the code coverage of your tests, use the ``coverage`` command to run pytest instead of running it directly. .. code-block:: none $ coverage run -m pytest You can either view a simple coverage report in the terminal: .. code-block:: none $ coverage report Name Stmts Miss Branch BrPart Cover ------------------------------------------------------ flaskr/__init__.py 21 0 2 0 100% flaskr/auth.py 54 0 22 0 100% flaskr/blog.py 54 0 16 0 100% flaskr/db.py 24 0 4 0 100% ------------------------------------------------------ TOTAL 153 0 44 0 100% An HTML report allows you to see which lines were covered in each file: .. code-block:: none $ coverage html This generates files in the ``htmlcov`` directory. Open ``htmlcov/index.html`` in your browser to see the report. Continue to :doc:`deploy`. flask-3.1.1/docs/tutorial/views.rst000066400000000000000000000252501501065614000173000ustar00rootroot00000000000000.. currentmodule:: flask Blueprints and Views ==================== A view function is the code you write to respond to requests to your application. Flask uses patterns to match the incoming request URL to the view that should handle it. The view returns data that Flask turns into an outgoing response. Flask can also go the other direction and generate a URL to a view based on its name and arguments. Create a Blueprint ------------------ A :class:`Blueprint` is a way to organize a group of related views and other code. Rather than registering views and other code directly with an application, they are registered with a blueprint. Then the blueprint is registered with the application when it is available in the factory function. Flaskr will have two blueprints, one for authentication functions and one for the blog posts functions. The code for each blueprint will go in a separate module. Since the blog needs to know about authentication, you'll write the authentication one first. .. code-block:: python :caption: ``flaskr/auth.py`` import functools from flask import ( Blueprint, flash, g, redirect, render_template, request, session, url_for ) from werkzeug.security import check_password_hash, generate_password_hash from flaskr.db import get_db bp = Blueprint('auth', __name__, url_prefix='/auth') This creates a :class:`Blueprint` named ``'auth'``. Like the application object, the blueprint needs to know where it's defined, so ``__name__`` is passed as the second argument. The ``url_prefix`` will be prepended to all the URLs associated with the blueprint. Import and register the blueprint from the factory using :meth:`app.register_blueprint() `. Place the new code at the end of the factory function before returning the app. .. code-block:: python :caption: ``flaskr/__init__.py`` def create_app(): app = ... # existing code omitted from . import auth app.register_blueprint(auth.bp) return app The authentication blueprint will have views to register new users and to log in and log out. The First View: Register ------------------------ When the user visits the ``/auth/register`` URL, the ``register`` view will return `HTML`_ with a form for them to fill out. When they submit the form, it will validate their input and either show the form again with an error message or create the new user and go to the login page. .. _HTML: https://developer.mozilla.org/docs/Web/HTML For now you will just write the view code. On the next page, you'll write templates to generate the HTML form. .. code-block:: python :caption: ``flaskr/auth.py`` @bp.route('/register', methods=('GET', 'POST')) def register(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] db = get_db() error = None if not username: error = 'Username is required.' elif not password: error = 'Password is required.' if error is None: try: db.execute( "INSERT INTO user (username, password) VALUES (?, ?)", (username, generate_password_hash(password)), ) db.commit() except db.IntegrityError: error = f"User {username} is already registered." else: return redirect(url_for("auth.login")) flash(error) return render_template('auth/register.html') Here's what the ``register`` view function is doing: #. :meth:`@bp.route ` associates the URL ``/register`` with the ``register`` view function. When Flask receives a request to ``/auth/register``, it will call the ``register`` view and use the return value as the response. #. If the user submitted the form, :attr:`request.method ` will be ``'POST'``. In this case, start validating the input. #. :attr:`request.form ` is a special type of :class:`dict` mapping submitted form keys and values. The user will input their ``username`` and ``password``. #. Validate that ``username`` and ``password`` are not empty. #. If validation succeeds, insert the new user data into the database. - :meth:`db.execute ` takes a SQL query with ``?`` placeholders for any user input, and a tuple of values to replace the placeholders with. The database library will take care of escaping the values so you are not vulnerable to a *SQL injection attack*. - For security, passwords should never be stored in the database directly. Instead, :func:`~werkzeug.security.generate_password_hash` is used to securely hash the password, and that hash is stored. Since this query modifies data, :meth:`db.commit() ` needs to be called afterwards to save the changes. - An :exc:`sqlite3.IntegrityError` will occur if the username already exists, which should be shown to the user as another validation error. #. After storing the user, they are redirected to the login page. :func:`url_for` generates the URL for the login view based on its name. This is preferable to writing the URL directly as it allows you to change the URL later without changing all code that links to it. :func:`redirect` generates a redirect response to the generated URL. #. If validation fails, the error is shown to the user. :func:`flash` stores messages that can be retrieved when rendering the template. #. When the user initially navigates to ``auth/register``, or there was a validation error, an HTML page with the registration form should be shown. :func:`render_template` will render a template containing the HTML, which you'll write in the next step of the tutorial. Login ----- This view follows the same pattern as the ``register`` view above. .. code-block:: python :caption: ``flaskr/auth.py`` @bp.route('/login', methods=('GET', 'POST')) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] db = get_db() error = None user = db.execute( 'SELECT * FROM user WHERE username = ?', (username,) ).fetchone() if user is None: error = 'Incorrect username.' elif not check_password_hash(user['password'], password): error = 'Incorrect password.' if error is None: session.clear() session['user_id'] = user['id'] return redirect(url_for('index')) flash(error) return render_template('auth/login.html') There are a few differences from the ``register`` view: #. The user is queried first and stored in a variable for later use. :meth:`~sqlite3.Cursor.fetchone` returns one row from the query. If the query returned no results, it returns ``None``. Later, :meth:`~sqlite3.Cursor.fetchall` will be used, which returns a list of all results. #. :func:`~werkzeug.security.check_password_hash` hashes the submitted password in the same way as the stored hash and securely compares them. If they match, the password is valid. #. :data:`session` is a :class:`dict` that stores data across requests. When validation succeeds, the user's ``id`` is stored in a new session. The data is stored in a *cookie* that is sent to the browser, and the browser then sends it back with subsequent requests. Flask securely *signs* the data so that it can't be tampered with. Now that the user's ``id`` is stored in the :data:`session`, it will be available on subsequent requests. At the beginning of each request, if a user is logged in their information should be loaded and made available to other views. .. code-block:: python :caption: ``flaskr/auth.py`` @bp.before_app_request def load_logged_in_user(): user_id = session.get('user_id') if user_id is None: g.user = None else: g.user = get_db().execute( 'SELECT * FROM user WHERE id = ?', (user_id,) ).fetchone() :meth:`bp.before_app_request() ` registers a function that runs before the view function, no matter what URL is requested. ``load_logged_in_user`` checks if a user id is stored in the :data:`session` and gets that user's data from the database, storing it on :data:`g.user `, which lasts for the length of the request. If there is no user id, or if the id doesn't exist, ``g.user`` will be ``None``. Logout ------ To log out, you need to remove the user id from the :data:`session`. Then ``load_logged_in_user`` won't load a user on subsequent requests. .. code-block:: python :caption: ``flaskr/auth.py`` @bp.route('/logout') def logout(): session.clear() return redirect(url_for('index')) Require Authentication in Other Views ------------------------------------- Creating, editing, and deleting blog posts will require a user to be logged in. A *decorator* can be used to check this for each view it's applied to. .. code-block:: python :caption: ``flaskr/auth.py`` def login_required(view): @functools.wraps(view) def wrapped_view(**kwargs): if g.user is None: return redirect(url_for('auth.login')) return view(**kwargs) return wrapped_view This decorator returns a new view function that wraps the original view it's applied to. The new function checks if a user is loaded and redirects to the login page otherwise. If a user is loaded the original view is called and continues normally. You'll use this decorator when writing the blog views. Endpoints and URLs ------------------ The :func:`url_for` function generates the URL to a view based on a name and arguments. The name associated with a view is also called the *endpoint*, and by default it's the same as the name of the view function. For example, the ``hello()`` view that was added to the app factory earlier in the tutorial has the name ``'hello'`` and can be linked to with ``url_for('hello')``. If it took an argument, which you'll see later, it would be linked to using ``url_for('hello', who='World')``. When using a blueprint, the name of the blueprint is prepended to the name of the function, so the endpoint for the ``login`` function you wrote above is ``'auth.login'`` because you added it to the ``'auth'`` blueprint. Continue to :doc:`templates`. flask-3.1.1/docs/views.rst000066400000000000000000000240361501065614000154360ustar00rootroot00000000000000Class-based Views ================= .. currentmodule:: flask.views This page introduces using the :class:`View` and :class:`MethodView` classes to write class-based views. A class-based view is a class that acts as a view function. Because it is a class, different instances of the class can be created with different arguments, to change the behavior of the view. This is also known as generic, reusable, or pluggable views. An example of where this is useful is defining a class that creates an API based on the database model it is initialized with. For more complex API behavior and customization, look into the various API extensions for Flask. Basic Reusable View ------------------- Let's walk through an example converting a view function to a view class. We start with a view function that queries a list of users then renders a template to show the list. .. code-block:: python @app.route("/users/") def user_list(): users = User.query.all() return render_template("users.html", users=users) This works for the user model, but let's say you also had more models that needed list pages. You'd need to write another view function for each model, even though the only thing that would change is the model and template name. Instead, you can write a :class:`View` subclass that will query a model and render a template. As the first step, we'll convert the view to a class without any customization. .. code-block:: python from flask.views import View class UserList(View): def dispatch_request(self): users = User.query.all() return render_template("users.html", objects=users) app.add_url_rule("/users/", view_func=UserList.as_view("user_list")) The :meth:`View.dispatch_request` method is the equivalent of the view function. Calling :meth:`View.as_view` method will create a view function that can be registered on the app with its :meth:`~flask.Flask.add_url_rule` method. The first argument to ``as_view`` is the name to use to refer to the view with :func:`~flask.url_for`. .. note:: You can't decorate the class with ``@app.route()`` the way you'd do with a basic view function. Next, we need to be able to register the same view class for different models and templates, to make it more useful than the original function. The class will take two arguments, the model and template, and store them on ``self``. Then ``dispatch_request`` can reference these instead of hard-coded values. .. code-block:: python class ListView(View): def __init__(self, model, template): self.model = model self.template = template def dispatch_request(self): items = self.model.query.all() return render_template(self.template, items=items) Remember, we create the view function with ``View.as_view()`` instead of creating the class directly. Any extra arguments passed to ``as_view`` are then passed when creating the class. Now we can register the same view to handle multiple models. .. code-block:: python app.add_url_rule( "/users/", view_func=ListView.as_view("user_list", User, "users.html"), ) app.add_url_rule( "/stories/", view_func=ListView.as_view("story_list", Story, "stories.html"), ) URL Variables ------------- Any variables captured by the URL are passed as keyword arguments to the ``dispatch_request`` method, as they would be for a regular view function. .. code-block:: python class DetailView(View): def __init__(self, model): self.model = model self.template = f"{model.__name__.lower()}/detail.html" def dispatch_request(self, id) item = self.model.query.get_or_404(id) return render_template(self.template, item=item) app.add_url_rule( "/users/", view_func=DetailView.as_view("user_detail", User) ) View Lifetime and ``self`` -------------------------- By default, a new instance of the view class is created every time a request is handled. This means that it is safe to write other data to ``self`` during the request, since the next request will not see it, unlike other forms of global state. However, if your view class needs to do a lot of complex initialization, doing it for every request is unnecessary and can be inefficient. To avoid this, set :attr:`View.init_every_request` to ``False``, which will only create one instance of the class and use it for every request. In this case, writing to ``self`` is not safe. If you need to store data during the request, use :data:`~flask.g` instead. In the ``ListView`` example, nothing writes to ``self`` during the request, so it is more efficient to create a single instance. .. code-block:: python class ListView(View): init_every_request = False def __init__(self, model, template): self.model = model self.template = template def dispatch_request(self): items = self.model.query.all() return render_template(self.template, items=items) Different instances will still be created each for each ``as_view`` call, but not for each request to those views. View Decorators --------------- The view class itself is not the view function. View decorators need to be applied to the view function returned by ``as_view``, not the class itself. Set :attr:`View.decorators` to a list of decorators to apply. .. code-block:: python class UserList(View): decorators = [cache(minutes=2), login_required] app.add_url_rule('/users/', view_func=UserList.as_view()) If you didn't set ``decorators``, you could apply them manually instead. This is equivalent to: .. code-block:: python view = UserList.as_view("users_list") view = cache(minutes=2)(view) view = login_required(view) app.add_url_rule('/users/', view_func=view) Keep in mind that order matters. If you're used to ``@decorator`` style, this is equivalent to: .. code-block:: python @app.route("/users/") @login_required @cache(minutes=2) def user_list(): ... Method Hints ------------ A common pattern is to register a view with ``methods=["GET", "POST"]``, then check ``request.method == "POST"`` to decide what to do. Setting :attr:`View.methods` is equivalent to passing the list of methods to ``add_url_rule`` or ``route``. .. code-block:: python class MyView(View): methods = ["GET", "POST"] def dispatch_request(self): if request.method == "POST": ... ... app.add_url_rule('/my-view', view_func=MyView.as_view('my-view')) This is equivalent to the following, except further subclasses can inherit or change the methods. .. code-block:: python app.add_url_rule( "/my-view", view_func=MyView.as_view("my-view"), methods=["GET", "POST"], ) Method Dispatching and APIs --------------------------- For APIs it can be helpful to use a different function for each HTTP method. :class:`MethodView` extends the basic :class:`View` to dispatch to different methods of the class based on the request method. Each HTTP method maps to a method of the class with the same (lowercase) name. :class:`MethodView` automatically sets :attr:`View.methods` based on the methods defined by the class. It even knows how to handle subclasses that override or define other methods. We can make a generic ``ItemAPI`` class that provides get (detail), patch (edit), and delete methods for a given model. A ``GroupAPI`` can provide get (list) and post (create) methods. .. code-block:: python from flask.views import MethodView class ItemAPI(MethodView): init_every_request = False def __init__(self, model): self.model = model self.validator = generate_validator(model) def _get_item(self, id): return self.model.query.get_or_404(id) def get(self, id): item = self._get_item(id) return jsonify(item.to_json()) def patch(self, id): item = self._get_item(id) errors = self.validator.validate(item, request.json) if errors: return jsonify(errors), 400 item.update_from_json(request.json) db.session.commit() return jsonify(item.to_json()) def delete(self, id): item = self._get_item(id) db.session.delete(item) db.session.commit() return "", 204 class GroupAPI(MethodView): init_every_request = False def __init__(self, model): self.model = model self.validator = generate_validator(model, create=True) def get(self): items = self.model.query.all() return jsonify([item.to_json() for item in items]) def post(self): errors = self.validator.validate(request.json) if errors: return jsonify(errors), 400 db.session.add(self.model.from_json(request.json)) db.session.commit() return jsonify(item.to_json()) def register_api(app, model, name): item = ItemAPI.as_view(f"{name}-item", model) group = GroupAPI.as_view(f"{name}-group", model) app.add_url_rule(f"/{name}/", view_func=item) app.add_url_rule(f"/{name}/", view_func=group) register_api(app, User, "users") register_api(app, Story, "stories") This produces the following views, a standard REST API! ================= ========== =================== URL Method Description ----------------- ---------- ------------------- ``/users/`` ``GET`` List all users ``/users/`` ``POST`` Create a new user ``/users/`` ``GET`` Show a single user ``/users/`` ``PATCH`` Update a user ``/users/`` ``DELETE`` Delete a user ``/stories/`` ``GET`` List all stories ``/stories/`` ``POST`` Create a new story ``/stories/`` ``GET`` Show a single story ``/stories/`` ``PATCH`` Update a story ``/stories/`` ``DELETE`` Delete a story ================= ========== =================== flask-3.1.1/docs/web-security.rst000066400000000000000000000277361501065614000167350ustar00rootroot00000000000000Security Considerations ======================= Web applications face many types of potential security problems, and it can be hard to get everything right, or even to know what "right" is in general. Flask tries to solve a few of these things by default, but there are other parts you may have to take care of yourself. Many of these solutions are tradeoffs, and will depend on each application's specific needs and threat model. Many hosting platforms may take care of certain types of problems without the need for the Flask application to handle them. Resource Use ------------ A common category of attacks is "Denial of Service" (DoS or DDoS). This is a very broad category, and different variants target different layers in a deployed application. In general, something is done to increase how much processing time or memory is used to handle each request, to the point where there are not enough resources to handle legitimate requests. Flask provides a few configuration options to handle resource use. They can also be set on individual requests to customize only that request. The documentation for each goes into more detail. - :data:`MAX_CONTENT_LENGTH` or :attr:`.Request.max_content_length` controls how much data will be read from a request. It is not set by default, although it will still block truly unlimited streams unless the WSGI server indicates support. - :data:`MAX_FORM_MEMORY_SIZE` or :attr:`.Request.max_form_memory_size` controls how large any non-file ``multipart/form-data`` field can be. It is set to 500kB by default. - :data:`MAX_FORM_PARTS` or :attr:`.Request.max_form_parts` controls how many ``multipart/form-data`` fields can be parsed. It is set to 1000 by default. Combined with the default `max_form_memory_size`, this means that a form will occupy at most 500MB of memory. Regardless of these settings, you should also review what settings are available from your operating system, container deployment (Docker etc), WSGI server, HTTP server, and hosting platform. They typically have ways to set process resource limits, timeouts, and other checks regardless of how Flask is configured. .. _security-xss: Cross-Site Scripting (XSS) -------------------------- Cross site scripting is the concept of injecting arbitrary HTML (and with it JavaScript) into the context of a website. To remedy this, developers have to properly escape text so that it cannot include arbitrary HTML tags. For more information on that have a look at the Wikipedia article on `Cross-Site Scripting `_. Flask configures Jinja2 to automatically escape all values unless explicitly told otherwise. This should rule out all XSS problems caused in templates, but there are still other places where you have to be careful: - generating HTML without the help of Jinja2 - calling :class:`~markupsafe.Markup` on data submitted by users - sending out HTML from uploaded files, never do that, use the ``Content-Disposition: attachment`` header to prevent that problem. - sending out textfiles from uploaded files. Some browsers are using content-type guessing based on the first few bytes so users could trick a browser to execute HTML. Another thing that is very important are unquoted attributes. While Jinja2 can protect you from XSS issues by escaping HTML, there is one thing it cannot protect you from: XSS by attribute injection. To counter this possible attack vector, be sure to always quote your attributes with either double or single quotes when using Jinja expressions in them: .. sourcecode:: html+jinja Why is this necessary? Because if you would not be doing that, an attacker could easily inject custom JavaScript handlers. For example an attacker could inject this piece of HTML+JavaScript: .. sourcecode:: html onmouseover=alert(document.cookie) When the user would then move with the mouse over the input, the cookie would be presented to the user in an alert window. But instead of showing the cookie to the user, a good attacker might also execute any other JavaScript code. In combination with CSS injections the attacker might even make the element fill out the entire page so that the user would just have to have the mouse anywhere on the page to trigger the attack. There is one class of XSS issues that Jinja's escaping does not protect against. The ``a`` tag's ``href`` attribute can contain a `javascript:` URI, which the browser will execute when clicked if not secured properly. .. sourcecode:: html click here click here To prevent this, you'll need to set the :ref:`security-csp` response header. Cross-Site Request Forgery (CSRF) --------------------------------- Another big problem is CSRF. This is a very complex topic and I won't outline it here in detail just mention what it is and how to theoretically prevent it. If your authentication information is stored in cookies, you have implicit state management. The state of "being logged in" is controlled by a cookie, and that cookie is sent with each request to a page. Unfortunately that includes requests triggered by 3rd party sites. If you don't keep that in mind, some people might be able to trick your application's users with social engineering to do stupid things without them knowing. Say you have a specific URL that, when you sent ``POST`` requests to will delete a user's profile (say ``http://example.com/user/delete``). If an attacker now creates a page that sends a post request to that page with some JavaScript they just have to trick some users to load that page and their profiles will end up being deleted. Imagine you were to run Facebook with millions of concurrent users and someone would send out links to images of little kittens. When users would go to that page, their profiles would get deleted while they are looking at images of fluffy cats. How can you prevent that? Basically for each request that modifies content on the server you would have to either use a one-time token and store that in the cookie **and** also transmit it with the form data. After receiving the data on the server again, you would then have to compare the two tokens and ensure they are equal. Why does Flask not do that for you? The ideal place for this to happen is the form validation framework, which does not exist in Flask. .. _security-json: JSON Security ------------- In Flask 0.10 and lower, :func:`~flask.jsonify` did not serialize top-level arrays to JSON. This was because of a security vulnerability in ECMAScript 4. ECMAScript 5 closed this vulnerability, so only extremely old browsers are still vulnerable. All of these browsers have `other more serious vulnerabilities `_, so this behavior was changed and :func:`~flask.jsonify` now supports serializing arrays. Security Headers ---------------- Browsers recognize various response headers in order to control security. We recommend reviewing each of the headers below for use in your application. The `Flask-Talisman`_ extension can be used to manage HTTPS and the security headers for you. .. _Flask-Talisman: https://github.com/GoogleCloudPlatform/flask-talisman HTTP Strict Transport Security (HSTS) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tells the browser to convert all HTTP requests to HTTPS, preventing man-in-the-middle (MITM) attacks. :: response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security .. _security-csp: Content Security Policy (CSP) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tell the browser where it can load various types of resource from. This header should be used whenever possible, but requires some work to define the correct policy for your site. A very strict policy would be:: response.headers['Content-Security-Policy'] = "default-src 'self'" - https://csp.withgoogle.com/docs/index.html - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy X-Content-Type-Options ~~~~~~~~~~~~~~~~~~~~~~ Forces the browser to honor the response content type instead of trying to detect it, which can be abused to generate a cross-site scripting (XSS) attack. :: response.headers['X-Content-Type-Options'] = 'nosniff' - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options X-Frame-Options ~~~~~~~~~~~~~~~ Prevents external sites from embedding your site in an ``iframe``. This prevents a class of attacks where clicks in the outer frame can be translated invisibly to clicks on your page's elements. This is also known as "clickjacking". :: response.headers['X-Frame-Options'] = 'SAMEORIGIN' - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options .. _security-cookie: Set-Cookie options ~~~~~~~~~~~~~~~~~~ These options can be added to a ``Set-Cookie`` header to improve their security. Flask has configuration options to set these on the session cookie. They can be set on other cookies too. - ``Secure`` limits cookies to HTTPS traffic only. - ``HttpOnly`` protects the contents of cookies from being read with JavaScript. - ``SameSite`` restricts how cookies are sent with requests from external sites. Can be set to ``'Lax'`` (recommended) or ``'Strict'``. ``Lax`` prevents sending cookies with CSRF-prone requests from external sites, such as submitting a form. ``Strict`` prevents sending cookies with all external requests, including following regular links. :: app.config.update( SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE='Lax', ) response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Lax') Specifying ``Expires`` or ``Max-Age`` options, will remove the cookie after the given time, or the current time plus the age, respectively. If neither option is set, the cookie will be removed when the browser is closed. :: # cookie expires after 10 minutes response.set_cookie('snakes', '3', max_age=600) For the session cookie, if :attr:`session.permanent ` is set, then :data:`PERMANENT_SESSION_LIFETIME` is used to set the expiration. Flask's default cookie implementation validates that the cryptographic signature is not older than this value. Lowering this value may help mitigate replay attacks, where intercepted cookies can be sent at a later time. :: app.config.update( PERMANENT_SESSION_LIFETIME=600 ) @app.route('/login', methods=['POST']) def login(): ... session.clear() session['user_id'] = user.id session.permanent = True ... Use :class:`itsdangerous.TimedSerializer` to sign and validate other cookie values (or any values that need secure signatures). - https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie .. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute Copy/Paste to Terminal ---------------------- Hidden characters such as the backspace character (``\b``, ``^H``) can cause text to render differently in HTML than how it is interpreted if `pasted into a terminal `__. For example, ``import y\bose\bm\bi\bt\be\b`` renders as ``import yosemite`` in HTML, but the backspaces are applied when pasted into a terminal, and it becomes ``import os``. If you expect users to copy and paste untrusted code from your site, such as from comments posted by users on a technical blog, consider applying extra filtering, such as replacing all ``\b`` characters. .. code-block:: python body = body.replace("\b", "") Most modern terminals will warn about and remove hidden characters when pasting, so this isn't strictly necessary. It's also possible to craft dangerous commands in other ways that aren't possible to filter. Depending on your site's use case, it may be good to show a warning about copying code in general. flask-3.1.1/examples/000077500000000000000000000000001501065614000144305ustar00rootroot00000000000000flask-3.1.1/examples/celery/000077500000000000000000000000001501065614000157135ustar00rootroot00000000000000flask-3.1.1/examples/celery/README.md000066400000000000000000000016661501065614000172030ustar00rootroot00000000000000Background Tasks with Celery ============================ This example shows how to configure Celery with Flask, how to set up an API for submitting tasks and polling results, and how to use that API with JavaScript. See [Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/). From this directory, create a virtualenv and install the application into it. Then run a Celery worker. ```shell $ python3 -m venv .venv $ . ./.venv/bin/activate $ pip install -r requirements.txt && pip install -e . $ celery -A make_celery worker --loglevel INFO ``` In a separate terminal, activate the virtualenv and run the Flask development server. ```shell $ . ./.venv/bin/activate $ flask -A task_app run --debug ``` Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling requests in the browser dev tools and the Flask logs. You can see the tasks submitting and completing in the Celery logs. flask-3.1.1/examples/celery/make_celery.py000066400000000000000000000001461501065614000205460ustar00rootroot00000000000000from task_app import create_app flask_app = create_app() celery_app = flask_app.extensions["celery"] flask-3.1.1/examples/celery/pyproject.toml000066400000000000000000000005751501065614000206360ustar00rootroot00000000000000[project] name = "flask-example-celery" version = "1.0.0" description = "Example Flask application with Celery background tasks." readme = "README.md" classifiers = ["Private :: Do Not Upload"] dependencies = ["flask", "celery[redis]"] [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "task_app" [tool.ruff] src = ["src"] flask-3.1.1/examples/celery/requirements.txt000066400000000000000000000020611501065614000211760ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --resolver=backtracking pyproject.toml # amqp==5.1.1 # via kombu async-timeout==4.0.2 # via redis billiard==3.6.4.0 # via celery blinker==1.6.2 # via flask celery[redis]==5.2.7 # via flask-example-celery (pyproject.toml) click==8.1.3 # via # celery # click-didyoumean # click-plugins # click-repl # flask click-didyoumean==0.3.0 # via celery click-plugins==1.1.1 # via celery click-repl==0.2.0 # via celery flask==2.3.2 # via flask-example-celery (pyproject.toml) itsdangerous==2.1.2 # via flask jinja2==3.1.2 # via flask kombu==5.2.4 # via celery markupsafe==2.1.2 # via # jinja2 # werkzeug prompt-toolkit==3.0.38 # via click-repl pytz==2023.3 # via celery redis==4.5.4 # via celery six==1.16.0 # via click-repl vine==5.0.0 # via # amqp # celery # kombu wcwidth==0.2.6 # via prompt-toolkit werkzeug==2.3.3 # via flask flask-3.1.1/examples/celery/src/000077500000000000000000000000001501065614000165025ustar00rootroot00000000000000flask-3.1.1/examples/celery/src/task_app/000077500000000000000000000000001501065614000203045ustar00rootroot00000000000000flask-3.1.1/examples/celery/src/task_app/__init__.py000066400000000000000000000017731501065614000224250ustar00rootroot00000000000000from celery import Celery from celery import Task from flask import Flask from flask import render_template def create_app() -> Flask: app = Flask(__name__) app.config.from_mapping( CELERY=dict( broker_url="redis://localhost", result_backend="redis://localhost", task_ignore_result=True, ), ) app.config.from_prefixed_env() celery_init_app(app) @app.route("/") def index() -> str: return render_template("index.html") from . import views app.register_blueprint(views.bp) return app def celery_init_app(app: Flask) -> Celery: class FlaskTask(Task): def __call__(self, *args: object, **kwargs: object) -> object: with app.app_context(): return self.run(*args, **kwargs) celery_app = Celery(app.name, task_cls=FlaskTask) celery_app.config_from_object(app.config["CELERY"]) celery_app.set_default() app.extensions["celery"] = celery_app return celery_app flask-3.1.1/examples/celery/src/task_app/tasks.py000066400000000000000000000007441501065614000220100ustar00rootroot00000000000000import time from celery import shared_task from celery import Task @shared_task(ignore_result=False) def add(a: int, b: int) -> int: return a + b @shared_task() def block() -> None: time.sleep(5) @shared_task(bind=True, ignore_result=False) def process(self: Task, total: int) -> object: for i in range(total): self.update_state(state="PROGRESS", meta={"current": i + 1, "total": total}) time.sleep(1) return {"current": total, "total": total} flask-3.1.1/examples/celery/src/task_app/templates/000077500000000000000000000000001501065614000223025ustar00rootroot00000000000000flask-3.1.1/examples/celery/src/task_app/templates/index.html000066400000000000000000000055351501065614000243070ustar00rootroot00000000000000 Celery Example

Celery Example

Execute background tasks with Celery. Submits tasks and shows results using JavaScript.

Add

Start a task to add two numbers, then poll for the result.



Result:


Block

Start a task that takes 5 seconds. However, the response will return immediately.


Process

Start a task that counts, waiting one second each time, showing progress.


flask-3.1.1/examples/celery/src/task_app/views.py000066400000000000000000000016721501065614000220210ustar00rootroot00000000000000from celery.result import AsyncResult from flask import Blueprint from flask import request from . import tasks bp = Blueprint("tasks", __name__, url_prefix="/tasks") @bp.get("/result/") def result(id: str) -> dict[str, object]: result = AsyncResult(id) ready = result.ready() return { "ready": ready, "successful": result.successful() if ready else None, "value": result.get() if ready else result.result, } @bp.post("/add") def add() -> dict[str, object]: a = request.form.get("a", type=int) b = request.form.get("b", type=int) result = tasks.add.delay(a, b) return {"result_id": result.id} @bp.post("/block") def block() -> dict[str, object]: result = tasks.block.delay() return {"result_id": result.id} @bp.post("/process") def process() -> dict[str, object]: result = tasks.process.delay(total=request.form.get("total", type=int)) return {"result_id": result.id} flask-3.1.1/examples/javascript/000077500000000000000000000000001501065614000165765ustar00rootroot00000000000000flask-3.1.1/examples/javascript/.gitignore000066400000000000000000000001671501065614000205720ustar00rootroot00000000000000.venv/ *.pyc __pycache__/ instance/ .cache/ .pytest_cache/ .coverage htmlcov/ dist/ build/ *.egg-info/ .idea/ *.swp *~ flask-3.1.1/examples/javascript/LICENSE.txt000066400000000000000000000027031501065614000204230ustar00rootroot00000000000000Copyright 2010 Pallets Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. flask-3.1.1/examples/javascript/README.rst000066400000000000000000000020761501065614000202720ustar00rootroot00000000000000JavaScript Ajax Example ======================= Demonstrates how to post form data and process a JSON response using JavaScript. This allows making requests without navigating away from the page. Demonstrates using |fetch|_, |XMLHttpRequest|_, and |jQuery.ajax|_. See the `Flask docs`_ about JavaScript and Ajax. .. |fetch| replace:: ``fetch`` .. _fetch: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch .. |XMLHttpRequest| replace:: ``XMLHttpRequest`` .. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest .. |jQuery.ajax| replace:: ``jQuery.ajax`` .. _jQuery.ajax: https://api.jquery.com/jQuery.ajax/ .. _Flask docs: https://flask.palletsprojects.com/patterns/javascript/ Install ------- .. code-block:: text $ python3 -m venv .venv $ . .venv/bin/activate $ pip install -e . Run --- .. code-block:: text $ flask --app js_example run Open http://127.0.0.1:5000 in a browser. Test ---- .. code-block:: text $ pip install -e '.[test]' $ coverage run -m pytest $ coverage report flask-3.1.1/examples/javascript/js_example/000077500000000000000000000000001501065614000207255ustar00rootroot00000000000000flask-3.1.1/examples/javascript/js_example/__init__.py000066400000000000000000000001411501065614000230320ustar00rootroot00000000000000from flask import Flask app = Flask(__name__) from js_example import views # noqa: E402, F401 flask-3.1.1/examples/javascript/js_example/templates/000077500000000000000000000000001501065614000227235ustar00rootroot00000000000000flask-3.1.1/examples/javascript/js_example/templates/base.html000066400000000000000000000022321501065614000245220ustar00rootroot00000000000000 JavaScript Example

{% block intro %}{% endblock %}


+
= {% block script %}{% endblock %} flask-3.1.1/examples/javascript/js_example/templates/fetch.html000066400000000000000000000015241501065614000247040ustar00rootroot00000000000000{% extends 'base.html' %} {% block intro %} fetch is the modern plain JavaScript way to make requests. It's supported in all modern browsers. {% endblock %} {% block script %} {% endblock %} flask-3.1.1/examples/javascript/js_example/templates/jquery.html000066400000000000000000000012321501065614000251260ustar00rootroot00000000000000{% extends 'base.html' %} {% block intro %} jQuery is a popular library that adds cross browser APIs for common tasks. However, it requires loading an extra library. {% endblock %} {% block script %} {% endblock %} flask-3.1.1/examples/javascript/js_example/templates/xhr.html000066400000000000000000000016451501065614000244200ustar00rootroot00000000000000{% extends 'base.html' %} {% block intro %} XMLHttpRequest is the original JavaScript way to make requests. It's natively supported by all browsers, but has been superseded by fetch. {% endblock %} {% block script %} {% endblock %} flask-3.1.1/examples/javascript/js_example/views.py000066400000000000000000000006551501065614000224420ustar00rootroot00000000000000from flask import jsonify from flask import render_template from flask import request from . import app @app.route("/", defaults={"js": "fetch"}) @app.route("/") def index(js): return render_template(f"{js}.html", js=js) @app.route("/add", methods=["POST"]) def add(): a = request.form.get("a", 0, type=float) b = request.form.get("b", 0, type=float) return jsonify(result=a + b) flask-3.1.1/examples/javascript/pyproject.toml000066400000000000000000000013401501065614000215100ustar00rootroot00000000000000[project] name = "js_example" version = "1.1.0" description = "Demonstrates making AJAX requests to Flask." readme = "README.rst" license = {file = "LICENSE.txt"} maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] classifiers = ["Private :: Do Not Upload"] dependencies = ["flask"] [project.urls] Documentation = "https://flask.palletsprojects.com/patterns/javascript/" [project.optional-dependencies] test = ["pytest"] [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "js_example" [tool.pytest.ini_options] testpaths = ["tests"] filterwarnings = ["error"] [tool.coverage.run] branch = true source = ["js_example", "tests"] [tool.ruff] src = ["src"] flask-3.1.1/examples/javascript/tests/000077500000000000000000000000001501065614000177405ustar00rootroot00000000000000flask-3.1.1/examples/javascript/tests/conftest.py000066400000000000000000000003301501065614000221330ustar00rootroot00000000000000import pytest from js_example import app @pytest.fixture(name="app") def fixture_app(): app.testing = True yield app app.testing = False @pytest.fixture def client(app): return app.test_client() flask-3.1.1/examples/javascript/tests/test_js_example.py000066400000000000000000000013271501065614000235030ustar00rootroot00000000000000import pytest from flask import template_rendered @pytest.mark.parametrize( ("path", "template_name"), ( ("/", "fetch.html"), ("/plain", "xhr.html"), ("/fetch", "fetch.html"), ("/jquery", "jquery.html"), ), ) def test_index(app, client, path, template_name): def check(sender, template, context): assert template.name == template_name with template_rendered.connected_to(check, app): client.get(path) @pytest.mark.parametrize( ("a", "b", "result"), ((2, 3, 5), (2.5, 3, 5.5), (2, None, 2), (2, "b", 2)) ) def test_add(client, a, b, result): response = client.post("/add", data={"a": a, "b": b}) assert response.get_json()["result"] == result flask-3.1.1/examples/tutorial/000077500000000000000000000000001501065614000162735ustar00rootroot00000000000000flask-3.1.1/examples/tutorial/.gitignore000066400000000000000000000001671501065614000202670ustar00rootroot00000000000000.venv/ *.pyc __pycache__/ instance/ .cache/ .pytest_cache/ .coverage htmlcov/ dist/ build/ *.egg-info/ .idea/ *.swp *~ flask-3.1.1/examples/tutorial/LICENSE.txt000066400000000000000000000027031501065614000201200ustar00rootroot00000000000000Copyright 2010 Pallets Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. flask-3.1.1/examples/tutorial/README.rst000066400000000000000000000023711501065614000177650ustar00rootroot00000000000000Flaskr ====== The basic blog app built in the Flask `tutorial`_. .. _tutorial: https://flask.palletsprojects.com/tutorial/ Install ------- **Be sure to use the same version of the code as the version of the docs you're reading.** You probably want the latest tagged version, but the default Git version is the main branch. :: # clone the repository $ git clone https://github.com/pallets/flask $ cd flask # checkout the correct version $ git tag # shows the tagged versions $ git checkout latest-tag-found-above $ cd examples/tutorial Create a virtualenv and activate it:: $ python3 -m venv .venv $ . .venv/bin/activate Or on Windows cmd:: $ py -3 -m venv .venv $ .venv\Scripts\activate.bat Install Flaskr:: $ pip install -e . Or if you are using the main branch, install Flask from source before installing Flaskr:: $ pip install -e ../.. $ pip install -e . Run --- .. code-block:: text $ flask --app flaskr init-db $ flask --app flaskr run --debug Open http://127.0.0.1:5000 in a browser. Test ---- :: $ pip install '.[test]' $ pytest Run with coverage report:: $ coverage run -m pytest $ coverage report $ coverage html # open htmlcov/index.html in a browser flask-3.1.1/examples/tutorial/flaskr/000077500000000000000000000000001501065614000175555ustar00rootroot00000000000000flask-3.1.1/examples/tutorial/flaskr/__init__.py000066400000000000000000000026351501065614000216740ustar00rootroot00000000000000import os from flask import Flask def create_app(test_config=None): """Create and configure an instance of the Flask application.""" app = Flask(__name__, instance_relative_config=True) app.config.from_mapping( # a default secret that should be overridden by instance config SECRET_KEY="dev", # store the database in the instance folder DATABASE=os.path.join(app.instance_path, "flaskr.sqlite"), ) if test_config is None: # load the instance config, if it exists, when not testing app.config.from_pyfile("config.py", silent=True) else: # load the test config if passed in app.config.update(test_config) # ensure the instance folder exists try: os.makedirs(app.instance_path) except OSError: pass @app.route("/hello") def hello(): return "Hello, World!" # register the database commands from . import db db.init_app(app) # apply the blueprints to the app from . import auth from . import blog app.register_blueprint(auth.bp) app.register_blueprint(blog.bp) # make url_for('index') == url_for('blog.index') # in another app, you might define a separate main index here with # app.route, while giving the blog blueprint a url_prefix, but for # the tutorial the blog will be the main index app.add_url_rule("/", endpoint="index") return app flask-3.1.1/examples/tutorial/flaskr/auth.py000066400000000000000000000063401501065614000210730ustar00rootroot00000000000000import functools from flask import Blueprint from flask import flash from flask import g from flask import redirect from flask import render_template from flask import request from flask import session from flask import url_for from werkzeug.security import check_password_hash from werkzeug.security import generate_password_hash from .db import get_db bp = Blueprint("auth", __name__, url_prefix="/auth") def login_required(view): """View decorator that redirects anonymous users to the login page.""" @functools.wraps(view) def wrapped_view(**kwargs): if g.user is None: return redirect(url_for("auth.login")) return view(**kwargs) return wrapped_view @bp.before_app_request def load_logged_in_user(): """If a user id is stored in the session, load the user object from the database into ``g.user``.""" user_id = session.get("user_id") if user_id is None: g.user = None else: g.user = ( get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone() ) @bp.route("/register", methods=("GET", "POST")) def register(): """Register a new user. Validates that the username is not already taken. Hashes the password for security. """ if request.method == "POST": username = request.form["username"] password = request.form["password"] db = get_db() error = None if not username: error = "Username is required." elif not password: error = "Password is required." if error is None: try: db.execute( "INSERT INTO user (username, password) VALUES (?, ?)", (username, generate_password_hash(password)), ) db.commit() except db.IntegrityError: # The username was already taken, which caused the # commit to fail. Show a validation error. error = f"User {username} is already registered." else: # Success, go to the login page. return redirect(url_for("auth.login")) flash(error) return render_template("auth/register.html") @bp.route("/login", methods=("GET", "POST")) def login(): """Log in a registered user by adding the user id to the session.""" if request.method == "POST": username = request.form["username"] password = request.form["password"] db = get_db() error = None user = db.execute( "SELECT * FROM user WHERE username = ?", (username,) ).fetchone() if user is None: error = "Incorrect username." elif not check_password_hash(user["password"], password): error = "Incorrect password." if error is None: # store the user id in a new session and return to the index session.clear() session["user_id"] = user["id"] return redirect(url_for("index")) flash(error) return render_template("auth/login.html") @bp.route("/logout") def logout(): """Clear the current session, including the stored user id.""" session.clear() return redirect(url_for("index")) flask-3.1.1/examples/tutorial/flaskr/blog.py000066400000000000000000000063511501065614000210570ustar00rootroot00000000000000from flask import Blueprint from flask import flash from flask import g from flask import redirect from flask import render_template from flask import request from flask import url_for from werkzeug.exceptions import abort from .auth import login_required from .db import get_db bp = Blueprint("blog", __name__) @bp.route("/") def index(): """Show all the posts, most recent first.""" db = get_db() posts = db.execute( "SELECT p.id, title, body, created, author_id, username" " FROM post p JOIN user u ON p.author_id = u.id" " ORDER BY created DESC" ).fetchall() return render_template("blog/index.html", posts=posts) def get_post(id, check_author=True): """Get a post and its author by id. Checks that the id exists and optionally that the current user is the author. :param id: id of post to get :param check_author: require the current user to be the author :return: the post with author information :raise 404: if a post with the given id doesn't exist :raise 403: if the current user isn't the author """ post = ( get_db() .execute( "SELECT p.id, title, body, created, author_id, username" " FROM post p JOIN user u ON p.author_id = u.id" " WHERE p.id = ?", (id,), ) .fetchone() ) if post is None: abort(404, f"Post id {id} doesn't exist.") if check_author and post["author_id"] != g.user["id"]: abort(403) return post @bp.route("/create", methods=("GET", "POST")) @login_required def create(): """Create a new post for the current user.""" if request.method == "POST": title = request.form["title"] body = request.form["body"] error = None if not title: error = "Title is required." if error is not None: flash(error) else: db = get_db() db.execute( "INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)", (title, body, g.user["id"]), ) db.commit() return redirect(url_for("blog.index")) return render_template("blog/create.html") @bp.route("//update", methods=("GET", "POST")) @login_required def update(id): """Update a post if the current user is the author.""" post = get_post(id) if request.method == "POST": title = request.form["title"] body = request.form["body"] error = None if not title: error = "Title is required." if error is not None: flash(error) else: db = get_db() db.execute( "UPDATE post SET title = ?, body = ? WHERE id = ?", (title, body, id) ) db.commit() return redirect(url_for("blog.index")) return render_template("blog/update.html", post=post) @bp.route("//delete", methods=("POST",)) @login_required def delete(id): """Delete a post. Ensures that the post exists and that the logged in user is the author of the post. """ get_post(id) db = get_db() db.execute("DELETE FROM post WHERE id = ?", (id,)) db.commit() return redirect(url_for("blog.index")) flask-3.1.1/examples/tutorial/flaskr/db.py000066400000000000000000000024451501065614000205210ustar00rootroot00000000000000import sqlite3 from datetime import datetime import click from flask import current_app from flask import g def get_db(): """Connect to the application's configured database. The connection is unique for each request and will be reused if this is called again. """ if "db" not in g: g.db = sqlite3.connect( current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES ) g.db.row_factory = sqlite3.Row return g.db def close_db(e=None): """If this request connected to the database, close the connection. """ db = g.pop("db", None) if db is not None: db.close() def init_db(): """Clear existing data and create new tables.""" db = get_db() with current_app.open_resource("schema.sql") as f: db.executescript(f.read().decode("utf8")) @click.command("init-db") def init_db_command(): """Clear existing data and create new tables.""" init_db() click.echo("Initialized the database.") sqlite3.register_converter("timestamp", lambda v: datetime.fromisoformat(v.decode())) def init_app(app): """Register database functions with the Flask app. This is called by the application factory. """ app.teardown_appcontext(close_db) app.cli.add_command(init_db_command) flask-3.1.1/examples/tutorial/flaskr/schema.sql000066400000000000000000000007621501065614000215430ustar00rootroot00000000000000-- Initialize the database. -- Drop any existing data and create empty tables. DROP TABLE IF EXISTS user; DROP TABLE IF EXISTS post; CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL ); CREATE TABLE post ( id INTEGER PRIMARY KEY AUTOINCREMENT, author_id INTEGER NOT NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title TEXT NOT NULL, body TEXT NOT NULL, FOREIGN KEY (author_id) REFERENCES user (id) ); flask-3.1.1/examples/tutorial/flaskr/static/000077500000000000000000000000001501065614000210445ustar00rootroot00000000000000flask-3.1.1/examples/tutorial/flaskr/static/style.css000066400000000000000000000032401501065614000227150ustar00rootroot00000000000000html { font-family: sans-serif; background: #eee; padding: 1rem; } body { max-width: 960px; margin: 0 auto; background: white; } h1, h2, h3, h4, h5, h6 { font-family: serif; color: #377ba8; margin: 1rem 0; } a { color: #377ba8; } hr { border: none; border-top: 1px solid lightgray; } nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } nav h1 { flex: auto; margin: 0; } nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } nav ul { display: flex; list-style: none; margin: 0; padding: 0; } nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } .content { padding: 0 1rem 1rem; } .content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } .content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } .flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } .post > header { display: flex; align-items: flex-end; font-size: 0.85em; } .post > header > div:first-of-type { flex: auto; } .post > header h1 { font-size: 1.5em; margin-bottom: 0; } .post .about { color: slategray; font-style: italic; } .post .body { white-space: pre-line; } .content:last-child { margin-bottom: 0; } .content form { margin: 1em 0; display: flex; flex-direction: column; } .content label { font-weight: bold; margin-bottom: 0.5em; } .content input, .content textarea { margin-bottom: 1em; } .content textarea { min-height: 12em; resize: vertical; } input.danger { color: #cc2f2e; } input[type=submit] { align-self: start; min-width: 10em; } flask-3.1.1/examples/tutorial/flaskr/templates/000077500000000000000000000000001501065614000215535ustar00rootroot00000000000000flask-3.1.1/examples/tutorial/flaskr/templates/auth/000077500000000000000000000000001501065614000225145ustar00rootroot00000000000000flask-3.1.1/examples/tutorial/flaskr/templates/auth/login.html000066400000000000000000000006501501065614000245130ustar00rootroot00000000000000{% extends 'base.html' %} {% block header %}

{% block title %}Log In{% endblock %}

{% endblock %} {% block content %}
{% endblock %} flask-3.1.1/examples/tutorial/flaskr/templates/auth/register.html000066400000000000000000000006541501065614000252330ustar00rootroot00000000000000{% extends 'base.html' %} {% block header %}

{% block title %}Register{% endblock %}

{% endblock %} {% block content %}
{% endblock %} flask-3.1.1/examples/tutorial/flaskr/templates/base.html000066400000000000000000000013601501065614000233530ustar00rootroot00000000000000 {% block title %}{% endblock %} - Flaskr
{% block header %}{% endblock %}
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %} {% block content %}{% endblock %}
flask-3.1.1/examples/tutorial/flaskr/templates/blog/000077500000000000000000000000001501065614000224765ustar00rootroot00000000000000flask-3.1.1/examples/tutorial/flaskr/templates/blog/create.html000066400000000000000000000006771501065614000246410ustar00rootroot00000000000000{% extends 'base.html' %} {% block header %}

{% block title %}New Post{% endblock %}

{% endblock %} {% block content %}
{% endblock %} flask-3.1.1/examples/tutorial/flaskr/templates/blog/index.html000066400000000000000000000014261501065614000244760ustar00rootroot00000000000000{% extends 'base.html' %} {% block header %}

{% block title %}Posts{% endblock %}

{% if g.user %} New {% endif %} {% endblock %} {% block content %} {% for post in posts %}

{{ post['title'] }}

by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
{% if g.user['id'] == post['author_id'] %} Edit {% endif %}

{{ post['body'] }}

{% if not loop.last %}
{% endif %} {% endfor %} {% endblock %} flask-3.1.1/examples/tutorial/flaskr/templates/blog/update.html000066400000000000000000000012621501065614000246470ustar00rootroot00000000000000{% extends 'base.html' %} {% block header %}

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

{% endblock %} {% block content %}

{% endblock %} flask-3.1.1/examples/tutorial/pyproject.toml000066400000000000000000000014031501065614000212050ustar00rootroot00000000000000[project] name = "flaskr" version = "1.0.0" description = "The basic blog app built in the Flask tutorial." readme = "README.rst" license = {file = "LICENSE.txt"} maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] classifiers = ["Private :: Do Not Upload"] dependencies = [ "flask", ] [project.urls] Documentation = "https://flask.palletsprojects.com/tutorial/" [project.optional-dependencies] test = ["pytest"] [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "flaskr" [tool.flit.sdist] include = [ "tests/", ] [tool.pytest.ini_options] testpaths = ["tests"] filterwarnings = ["error"] [tool.coverage.run] branch = true source = ["flaskr", "tests"] [tool.ruff] src = ["src"] flask-3.1.1/examples/tutorial/tests/000077500000000000000000000000001501065614000174355ustar00rootroot00000000000000flask-3.1.1/examples/tutorial/tests/conftest.py000066400000000000000000000026561501065614000216450ustar00rootroot00000000000000import os import tempfile import pytest from flaskr import create_app from flaskr.db import get_db from flaskr.db import init_db # read in SQL for populating test data with open(os.path.join(os.path.dirname(__file__), "data.sql"), "rb") as f: _data_sql = f.read().decode("utf8") @pytest.fixture def app(): """Create and configure a new app instance for each test.""" # create a temporary file to isolate the database for each test db_fd, db_path = tempfile.mkstemp() # create the app with common test config app = create_app({"TESTING": True, "DATABASE": db_path}) # create the database and load test data with app.app_context(): init_db() get_db().executescript(_data_sql) yield app # close and remove the temporary database os.close(db_fd) os.unlink(db_path) @pytest.fixture def client(app): """A test client for the app.""" return app.test_client() @pytest.fixture def runner(app): """A test runner for the app's Click commands.""" return app.test_cli_runner() class AuthActions: def __init__(self, client): self._client = client def login(self, username="test", password="test"): return self._client.post( "/auth/login", data={"username": username, "password": password} ) def logout(self): return self._client.get("/auth/logout") @pytest.fixture def auth(client): return AuthActions(client) flask-3.1.1/examples/tutorial/tests/data.sql000066400000000000000000000006121501065614000210660ustar00rootroot00000000000000INSERT INTO user (username, password) VALUES ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); INSERT INTO post (title, body, author_id, created) VALUES ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); flask-3.1.1/examples/tutorial/tests/test_auth.py000066400000000000000000000040261501065614000220110ustar00rootroot00000000000000import pytest from flask import g from flask import session from flaskr.db import get_db def test_register(client, app): # test that viewing the page renders without template errors assert client.get("/auth/register").status_code == 200 # test that successful registration redirects to the login page response = client.post("/auth/register", data={"username": "a", "password": "a"}) assert response.headers["Location"] == "/auth/login" # test that the user was inserted into the database with app.app_context(): assert ( get_db().execute("SELECT * FROM user WHERE username = 'a'").fetchone() is not None ) @pytest.mark.parametrize( ("username", "password", "message"), ( ("", "", b"Username is required."), ("a", "", b"Password is required."), ("test", "test", b"already registered"), ), ) def test_register_validate_input(client, username, password, message): response = client.post( "/auth/register", data={"username": username, "password": password} ) assert message in response.data def test_login(client, auth): # test that viewing the page renders without template errors assert client.get("/auth/login").status_code == 200 # test that successful login redirects to the index page response = auth.login() assert response.headers["Location"] == "/" # login request set the user_id in the session # check that the user is loaded from the session with client: client.get("/") assert session["user_id"] == 1 assert g.user["username"] == "test" @pytest.mark.parametrize( ("username", "password", "message"), (("a", "test", b"Incorrect username."), ("test", "a", b"Incorrect password.")), ) def test_login_validate_input(auth, username, password, message): response = auth.login(username, password) assert message in response.data def test_logout(client, auth): auth.login() with client: auth.logout() assert "user_id" not in session flask-3.1.1/examples/tutorial/tests/test_blog.py000066400000000000000000000047071501065614000220010ustar00rootroot00000000000000import pytest from flaskr.db import get_db def test_index(client, auth): response = client.get("/") assert b"Log In" in response.data assert b"Register" in response.data auth.login() response = client.get("/") assert b"test title" in response.data assert b"by test on 2018-01-01" in response.data assert b"test\nbody" in response.data assert b'href="/1/update"' in response.data @pytest.mark.parametrize("path", ("/create", "/1/update", "/1/delete")) def test_login_required(client, path): response = client.post(path) assert response.headers["Location"] == "/auth/login" def test_author_required(app, client, auth): # change the post author to another user with app.app_context(): db = get_db() db.execute("UPDATE post SET author_id = 2 WHERE id = 1") db.commit() auth.login() # current user can't modify other user's post assert client.post("/1/update").status_code == 403 assert client.post("/1/delete").status_code == 403 # current user doesn't see edit link assert b'href="/1/update"' not in client.get("/").data @pytest.mark.parametrize("path", ("/2/update", "/2/delete")) def test_exists_required(client, auth, path): auth.login() assert client.post(path).status_code == 404 def test_create(client, auth, app): auth.login() assert client.get("/create").status_code == 200 client.post("/create", data={"title": "created", "body": ""}) with app.app_context(): db = get_db() count = db.execute("SELECT COUNT(id) FROM post").fetchone()[0] assert count == 2 def test_update(client, auth, app): auth.login() assert client.get("/1/update").status_code == 200 client.post("/1/update", data={"title": "updated", "body": ""}) with app.app_context(): db = get_db() post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() assert post["title"] == "updated" @pytest.mark.parametrize("path", ("/create", "/1/update")) def test_create_update_validate(client, auth, path): auth.login() response = client.post(path, data={"title": "", "body": ""}) assert b"Title is required." in response.data def test_delete(client, auth, app): auth.login() response = client.post("/1/delete") assert response.headers["Location"] == "/" with app.app_context(): db = get_db() post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() assert post is None flask-3.1.1/examples/tutorial/tests/test_db.py000066400000000000000000000011501501065614000214300ustar00rootroot00000000000000import sqlite3 import pytest from flaskr.db import get_db def test_get_close_db(app): with app.app_context(): db = get_db() assert db is get_db() with pytest.raises(sqlite3.ProgrammingError) as e: db.execute("SELECT 1") assert "closed" in str(e.value) def test_init_db_command(runner, monkeypatch): class Recorder: called = False def fake_init_db(): Recorder.called = True monkeypatch.setattr("flaskr.db.init_db", fake_init_db) result = runner.invoke(args=["init-db"]) assert "Initialized" in result.output assert Recorder.called flask-3.1.1/examples/tutorial/tests/test_factory.py000066400000000000000000000004521501065614000225160ustar00rootroot00000000000000from flaskr import create_app def test_config(): """Test create_app without passing test config.""" assert not create_app().testing assert create_app({"TESTING": True}).testing def test_hello(client): response = client.get("/hello") assert response.data == b"Hello, World!" flask-3.1.1/pyproject.toml000066400000000000000000000147641501065614000155420ustar00rootroot00000000000000[project] name = "Flask" version = "3.1.1" description = "A simple framework for building complex web applications." readme = "README.md" license = "BSD-3-Clause" license-files = ["LICENSE.txt"] maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", "Typing :: Typed", ] requires-python = ">=3.9" dependencies = [ "blinker>=1.9.0", "click>=8.1.3", "importlib-metadata>=3.6.0; python_version < '3.10'", "itsdangerous>=2.2.0", "jinja2>=3.1.2", "markupsafe>=2.1.1", "werkzeug>=3.1.0", ] [project.optional-dependencies] async = ["asgiref>=3.2"] dotenv = ["python-dotenv"] [dependency-groups] dev = [ "ruff", "tox", "tox-uv", ] docs = [ "pallets-sphinx-themes", "sphinx", "sphinx-tabs", "sphinxcontrib-log-cabinet", ] docs-auto = [ "sphinx-autobuild", ] gha-update = [ "gha-update ; python_full_version >= '3.12'", ] pre-commit = [ "pre-commit", "pre-commit-uv", ] tests = [ "asgiref", "greenlet ; python_version < '3.11'", "pytest", "python-dotenv", ] typing = [ "asgiref", "cryptography", "mypy", "pyright", "pytest", "python-dotenv", "types-contextvars", "types-dataclasses", ] [project.urls] Donate = "https://palletsprojects.com/donate" Documentation = "https://flask.palletsprojects.com/" Changes = "https://flask.palletsprojects.com/page/changes/" Source = "https://github.com/pallets/flask/" Chat = "https://discord.gg/pallets" [project.scripts] flask = "flask.cli:main" [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "flask" [tool.flit.sdist] include = [ "docs/", "examples/", "tests/", "CHANGES.rst", "uv.lock" ] exclude = [ "docs/_build/", ] [tool.uv] default-groups = ["dev", "pre-commit", "tests", "typing"] [tool.pytest.ini_options] testpaths = ["tests"] filterwarnings = [ "error", ] [tool.coverage.run] branch = true source = ["flask", "tests"] [tool.coverage.paths] source = ["src", "*/site-packages"] [tool.coverage.report] exclude_also = [ "if t.TYPE_CHECKING", "raise NotImplementedError", ": \\.{3}", ] [tool.mypy] python_version = "3.9" files = ["src", "tests/type_check"] show_error_codes = true pretty = true strict = true [[tool.mypy.overrides]] module = [ "asgiref.*", "dotenv.*", "cryptography.*", "importlib_metadata", ] ignore_missing_imports = true [tool.pyright] pythonVersion = "3.9" include = ["src", "tests/type_check"] typeCheckingMode = "basic" [tool.ruff] src = ["src"] fix = true show-fixes = true output-format = "full" [tool.ruff.lint] select = [ "B", # flake8-bugbear "E", # pycodestyle error "F", # pyflakes "I", # isort "UP", # pyupgrade "W", # pycodestyle warning ] [tool.ruff.lint.isort] force-single-line = true order-by-type = false [tool.gha-update] tag-only = [ "slsa-framework/slsa-github-generator", ] [tool.tox] env_list = [ "py3.13", "py3.12", "py3.11", "py3.10", "py3.9", "pypy3.11", "tests-min", "tests-dev", "style", "typing", "docs", ] [tool.tox.env_run_base] description = "pytest on latest dependency versions" runner = "uv-venv-lock-runner" package = "wheel" wheel_build_env = ".pkg" constrain_package_deps = true use_frozen_constraints = true dependency_groups = ["tests"] env_tmp_dir = "{toxworkdir}/tmp/{envname}" commands = [[ "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", {replace = "posargs", default = [], extend = true}, ]] [tool.tox.env.tests-min] description = "pytest on minimum dependency versions" base_python = ["3.13"] commands = [ [ "uv", "pip", "install", "blinker==1.9.0", "click==8.1.3", "itsdangerous==2.2.0", "jinja2==3.1.2", "markupsafe==2.1.1", "werkzeug==3.1.0", ], [ "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", {replace = "posargs", default = [], extend = true}, ], ] [tool.tox.env.tests-dev] description = "pytest on development dependency versions (git main branch)" base_python = ["3.10"] commands = [ [ "uv", "pip", "install", "https://github.com/pallets-eco/blinker/archive/refs/heads/main.tar.gz", "https://github.com/pallets/click/archive/refs/heads/main.tar.gz", "https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz", "https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz", "https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz", "https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz", ], [ "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", {replace = "posargs", default = [], extend = true}, ], ] [tool.tox.env.style] description = "run all pre-commit hooks on all files" dependency_groups = ["pre-commit"] skip_install = true commands = [["pre-commit", "run", "--all-files"]] [tool.tox.env.typing] description = "run static type checkers" dependency_groups = ["typing"] commands = [ ["mypy"], ["pyright"], ] [tool.tox.env.docs] description = "build docs" dependency_groups = ["docs"] commands = [["sphinx-build", "-E", "-W", "-b", "dirhtml", "docs", "docs/_build/dirhtml"]] [tool.tox.env.docs-auto] description = "continuously rebuild docs and start a local server" dependency_groups = ["docs", "docs-auto"] commands = [["sphinx-autobuild", "-W", "-b", "dirhtml", "--watch", "src", "docs", "docs/_build/dirhtml"]] [tool.tox.env.update-actions] description = "update GitHub Actions pins" labels = ["update"] dependency_groups = ["gha-update"] skip_install = true commands = [["gha-update"]] [tool.tox.env.update-pre_commit] description = "update pre-commit pins" labels = ["update"] dependency_groups = ["pre-commit"] skip_install = true commands = [["pre-commit", "autoupdate", "--freeze", "-j4"]] [tool.tox.env.update-requirements] description = "update uv lock" labels = ["update"] dependency_groups = [] no_default_groups = true skip_install = true commands = [["uv", "lock", {replace = "posargs", default = ["-U"], extend = true}]] flask-3.1.1/src/000077500000000000000000000000001501065614000134015ustar00rootroot00000000000000flask-3.1.1/src/flask/000077500000000000000000000000001501065614000145015ustar00rootroot00000000000000flask-3.1.1/src/flask/__init__.py000066400000000000000000000052151501065614000166150ustar00rootroot00000000000000from __future__ import annotations import typing as t from . import json as json from .app import Flask as Flask from .blueprints import Blueprint as Blueprint from .config import Config as Config from .ctx import after_this_request as after_this_request from .ctx import copy_current_request_context as copy_current_request_context from .ctx import has_app_context as has_app_context from .ctx import has_request_context as has_request_context from .globals import current_app as current_app from .globals import g as g from .globals import request as request from .globals import session as session from .helpers import abort as abort from .helpers import flash as flash from .helpers import get_flashed_messages as get_flashed_messages from .helpers import get_template_attribute as get_template_attribute from .helpers import make_response as make_response from .helpers import redirect as redirect from .helpers import send_file as send_file from .helpers import send_from_directory as send_from_directory from .helpers import stream_with_context as stream_with_context from .helpers import url_for as url_for from .json import jsonify as jsonify from .signals import appcontext_popped as appcontext_popped from .signals import appcontext_pushed as appcontext_pushed from .signals import appcontext_tearing_down as appcontext_tearing_down from .signals import before_render_template as before_render_template from .signals import got_request_exception as got_request_exception from .signals import message_flashed as message_flashed from .signals import request_finished as request_finished from .signals import request_started as request_started from .signals import request_tearing_down as request_tearing_down from .signals import template_rendered as template_rendered from .templating import render_template as render_template from .templating import render_template_string as render_template_string from .templating import stream_template as stream_template from .templating import stream_template_string as stream_template_string from .wrappers import Request as Request from .wrappers import Response as Response if not t.TYPE_CHECKING: def __getattr__(name: str) -> t.Any: if name == "__version__": import importlib.metadata import warnings warnings.warn( "The '__version__' attribute is deprecated and will be removed in" " Flask 3.2. Use feature detection or" " 'importlib.metadata.version(\"flask\")' instead.", DeprecationWarning, stacklevel=2, ) return importlib.metadata.version("flask") raise AttributeError(name) flask-3.1.1/src/flask/__main__.py000066400000000000000000000000361501065614000165720ustar00rootroot00000000000000from .cli import main main() flask-3.1.1/src/flask/app.py000066400000000000000000001704601501065614000156430ustar00rootroot00000000000000from __future__ import annotations import collections.abc as cabc import os import sys import typing as t import weakref from datetime import timedelta from inspect import iscoroutinefunction from itertools import chain from types import TracebackType from urllib.parse import quote as _url_quote import click from werkzeug.datastructures import Headers from werkzeug.datastructures import ImmutableDict from werkzeug.exceptions import BadRequestKeyError from werkzeug.exceptions import HTTPException from werkzeug.exceptions import InternalServerError from werkzeug.routing import BuildError from werkzeug.routing import MapAdapter from werkzeug.routing import RequestRedirect from werkzeug.routing import RoutingException from werkzeug.routing import Rule from werkzeug.serving import is_running_from_reloader from werkzeug.wrappers import Response as BaseResponse from werkzeug.wsgi import get_host from . import cli from . import typing as ft from .ctx import AppContext from .ctx import RequestContext from .globals import _cv_app from .globals import _cv_request from .globals import current_app from .globals import g from .globals import request from .globals import request_ctx from .globals import session from .helpers import get_debug_flag from .helpers import get_flashed_messages from .helpers import get_load_dotenv from .helpers import send_from_directory from .sansio.app import App from .sansio.scaffold import _sentinel from .sessions import SecureCookieSessionInterface from .sessions import SessionInterface from .signals import appcontext_tearing_down from .signals import got_request_exception from .signals import request_finished from .signals import request_started from .signals import request_tearing_down from .templating import Environment from .wrappers import Request from .wrappers import Response if t.TYPE_CHECKING: # pragma: no cover from _typeshed.wsgi import StartResponse from _typeshed.wsgi import WSGIEnvironment from .testing import FlaskClient from .testing import FlaskCliRunner from .typing import HeadersValue T_shell_context_processor = t.TypeVar( "T_shell_context_processor", bound=ft.ShellContextProcessorCallable ) T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) def _make_timedelta(value: timedelta | int | None) -> timedelta | None: if value is None or isinstance(value, timedelta): return value return timedelta(seconds=value) class Flask(App): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the application. Once it is created it will act as a central registry for the view functions, the URL rules, template configuration and much more. The name of the package is used to resolve resources from inside the package or the folder the module is contained in depending on if the package parameter resolves to an actual python package (a folder with an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). For more information about resource loading, see :func:`open_resource`. Usually you create a :class:`Flask` instance in your main module or in the :file:`__init__.py` file of your package like this:: from flask import Flask app = Flask(__name__) .. admonition:: About the First Parameter The idea of the first parameter is to give Flask an idea of what belongs to your application. This name is used to find resources on the filesystem, can be used by extensions to improve debugging information and a lot more. So it's important what you provide there. If you are using a single module, `__name__` is always the correct value. If you however are using a package, it's usually recommended to hardcode the name of your package there. For example if your application is defined in :file:`yourapplication/app.py` you should create it with one of the two versions below:: app = Flask('yourapplication') app = Flask(__name__.split('.')[0]) Why is that? The application will work even with `__name__`, thanks to how resources are looked up. However it will make debugging more painful. Certain extensions can make assumptions based on the import name of your application. For example the Flask-SQLAlchemy extension will look for the code in your application that triggered an SQL query in debug mode. If the import name is not properly set up, that debugging information is lost. (For example it would only pick up SQL queries in `yourapplication.app` and not `yourapplication.views.frontend`) .. versionadded:: 0.7 The `static_url_path`, `static_folder`, and `template_folder` parameters were added. .. versionadded:: 0.8 The `instance_path` and `instance_relative_config` parameters were added. .. versionadded:: 0.11 The `root_path` parameter was added. .. versionadded:: 1.0 The ``host_matching`` and ``static_host`` parameters were added. .. versionadded:: 1.0 The ``subdomain_matching`` parameter was added. Subdomain matching needs to be enabled manually now. Setting :data:`SERVER_NAME` does not implicitly enable it. :param import_name: the name of the application package :param static_url_path: can be used to specify a different path for the static files on the web. Defaults to the name of the `static_folder` folder. :param static_folder: The folder with static files that is served at ``static_url_path``. Relative to the application ``root_path`` or an absolute path. Defaults to ``'static'``. :param static_host: the host to use when adding the static route. Defaults to None. Required when using ``host_matching=True`` with a ``static_folder`` configured. :param host_matching: set ``url_map.host_matching`` attribute. Defaults to False. :param subdomain_matching: consider the subdomain relative to :data:`SERVER_NAME` when matching routes. Defaults to False. :param template_folder: the folder that contains the templates that should be used by the application. Defaults to ``'templates'`` folder in the root path of the application. :param instance_path: An alternative instance path for the application. By default the folder ``'instance'`` next to the package or module is assumed to be the instance path. :param instance_relative_config: if set to ``True`` relative filenames for loading the config are assumed to be relative to the instance path instead of the application root. :param root_path: The path to the root of the application files. This should only be set manually when it can't be detected automatically, such as for namespace packages. """ default_config = ImmutableDict( { "DEBUG": None, "TESTING": False, "PROPAGATE_EXCEPTIONS": None, "SECRET_KEY": None, "SECRET_KEY_FALLBACKS": None, "PERMANENT_SESSION_LIFETIME": timedelta(days=31), "USE_X_SENDFILE": False, "TRUSTED_HOSTS": None, "SERVER_NAME": None, "APPLICATION_ROOT": "/", "SESSION_COOKIE_NAME": "session", "SESSION_COOKIE_DOMAIN": None, "SESSION_COOKIE_PATH": None, "SESSION_COOKIE_HTTPONLY": True, "SESSION_COOKIE_SECURE": False, "SESSION_COOKIE_PARTITIONED": False, "SESSION_COOKIE_SAMESITE": None, "SESSION_REFRESH_EACH_REQUEST": True, "MAX_CONTENT_LENGTH": None, "MAX_FORM_MEMORY_SIZE": 500_000, "MAX_FORM_PARTS": 1_000, "SEND_FILE_MAX_AGE_DEFAULT": None, "TRAP_BAD_REQUEST_ERRORS": None, "TRAP_HTTP_EXCEPTIONS": False, "EXPLAIN_TEMPLATE_LOADING": False, "PREFERRED_URL_SCHEME": "http", "TEMPLATES_AUTO_RELOAD": None, "MAX_COOKIE_SIZE": 4093, "PROVIDE_AUTOMATIC_OPTIONS": True, } ) #: The class that is used for request objects. See :class:`~flask.Request` #: for more information. request_class: type[Request] = Request #: The class that is used for response objects. See #: :class:`~flask.Response` for more information. response_class: type[Response] = Response #: the session interface to use. By default an instance of #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. #: #: .. versionadded:: 0.8 session_interface: SessionInterface = SecureCookieSessionInterface() def __init__( self, import_name: str, static_url_path: str | None = None, static_folder: str | os.PathLike[str] | None = "static", static_host: str | None = None, host_matching: bool = False, subdomain_matching: bool = False, template_folder: str | os.PathLike[str] | None = "templates", instance_path: str | None = None, instance_relative_config: bool = False, root_path: str | None = None, ): super().__init__( import_name=import_name, static_url_path=static_url_path, static_folder=static_folder, static_host=static_host, host_matching=host_matching, subdomain_matching=subdomain_matching, template_folder=template_folder, instance_path=instance_path, instance_relative_config=instance_relative_config, root_path=root_path, ) #: The Click command group for registering CLI commands for this #: object. The commands are available from the ``flask`` command #: once the application has been discovered and blueprints have #: been registered. self.cli = cli.AppGroup() # Set the name of the Click group in case someone wants to add # the app's commands to another CLI tool. self.cli.name = self.name # Add a static route using the provided static_url_path, static_host, # and static_folder if there is a configured static_folder. # Note we do this without checking if static_folder exists. # For one, it might be created while the server is running (e.g. during # development). Also, Google App Engine stores static files somewhere if self.has_static_folder: assert bool(static_host) == host_matching, ( "Invalid static_host/host_matching combination" ) # Use a weakref to avoid creating a reference cycle between the app # and the view function (see #3761). self_ref = weakref.ref(self) self.add_url_rule( f"{self.static_url_path}/", endpoint="static", host=static_host, view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 ) def get_send_file_max_age(self, filename: str | None) -> int | None: """Used by :func:`send_file` to determine the ``max_age`` cache value for a given file path if it wasn't passed. By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from the configuration of :data:`~flask.current_app`. This defaults to ``None``, which tells the browser to use conditional requests instead of a timed cache, which is usually preferable. Note this is a duplicate of the same method in the Flask class. .. versionchanged:: 2.0 The default configuration is ``None`` instead of 12 hours. .. versionadded:: 0.9 """ value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] if value is None: return None if isinstance(value, timedelta): return int(value.total_seconds()) return value # type: ignore[no-any-return] def send_static_file(self, filename: str) -> Response: """The view function used to serve files from :attr:`static_folder`. A route is automatically registered for this view at :attr:`static_url_path` if :attr:`static_folder` is set. Note this is a duplicate of the same method in the Flask class. .. versionadded:: 0.5 """ if not self.has_static_folder: raise RuntimeError("'static_folder' must be set to serve static_files.") # send_file only knows to call get_send_file_max_age on the app, # call it here so it works for blueprints too. max_age = self.get_send_file_max_age(filename) return send_from_directory( t.cast(str, self.static_folder), filename, max_age=max_age ) def open_resource( self, resource: str, mode: str = "rb", encoding: str | None = None ) -> t.IO[t.AnyStr]: """Open a resource file relative to :attr:`root_path` for reading. For example, if the file ``schema.sql`` is next to the file ``app.py`` where the ``Flask`` app is defined, it can be opened with: .. code-block:: python with app.open_resource("schema.sql") as f: conn.executescript(f.read()) :param resource: Path to the resource relative to :attr:`root_path`. :param mode: Open the file in this mode. Only reading is supported, valid values are ``"r"`` (or ``"rt"``) and ``"rb"``. :param encoding: Open the file with this encoding when opening in text mode. This is ignored when opening in binary mode. .. versionchanged:: 3.1 Added the ``encoding`` parameter. """ if mode not in {"r", "rt", "rb"}: raise ValueError("Resources can only be opened for reading.") path = os.path.join(self.root_path, resource) if mode == "rb": return open(path, mode) # pyright: ignore return open(path, mode, encoding=encoding) def open_instance_resource( self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" ) -> t.IO[t.AnyStr]: """Open a resource file relative to the application's instance folder :attr:`instance_path`. Unlike :meth:`open_resource`, files in the instance folder can be opened for writing. :param resource: Path to the resource relative to :attr:`instance_path`. :param mode: Open the file in this mode. :param encoding: Open the file with this encoding when opening in text mode. This is ignored when opening in binary mode. .. versionchanged:: 3.1 Added the ``encoding`` parameter. """ path = os.path.join(self.instance_path, resource) if "b" in mode: return open(path, mode) return open(path, mode, encoding=encoding) def create_jinja_environment(self) -> Environment: """Create the Jinja environment based on :attr:`jinja_options` and the various Jinja-related methods of the app. Changing :attr:`jinja_options` after this will have no effect. Also adds Flask-related globals and filters to the environment. .. versionchanged:: 0.11 ``Environment.auto_reload`` set in accordance with ``TEMPLATES_AUTO_RELOAD`` configuration option. .. versionadded:: 0.5 """ options = dict(self.jinja_options) if "autoescape" not in options: options["autoescape"] = self.select_jinja_autoescape if "auto_reload" not in options: auto_reload = self.config["TEMPLATES_AUTO_RELOAD"] if auto_reload is None: auto_reload = self.debug options["auto_reload"] = auto_reload rv = self.jinja_environment(self, **options) rv.globals.update( url_for=self.url_for, get_flashed_messages=get_flashed_messages, config=self.config, # request, session and g are normally added with the # context processor for efficiency reasons but for imported # templates we also want the proxies in there. request=request, session=session, g=g, ) rv.policies["json.dumps_function"] = self.json.dumps return rv def create_url_adapter(self, request: Request | None) -> MapAdapter | None: """Creates a URL adapter for the given request. The URL adapter is created at a point where the request context is not yet set up so the request is passed explicitly. .. versionchanged:: 3.1 If :data:`SERVER_NAME` is set, it does not restrict requests to only that domain, for both ``subdomain_matching`` and ``host_matching``. .. versionchanged:: 1.0 :data:`SERVER_NAME` no longer implicitly enables subdomain matching. Use :attr:`subdomain_matching` instead. .. versionchanged:: 0.9 This can be called outside a request when the URL adapter is created for an application context. .. versionadded:: 0.6 """ if request is not None: if (trusted_hosts := self.config["TRUSTED_HOSTS"]) is not None: request.trusted_hosts = trusted_hosts # Check trusted_hosts here until bind_to_environ does. request.host = get_host(request.environ, request.trusted_hosts) # pyright: ignore subdomain = None server_name = self.config["SERVER_NAME"] if self.url_map.host_matching: # Don't pass SERVER_NAME, otherwise it's used and the actual # host is ignored, which breaks host matching. server_name = None elif not self.subdomain_matching: # Werkzeug doesn't implement subdomain matching yet. Until then, # disable it by forcing the current subdomain to the default, or # the empty string. subdomain = self.url_map.default_subdomain or "" return self.url_map.bind_to_environ( request.environ, server_name=server_name, subdomain=subdomain ) # Need at least SERVER_NAME to match/build outside a request. if self.config["SERVER_NAME"] is not None: return self.url_map.bind( self.config["SERVER_NAME"], script_name=self.config["APPLICATION_ROOT"], url_scheme=self.config["PREFERRED_URL_SCHEME"], ) return None def raise_routing_exception(self, request: Request) -> t.NoReturn: """Intercept routing exceptions and possibly do something else. In debug mode, intercept a routing redirect and replace it with an error if the body will be discarded. With modern Werkzeug this shouldn't occur, since it now uses a 308 status which tells the browser to resend the method and body. .. versionchanged:: 2.1 Don't intercept 307 and 308 redirects. :meta private: :internal: """ if ( not self.debug or not isinstance(request.routing_exception, RequestRedirect) or request.routing_exception.code in {307, 308} or request.method in {"GET", "HEAD", "OPTIONS"} ): raise request.routing_exception # type: ignore[misc] from .debughelpers import FormDataRoutingRedirect raise FormDataRoutingRedirect(request) def update_template_context(self, context: dict[str, t.Any]) -> None: """Update the template context with some commonly used variables. This injects request, session, config and g into the template context as well as everything template context processors want to inject. Note that the as of Flask 0.6, the original values in the context will not be overridden if a context processor decides to return a value with the same key. :param context: the context as a dictionary that is updated in place to add extra variables. """ names: t.Iterable[str | None] = (None,) # A template may be rendered outside a request context. if request: names = chain(names, reversed(request.blueprints)) # The values passed to render_template take precedence. Keep a # copy to re-apply after all context functions. orig_ctx = context.copy() for name in names: if name in self.template_context_processors: for func in self.template_context_processors[name]: context.update(self.ensure_sync(func)()) context.update(orig_ctx) def make_shell_context(self) -> dict[str, t.Any]: """Returns the shell context for an interactive shell for this application. This runs all the registered shell context processors. .. versionadded:: 0.11 """ rv = {"app": self, "g": g} for processor in self.shell_context_processors: rv.update(processor()) return rv def run( self, host: str | None = None, port: int | None = None, debug: bool | None = None, load_dotenv: bool = True, **options: t.Any, ) -> None: """Runs the application on a local development server. Do not use ``run()`` in a production setting. It is not intended to meet security and performance requirements for a production server. Instead, see :doc:`/deploying/index` for WSGI server recommendations. If the :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. If you want to run the application in debug mode, but disable the code execution on the interactive debugger, you can pass ``use_evalex=False`` as parameter. This will keep the debugger's traceback screen active, but disable code execution. It is not recommended to use this function for development with automatic reloading as this is badly supported. Instead you should be using the :command:`flask` command line script's ``run`` support. .. admonition:: Keep in Mind Flask will suppress any server error with a generic error page unless it is in debug mode. As such to enable just the interactive debugger without the code reloading, you have to invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. Setting ``use_debugger`` to ``True`` without being in debug mode won't catch any exceptions because there won't be any to catch. :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to have the server available externally as well. Defaults to ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable if present. :param port: the port of the webserver. Defaults to ``5000`` or the port defined in the ``SERVER_NAME`` config variable if present. :param debug: if given, enable or disable debug mode. See :attr:`debug`. :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` files to set environment variables. Will also change the working directory to the directory containing the first file found. :param options: the options to be forwarded to the underlying Werkzeug server. See :func:`werkzeug.serving.run_simple` for more information. .. versionchanged:: 1.0 If installed, python-dotenv will be used to load environment variables from :file:`.env` and :file:`.flaskenv` files. The :envvar:`FLASK_DEBUG` environment variable will override :attr:`debug`. Threaded mode is enabled by default. .. versionchanged:: 0.10 The default port is now picked from the ``SERVER_NAME`` variable. """ # Ignore this call so that it doesn't start another server if # the 'flask run' command is used. if os.environ.get("FLASK_RUN_FROM_CLI") == "true": if not is_running_from_reloader(): click.secho( " * Ignoring a call to 'app.run()' that would block" " the current 'flask' CLI command.\n" " Only call 'app.run()' in an 'if __name__ ==" ' "__main__"\' guard.', fg="red", ) return if get_load_dotenv(load_dotenv): cli.load_dotenv() # if set, env var overrides existing value if "FLASK_DEBUG" in os.environ: self.debug = get_debug_flag() # debug passed to method overrides all other sources if debug is not None: self.debug = bool(debug) server_name = self.config.get("SERVER_NAME") sn_host = sn_port = None if server_name: sn_host, _, sn_port = server_name.partition(":") if not host: if sn_host: host = sn_host else: host = "127.0.0.1" if port or port == 0: port = int(port) elif sn_port: port = int(sn_port) else: port = 5000 options.setdefault("use_reloader", self.debug) options.setdefault("use_debugger", self.debug) options.setdefault("threaded", True) cli.show_server_banner(self.debug, self.name) from werkzeug.serving import run_simple try: run_simple(t.cast(str, host), port, self, **options) finally: # reset the first request information if the development server # reset normally. This makes it possible to restart the server # without reloader and that stuff from an interactive shell. self._got_first_request = False def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> FlaskClient: """Creates a test client for this application. For information about unit testing head over to :doc:`/testing`. Note that if you are testing for assertions or exceptions in your application code, you must set ``app.testing = True`` in order for the exceptions to propagate to the test client. Otherwise, the exception will be handled by the application (not visible to the test client) and the only indication of an AssertionError or other exception will be a 500 status code response to the test client. See the :attr:`testing` attribute. For example:: app.testing = True client = app.test_client() The test client can be used in a ``with`` block to defer the closing down of the context until the end of the ``with`` block. This is useful if you want to access the context locals for testing:: with app.test_client() as c: rv = c.get('/?vodka=42') assert request.args['vodka'] == '42' Additionally, you may pass optional keyword arguments that will then be passed to the application's :attr:`test_client_class` constructor. For example:: from flask.testing import FlaskClient class CustomClient(FlaskClient): def __init__(self, *args, **kwargs): self._authentication = kwargs.pop("authentication") super(CustomClient,self).__init__( *args, **kwargs) app.test_client_class = CustomClient client = app.test_client(authentication='Basic ....') See :class:`~flask.testing.FlaskClient` for more information. .. versionchanged:: 0.4 added support for ``with`` block usage for the client. .. versionadded:: 0.7 The `use_cookies` parameter was added as well as the ability to override the client to be used by setting the :attr:`test_client_class` attribute. .. versionchanged:: 0.11 Added `**kwargs` to support passing additional keyword arguments to the constructor of :attr:`test_client_class`. """ cls = self.test_client_class if cls is None: from .testing import FlaskClient as cls return cls( # type: ignore self, self.response_class, use_cookies=use_cookies, **kwargs ) def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner: """Create a CLI runner for testing CLI commands. See :ref:`testing-cli`. Returns an instance of :attr:`test_cli_runner_class`, by default :class:`~flask.testing.FlaskCliRunner`. The Flask app object is passed as the first argument. .. versionadded:: 1.0 """ cls = self.test_cli_runner_class if cls is None: from .testing import FlaskCliRunner as cls return cls(self, **kwargs) # type: ignore def handle_http_exception( self, e: HTTPException ) -> HTTPException | ft.ResponseReturnValue: """Handles an HTTP exception. By default this will invoke the registered error handlers and fall back to returning the exception as response. .. versionchanged:: 1.0.3 ``RoutingException``, used internally for actions such as slash redirects during routing, is not passed to error handlers. .. versionchanged:: 1.0 Exceptions are looked up by code *and* by MRO, so ``HTTPException`` subclasses can be handled with a catch-all handler for the base ``HTTPException``. .. versionadded:: 0.3 """ # Proxy exceptions don't have error codes. We want to always return # those unchanged as errors if e.code is None: return e # RoutingExceptions are used internally to trigger routing # actions, such as slash redirects raising RequestRedirect. They # are not raised or handled in user code. if isinstance(e, RoutingException): return e handler = self._find_error_handler(e, request.blueprints) if handler is None: return e return self.ensure_sync(handler)(e) # type: ignore[no-any-return] def handle_user_exception( self, e: Exception ) -> HTTPException | ft.ResponseReturnValue: """This method is called whenever an exception occurs that should be handled. A special case is :class:`~werkzeug .exceptions.HTTPException` which is forwarded to the :meth:`handle_http_exception` method. This function will either return a response value or reraise the exception with the same traceback. .. versionchanged:: 1.0 Key errors raised from request data like ``form`` show the bad key in debug mode rather than a generic bad request message. .. versionadded:: 0.7 """ if isinstance(e, BadRequestKeyError) and ( self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] ): e.show_exception = True if isinstance(e, HTTPException) and not self.trap_http_exception(e): return self.handle_http_exception(e) handler = self._find_error_handler(e, request.blueprints) if handler is None: raise return self.ensure_sync(handler)(e) # type: ignore[no-any-return] def handle_exception(self, e: Exception) -> Response: """Handle an exception that did not have an error handler associated with it, or that was raised from an error handler. This always causes a 500 ``InternalServerError``. Always sends the :data:`got_request_exception` signal. If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug mode, the error will be re-raised so that the debugger can display it. Otherwise, the original exception is logged, and an :exc:`~werkzeug.exceptions.InternalServerError` is returned. If an error handler is registered for ``InternalServerError`` or ``500``, it will be used. For consistency, the handler will always receive the ``InternalServerError``. The original unhandled exception is available as ``e.original_exception``. .. versionchanged:: 1.1.0 Always passes the ``InternalServerError`` instance to the handler, setting ``original_exception`` to the unhandled error. .. versionchanged:: 1.1.0 ``after_request`` functions and other finalization is done even for the default 500 response when there is no handler. .. versionadded:: 0.3 """ exc_info = sys.exc_info() got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e) propagate = self.config["PROPAGATE_EXCEPTIONS"] if propagate is None: propagate = self.testing or self.debug if propagate: # Re-raise if called with an active exception, otherwise # raise the passed in exception. if exc_info[1] is e: raise raise e self.log_exception(exc_info) server_error: InternalServerError | ft.ResponseReturnValue server_error = InternalServerError(original_exception=e) handler = self._find_error_handler(server_error, request.blueprints) if handler is not None: server_error = self.ensure_sync(handler)(server_error) return self.finalize_request(server_error, from_error_handler=True) def log_exception( self, exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]), ) -> None: """Logs an exception. This is called by :meth:`handle_exception` if debugging is disabled and right before the handler is called. The default implementation logs the exception as error on the :attr:`logger`. .. versionadded:: 0.8 """ self.logger.error( f"Exception on {request.path} [{request.method}]", exc_info=exc_info ) def dispatch_request(self) -> ft.ResponseReturnValue: """Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to be a response object. In order to convert the return value to a proper response object, call :func:`make_response`. .. versionchanged:: 0.7 This no longer does the exception handling, this code was moved to the new :meth:`full_dispatch_request`. """ req = request_ctx.request if req.routing_exception is not None: self.raise_routing_exception(req) rule: Rule = req.url_rule # type: ignore[assignment] # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically if ( getattr(rule, "provide_automatic_options", False) and req.method == "OPTIONS" ): return self.make_default_options_response() # otherwise dispatch to the handler for that endpoint view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] def full_dispatch_request(self) -> Response: """Dispatches the request and on top of that performs request pre and postprocessing as well as HTTP exception catching and error handling. .. versionadded:: 0.7 """ self._got_first_request = True try: request_started.send(self, _async_wrapper=self.ensure_sync) rv = self.preprocess_request() if rv is None: rv = self.dispatch_request() except Exception as e: rv = self.handle_user_exception(e) return self.finalize_request(rv) def finalize_request( self, rv: ft.ResponseReturnValue | HTTPException, from_error_handler: bool = False, ) -> Response: """Given the return value from a view function this finalizes the request by converting it into a response and invoking the postprocessing functions. This is invoked for both normal request dispatching as well as error handlers. Because this means that it might be called as a result of a failure a special safe mode is available which can be enabled with the `from_error_handler` flag. If enabled, failures in response processing will be logged and otherwise ignored. :internal: """ response = self.make_response(rv) try: response = self.process_response(response) request_finished.send( self, _async_wrapper=self.ensure_sync, response=response ) except Exception: if not from_error_handler: raise self.logger.exception( "Request finalizing failed with an error while handling an error" ) return response def make_default_options_response(self) -> Response: """This method is called to create the default ``OPTIONS`` response. This can be changed through subclassing to change the default behavior of ``OPTIONS`` responses. .. versionadded:: 0.7 """ adapter = request_ctx.url_adapter methods = adapter.allowed_methods() # type: ignore[union-attr] rv = self.response_class() rv.allow.update(methods) return rv def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: """Ensure that the function is synchronous for WSGI workers. Plain ``def`` functions are returned as-is. ``async def`` functions are wrapped to run and wait for the response. Override this method to change how the app runs async views. .. versionadded:: 2.0 """ if iscoroutinefunction(func): return self.async_to_sync(func) return func def async_to_sync( self, func: t.Callable[..., t.Coroutine[t.Any, t.Any, t.Any]] ) -> t.Callable[..., t.Any]: """Return a sync function that will run the coroutine function. .. code-block:: python result = app.async_to_sync(func)(*args, **kwargs) Override this method to change how the app converts async code to be synchronously callable. .. versionadded:: 2.0 """ try: from asgiref.sync import async_to_sync as asgiref_async_to_sync except ImportError: raise RuntimeError( "Install Flask with the 'async' extra in order to use async views." ) from None return asgiref_async_to_sync(func) def url_for( self, /, endpoint: str, *, _anchor: str | None = None, _method: str | None = None, _scheme: str | None = None, _external: bool | None = None, **values: t.Any, ) -> str: """Generate a URL to the given endpoint with the given values. This is called by :func:`flask.url_for`, and can be called directly as well. An *endpoint* is the name of a URL rule, usually added with :meth:`@app.route() `, and usually the same name as the view function. A route defined in a :class:`~flask.Blueprint` will prepend the blueprint's name separated by a ``.`` to the endpoint. In some cases, such as email messages, you want URLs to include the scheme and domain, like ``https://example.com/hello``. When not in an active request, URLs will be external by default, but this requires setting :data:`SERVER_NAME` so Flask knows what domain to use. :data:`APPLICATION_ROOT` and :data:`PREFERRED_URL_SCHEME` should also be configured as needed. This config is only used when not in an active request. Functions can be decorated with :meth:`url_defaults` to modify keyword arguments before the URL is built. If building fails for some reason, such as an unknown endpoint or incorrect values, the app's :meth:`handle_url_build_error` method is called. If that returns a string, that is returned, otherwise a :exc:`~werkzeug.routing.BuildError` is raised. :param endpoint: The endpoint name associated with the URL to generate. If this starts with a ``.``, the current blueprint name (if any) will be used. :param _anchor: If given, append this as ``#anchor`` to the URL. :param _method: If given, generate the URL associated with this method for the endpoint. :param _scheme: If given, the URL will have this scheme if it is external. :param _external: If given, prefer the URL to be internal (False) or require it to be external (True). External URLs include the scheme and domain. When not in an active request, URLs are external by default. :param values: Values to use for the variable parts of the URL rule. Unknown keys are appended as query string arguments, like ``?a=b&c=d``. .. versionadded:: 2.2 Moved from ``flask.url_for``, which calls this method. """ req_ctx = _cv_request.get(None) if req_ctx is not None: url_adapter = req_ctx.url_adapter blueprint_name = req_ctx.request.blueprint # If the endpoint starts with "." and the request matches a # blueprint, the endpoint is relative to the blueprint. if endpoint[:1] == ".": if blueprint_name is not None: endpoint = f"{blueprint_name}{endpoint}" else: endpoint = endpoint[1:] # When in a request, generate a URL without scheme and # domain by default, unless a scheme is given. if _external is None: _external = _scheme is not None else: app_ctx = _cv_app.get(None) # If called by helpers.url_for, an app context is active, # use its url_adapter. Otherwise, app.url_for was called # directly, build an adapter. if app_ctx is not None: url_adapter = app_ctx.url_adapter else: url_adapter = self.create_url_adapter(None) if url_adapter is None: raise RuntimeError( "Unable to build URLs outside an active request" " without 'SERVER_NAME' configured. Also configure" " 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as" " needed." ) # When outside a request, generate a URL with scheme and # domain by default. if _external is None: _external = True # It is an error to set _scheme when _external=False, in order # to avoid accidental insecure URLs. if _scheme is not None and not _external: raise ValueError("When specifying '_scheme', '_external' must be True.") self.inject_url_defaults(endpoint, values) try: rv = url_adapter.build( # type: ignore[union-attr] endpoint, values, method=_method, url_scheme=_scheme, force_external=_external, ) except BuildError as error: values.update( _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external ) return self.handle_url_build_error(error, endpoint, values) if _anchor is not None: _anchor = _url_quote(_anchor, safe="%!#$&'()*+,/:;=?@") rv = f"{rv}#{_anchor}" return rv def make_response(self, rv: ft.ResponseReturnValue) -> Response: """Convert the return value from a view function to an instance of :attr:`response_class`. :param rv: the return value from the view function. The view function must return a response. Returning ``None``, or the view ending without returning, is not allowed. The following types are allowed for ``view_rv``: ``str`` A response object is created with the string encoded to UTF-8 as the body. ``bytes`` A response object is created with the bytes as the body. ``dict`` A dictionary that will be jsonify'd before being returned. ``list`` A list that will be jsonify'd before being returned. ``generator`` or ``iterator`` A generator that returns ``str`` or ``bytes`` to be streamed as the response. ``tuple`` Either ``(body, status, headers)``, ``(body, status)``, or ``(body, headers)``, where ``body`` is any of the other types allowed here, ``status`` is a string or an integer, and ``headers`` is a dictionary or a list of ``(key, value)`` tuples. If ``body`` is a :attr:`response_class` instance, ``status`` overwrites the exiting value and ``headers`` are extended. :attr:`response_class` The object is returned unchanged. other :class:`~werkzeug.wrappers.Response` class The object is coerced to :attr:`response_class`. :func:`callable` The function is called as a WSGI application. The result is used to create a response object. .. versionchanged:: 2.2 A generator will be converted to a streaming response. A list will be converted to a JSON response. .. versionchanged:: 1.1 A dict will be converted to a JSON response. .. versionchanged:: 0.9 Previously a tuple was interpreted as the arguments for the response object. """ status: int | None = None headers: HeadersValue | None = None # unpack tuple returns if isinstance(rv, tuple): len_rv = len(rv) # a 3-tuple is unpacked directly if len_rv == 3: rv, status, headers = rv # type: ignore[misc] # decide if a 2-tuple has status or headers elif len_rv == 2: if isinstance(rv[1], (Headers, dict, tuple, list)): rv, headers = rv # pyright: ignore else: rv, status = rv # type: ignore[assignment,misc] # other sized tuples are not allowed else: raise TypeError( "The view function did not return a valid response tuple." " The tuple must have the form (body, status, headers)," " (body, status), or (body, headers)." ) # the body must not be None if rv is None: raise TypeError( f"The view function for {request.endpoint!r} did not" " return a valid response. The function either returned" " None or ended without a return statement." ) # make sure the body is an instance of the response class if not isinstance(rv, self.response_class): if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, cabc.Iterator): # let the response class set the status and headers instead of # waiting to do it manually, so that the class can handle any # special logic rv = self.response_class( rv, # pyright: ignore status=status, headers=headers, # type: ignore[arg-type] ) status = headers = None elif isinstance(rv, (dict, list)): rv = self.json.response(rv) elif isinstance(rv, BaseResponse) or callable(rv): # evaluate a WSGI callable, or coerce a different response # class to the correct type try: rv = self.response_class.force_type( rv, # type: ignore[arg-type] request.environ, ) except TypeError as e: raise TypeError( f"{e}\nThe view function did not return a valid" " response. The return type must be a string," " dict, list, tuple with headers or status," " Response instance, or WSGI callable, but it" f" was a {type(rv).__name__}." ).with_traceback(sys.exc_info()[2]) from None else: raise TypeError( "The view function did not return a valid" " response. The return type must be a string," " dict, list, tuple with headers or status," " Response instance, or WSGI callable, but it was a" f" {type(rv).__name__}." ) rv = t.cast(Response, rv) # prefer the status if it was provided if status is not None: if isinstance(status, (str, bytes, bytearray)): rv.status = status else: rv.status_code = status # extend existing headers with provided headers if headers: rv.headers.update(headers) return rv def preprocess_request(self) -> ft.ResponseReturnValue | None: """Called before the request is dispatched. Calls :attr:`url_value_preprocessors` registered with the app and the current blueprint (if any). Then calls :attr:`before_request_funcs` registered with the app and the blueprint. If any :meth:`before_request` handler returns a non-None value, the value is handled as if it was the return value from the view, and further request handling is stopped. """ names = (None, *reversed(request.blueprints)) for name in names: if name in self.url_value_preprocessors: for url_func in self.url_value_preprocessors[name]: url_func(request.endpoint, request.view_args) for name in names: if name in self.before_request_funcs: for before_func in self.before_request_funcs[name]: rv = self.ensure_sync(before_func)() if rv is not None: return rv # type: ignore[no-any-return] return None def process_response(self, response: Response) -> Response: """Can be overridden in order to modify the response object before it's sent to the WSGI server. By default this will call all the :meth:`after_request` decorated functions. .. versionchanged:: 0.5 As of Flask 0.5 the functions registered for after request execution are called in reverse order of registration. :param response: a :attr:`response_class` object. :return: a new response object or the same, has to be an instance of :attr:`response_class`. """ ctx = request_ctx._get_current_object() # type: ignore[attr-defined] for func in ctx._after_request_functions: response = self.ensure_sync(func)(response) for name in chain(request.blueprints, (None,)): if name in self.after_request_funcs: for func in reversed(self.after_request_funcs[name]): response = self.ensure_sync(func)(response) if not self.session_interface.is_null_session(ctx.session): self.session_interface.save_session(self, ctx.session, response) return response def do_teardown_request( self, exc: BaseException | None = _sentinel, # type: ignore[assignment] ) -> None: """Called after the request is dispatched and the response is returned, right before the request context is popped. This calls all functions decorated with :meth:`teardown_request`, and :meth:`Blueprint.teardown_request` if a blueprint handled the request. Finally, the :data:`request_tearing_down` signal is sent. This is called by :meth:`RequestContext.pop() `, which may be delayed during testing to maintain access to resources. :param exc: An unhandled exception raised while dispatching the request. Detected from the current exception information if not passed. Passed to each teardown function. .. versionchanged:: 0.9 Added the ``exc`` argument. """ if exc is _sentinel: exc = sys.exc_info()[1] for name in chain(request.blueprints, (None,)): if name in self.teardown_request_funcs: for func in reversed(self.teardown_request_funcs[name]): self.ensure_sync(func)(exc) request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) def do_teardown_appcontext( self, exc: BaseException | None = _sentinel, # type: ignore[assignment] ) -> None: """Called right before the application context is popped. When handling a request, the application context is popped after the request context. See :meth:`do_teardown_request`. This calls all functions decorated with :meth:`teardown_appcontext`. Then the :data:`appcontext_tearing_down` signal is sent. This is called by :meth:`AppContext.pop() `. .. versionadded:: 0.9 """ if exc is _sentinel: exc = sys.exc_info()[1] for func in reversed(self.teardown_appcontext_funcs): self.ensure_sync(func)(exc) appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) def app_context(self) -> AppContext: """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` block to push the context, which will make :data:`current_app` point at this application. An application context is automatically pushed by :meth:`RequestContext.push() ` when handling a request, and when running a CLI command. Use this to manually create a context outside of these situations. :: with app.app_context(): init_db() See :doc:`/appcontext`. .. versionadded:: 0.9 """ return AppContext(self) def request_context(self, environ: WSGIEnvironment) -> RequestContext: """Create a :class:`~flask.ctx.RequestContext` representing a WSGI environment. Use a ``with`` block to push the context, which will make :data:`request` point at this request. See :doc:`/reqcontext`. Typically you should not call this from your own code. A request context is automatically pushed by the :meth:`wsgi_app` when handling a request. Use :meth:`test_request_context` to create an environment and context instead of this method. :param environ: a WSGI environment """ return RequestContext(self, environ) def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: """Create a :class:`~flask.ctx.RequestContext` for a WSGI environment created from the given values. This is mostly useful during testing, where you may want to run a function that uses request data without dispatching a full request. See :doc:`/reqcontext`. Use a ``with`` block to push the context, which will make :data:`request` point at the request for the created environment. :: with app.test_request_context(...): generate_report() When using the shell, it may be easier to push and pop the context manually to avoid indentation. :: ctx = app.test_request_context(...) ctx.push() ... ctx.pop() Takes the same arguments as Werkzeug's :class:`~werkzeug.test.EnvironBuilder`, with some defaults from the application. See the linked Werkzeug docs for most of the available arguments. Flask-specific behavior is listed here. :param path: URL path being requested. :param base_url: Base URL where the app is being served, which ``path`` is relative to. If not given, built from :data:`PREFERRED_URL_SCHEME`, ``subdomain``, :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. :param subdomain: Subdomain name to append to :data:`SERVER_NAME`. :param url_scheme: Scheme to use instead of :data:`PREFERRED_URL_SCHEME`. :param data: The request body, either as a string or a dict of form keys and values. :param json: If given, this is serialized as JSON and passed as ``data``. Also defaults ``content_type`` to ``application/json``. :param args: other positional arguments passed to :class:`~werkzeug.test.EnvironBuilder`. :param kwargs: other keyword arguments passed to :class:`~werkzeug.test.EnvironBuilder`. """ from .testing import EnvironBuilder builder = EnvironBuilder(self, *args, **kwargs) try: return self.request_context(builder.get_environ()) finally: builder.close() def wsgi_app( self, environ: WSGIEnvironment, start_response: StartResponse ) -> cabc.Iterable[bytes]: """The actual WSGI application. This is not implemented in :meth:`__call__` so that middlewares can be applied without losing a reference to the app object. Instead of doing this:: app = MyMiddleware(app) It's a better idea to do this instead:: app.wsgi_app = MyMiddleware(app.wsgi_app) Then you still have the original application object around and can continue to call methods on it. .. versionchanged:: 0.7 Teardown events for the request and app contexts are called even if an unhandled error occurs. Other events may not be called depending on when an error occurs during dispatch. See :ref:`callbacks-and-errors`. :param environ: A WSGI environment. :param start_response: A callable accepting a status code, a list of headers, and an optional exception context to start the response. """ ctx = self.request_context(environ) error: BaseException | None = None try: try: ctx.push() response = self.full_dispatch_request() except Exception as e: error = e response = self.handle_exception(e) except: # noqa: B001 error = sys.exc_info()[1] raise return response(environ, start_response) finally: if "werkzeug.debug.preserve_context" in environ: environ["werkzeug.debug.preserve_context"](_cv_app.get()) environ["werkzeug.debug.preserve_context"](_cv_request.get()) if error is not None and self.should_ignore_error(error): error = None ctx.pop(error) def __call__( self, environ: WSGIEnvironment, start_response: StartResponse ) -> cabc.Iterable[bytes]: """The WSGI server calls the Flask application object as the WSGI application. This calls :meth:`wsgi_app`, which can be wrapped to apply middleware. """ return self.wsgi_app(environ, start_response) flask-3.1.1/src/flask/blueprints.py000066400000000000000000000106751501065614000172530ustar00rootroot00000000000000from __future__ import annotations import os import typing as t from datetime import timedelta from .cli import AppGroup from .globals import current_app from .helpers import send_from_directory from .sansio.blueprints import Blueprint as SansioBlueprint from .sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa from .sansio.scaffold import _sentinel if t.TYPE_CHECKING: # pragma: no cover from .wrappers import Response class Blueprint(SansioBlueprint): def __init__( self, name: str, import_name: str, static_folder: str | os.PathLike[str] | None = None, static_url_path: str | None = None, template_folder: str | os.PathLike[str] | None = None, url_prefix: str | None = None, subdomain: str | None = None, url_defaults: dict[str, t.Any] | None = None, root_path: str | None = None, cli_group: str | None = _sentinel, # type: ignore ) -> None: super().__init__( name, import_name, static_folder, static_url_path, template_folder, url_prefix, subdomain, url_defaults, root_path, cli_group, ) #: The Click command group for registering CLI commands for this #: object. The commands are available from the ``flask`` command #: once the application has been discovered and blueprints have #: been registered. self.cli = AppGroup() # Set the name of the Click group in case someone wants to add # the app's commands to another CLI tool. self.cli.name = self.name def get_send_file_max_age(self, filename: str | None) -> int | None: """Used by :func:`send_file` to determine the ``max_age`` cache value for a given file path if it wasn't passed. By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from the configuration of :data:`~flask.current_app`. This defaults to ``None``, which tells the browser to use conditional requests instead of a timed cache, which is usually preferable. Note this is a duplicate of the same method in the Flask class. .. versionchanged:: 2.0 The default configuration is ``None`` instead of 12 hours. .. versionadded:: 0.9 """ value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] if value is None: return None if isinstance(value, timedelta): return int(value.total_seconds()) return value # type: ignore[no-any-return] def send_static_file(self, filename: str) -> Response: """The view function used to serve files from :attr:`static_folder`. A route is automatically registered for this view at :attr:`static_url_path` if :attr:`static_folder` is set. Note this is a duplicate of the same method in the Flask class. .. versionadded:: 0.5 """ if not self.has_static_folder: raise RuntimeError("'static_folder' must be set to serve static_files.") # send_file only knows to call get_send_file_max_age on the app, # call it here so it works for blueprints too. max_age = self.get_send_file_max_age(filename) return send_from_directory( t.cast(str, self.static_folder), filename, max_age=max_age ) def open_resource( self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" ) -> t.IO[t.AnyStr]: """Open a resource file relative to :attr:`root_path` for reading. The blueprint-relative equivalent of the app's :meth:`~.Flask.open_resource` method. :param resource: Path to the resource relative to :attr:`root_path`. :param mode: Open the file in this mode. Only reading is supported, valid values are ``"r"`` (or ``"rt"``) and ``"rb"``. :param encoding: Open the file with this encoding when opening in text mode. This is ignored when opening in binary mode. .. versionchanged:: 3.1 Added the ``encoding`` parameter. """ if mode not in {"r", "rt", "rb"}: raise ValueError("Resources can only be opened for reading.") path = os.path.join(self.root_path, resource) if mode == "rb": return open(path, mode) # pyright: ignore return open(path, mode, encoding=encoding) flask-3.1.1/src/flask/cli.py000066400000000000000000001105001501065614000156170ustar00rootroot00000000000000from __future__ import annotations import ast import collections.abc as cabc import importlib.metadata import inspect import os import platform import re import sys import traceback import typing as t from functools import update_wrapper from operator import itemgetter from types import ModuleType import click from click.core import ParameterSource from werkzeug import run_simple from werkzeug.serving import is_running_from_reloader from werkzeug.utils import import_string from .globals import current_app from .helpers import get_debug_flag from .helpers import get_load_dotenv if t.TYPE_CHECKING: import ssl from _typeshed.wsgi import StartResponse from _typeshed.wsgi import WSGIApplication from _typeshed.wsgi import WSGIEnvironment from .app import Flask class NoAppException(click.UsageError): """Raised if an application cannot be found or loaded.""" def find_best_app(module: ModuleType) -> Flask: """Given a module instance this tries to find the best possible application in the module or raises an exception. """ from . import Flask # Search for the most common names first. for attr_name in ("app", "application"): app = getattr(module, attr_name, None) if isinstance(app, Flask): return app # Otherwise find the only object that is a Flask instance. matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] if len(matches) == 1: return matches[0] elif len(matches) > 1: raise NoAppException( "Detected multiple Flask applications in module" f" '{module.__name__}'. Use '{module.__name__}:name'" " to specify the correct one." ) # Search for app factory functions. for attr_name in ("create_app", "make_app"): app_factory = getattr(module, attr_name, None) if inspect.isfunction(app_factory): try: app = app_factory() if isinstance(app, Flask): return app except TypeError as e: if not _called_with_wrong_args(app_factory): raise raise NoAppException( f"Detected factory '{attr_name}' in module '{module.__name__}'," " but could not call it without arguments. Use" f" '{module.__name__}:{attr_name}(args)'" " to specify arguments." ) from e raise NoAppException( "Failed to find Flask application or factory in module" f" '{module.__name__}'. Use '{module.__name__}:name'" " to specify one." ) def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool: """Check whether calling a function raised a ``TypeError`` because the call failed or because something in the factory raised the error. :param f: The function that was called. :return: ``True`` if the call failed. """ tb = sys.exc_info()[2] try: while tb is not None: if tb.tb_frame.f_code is f.__code__: # In the function, it was called successfully. return False tb = tb.tb_next # Didn't reach the function. return True finally: # Delete tb to break a circular reference. # https://docs.python.org/2/library/sys.html#sys.exc_info del tb def find_app_by_string(module: ModuleType, app_name: str) -> Flask: """Check if the given string is a variable name or a function. Call a function to get the app instance, or return the variable directly. """ from . import Flask # Parse app_name as a single expression to determine if it's a valid # attribute name or function call. try: expr = ast.parse(app_name.strip(), mode="eval").body except SyntaxError: raise NoAppException( f"Failed to parse {app_name!r} as an attribute name or function call." ) from None if isinstance(expr, ast.Name): name = expr.id args = [] kwargs = {} elif isinstance(expr, ast.Call): # Ensure the function name is an attribute name only. if not isinstance(expr.func, ast.Name): raise NoAppException( f"Function reference must be a simple name: {app_name!r}." ) name = expr.func.id # Parse the positional and keyword arguments as literals. try: args = [ast.literal_eval(arg) for arg in expr.args] kwargs = { kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords if kw.arg is not None } except ValueError: # literal_eval gives cryptic error messages, show a generic # message with the full expression instead. raise NoAppException( f"Failed to parse arguments as literal values: {app_name!r}." ) from None else: raise NoAppException( f"Failed to parse {app_name!r} as an attribute name or function call." ) try: attr = getattr(module, name) except AttributeError as e: raise NoAppException( f"Failed to find attribute {name!r} in {module.__name__!r}." ) from e # If the attribute is a function, call it with any args and kwargs # to get the real application. if inspect.isfunction(attr): try: app = attr(*args, **kwargs) except TypeError as e: if not _called_with_wrong_args(attr): raise raise NoAppException( f"The factory {app_name!r} in module" f" {module.__name__!r} could not be called with the" " specified arguments." ) from e else: app = attr if isinstance(app, Flask): return app raise NoAppException( "A valid Flask application was not obtained from" f" '{module.__name__}:{app_name}'." ) def prepare_import(path: str) -> str: """Given a filename this will try to calculate the python path, add it to the search path and return the actual module name that is expected. """ path = os.path.realpath(path) fname, ext = os.path.splitext(path) if ext == ".py": path = fname if os.path.basename(path) == "__init__": path = os.path.dirname(path) module_name = [] # move up until outside package structure (no __init__.py) while True: path, name = os.path.split(path) module_name.append(name) if not os.path.exists(os.path.join(path, "__init__.py")): break if sys.path[0] != path: sys.path.insert(0, path) return ".".join(module_name[::-1]) @t.overload def locate_app( module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True ) -> Flask: ... @t.overload def locate_app( module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ... ) -> Flask | None: ... def locate_app( module_name: str, app_name: str | None, raise_if_not_found: bool = True ) -> Flask | None: try: __import__(module_name) except ImportError: # Reraise the ImportError if it occurred within the imported module. # Determine this by checking whether the trace has a depth > 1. if sys.exc_info()[2].tb_next: # type: ignore[union-attr] raise NoAppException( f"While importing {module_name!r}, an ImportError was" f" raised:\n\n{traceback.format_exc()}" ) from None elif raise_if_not_found: raise NoAppException(f"Could not import {module_name!r}.") from None else: return None module = sys.modules[module_name] if app_name is None: return find_best_app(module) else: return find_app_by_string(module, app_name) def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None: if not value or ctx.resilient_parsing: return flask_version = importlib.metadata.version("flask") werkzeug_version = importlib.metadata.version("werkzeug") click.echo( f"Python {platform.python_version()}\n" f"Flask {flask_version}\n" f"Werkzeug {werkzeug_version}", color=ctx.color, ) ctx.exit() version_option = click.Option( ["--version"], help="Show the Flask version.", expose_value=False, callback=get_version, is_flag=True, is_eager=True, ) class ScriptInfo: """Helper object to deal with Flask applications. This is usually not necessary to interface with as it's used internally in the dispatching to click. In future versions of Flask this object will most likely play a bigger role. Typically it's created automatically by the :class:`FlaskGroup` but you can also manually create it and pass it onwards as click object. .. versionchanged:: 3.1 Added the ``load_dotenv_defaults`` parameter and attribute. """ def __init__( self, app_import_path: str | None = None, create_app: t.Callable[..., Flask] | None = None, set_debug_flag: bool = True, load_dotenv_defaults: bool = True, ) -> None: #: Optionally the import path for the Flask application. self.app_import_path = app_import_path #: Optionally a function that is passed the script info to create #: the instance of the application. self.create_app = create_app #: A dictionary with arbitrary data that can be associated with #: this script info. self.data: dict[t.Any, t.Any] = {} self.set_debug_flag = set_debug_flag self.load_dotenv_defaults = get_load_dotenv(load_dotenv_defaults) """Whether default ``.flaskenv`` and ``.env`` files should be loaded. ``ScriptInfo`` doesn't load anything, this is for reference when doing the load elsewhere during processing. .. versionadded:: 3.1 """ self._loaded_app: Flask | None = None def load_app(self) -> Flask: """Loads the Flask app (if not yet loaded) and returns it. Calling this multiple times will just result in the already loaded app to be returned. """ if self._loaded_app is not None: return self._loaded_app app: Flask | None = None if self.create_app is not None: app = self.create_app() else: if self.app_import_path: path, name = ( re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None] )[:2] import_name = prepare_import(path) app = locate_app(import_name, name) else: for path in ("wsgi.py", "app.py"): import_name = prepare_import(path) app = locate_app(import_name, None, raise_if_not_found=False) if app is not None: break if app is None: raise NoAppException( "Could not locate a Flask application. Use the" " 'flask --app' option, 'FLASK_APP' environment" " variable, or a 'wsgi.py' or 'app.py' file in the" " current directory." ) if self.set_debug_flag: # Update the app's debug flag through the descriptor so that # other values repopulate as well. app.debug = get_debug_flag() self._loaded_app = app return app pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) F = t.TypeVar("F", bound=t.Callable[..., t.Any]) def with_appcontext(f: F) -> F: """Wraps a callback so that it's guaranteed to be executed with the script's application context. Custom commands (and their options) registered under ``app.cli`` or ``blueprint.cli`` will always have an app context available, this decorator is not required in that case. .. versionchanged:: 2.2 The app context is active for subcommands as well as the decorated callback. The app context is always available to ``app.cli`` command and parameter callbacks. """ @click.pass_context def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any: if not current_app: app = ctx.ensure_object(ScriptInfo).load_app() ctx.with_resource(app.app_context()) return ctx.invoke(f, *args, **kwargs) return update_wrapper(decorator, f) # type: ignore[return-value] class AppGroup(click.Group): """This works similar to a regular click :class:`~click.Group` but it changes the behavior of the :meth:`command` decorator so that it automatically wraps the functions in :func:`with_appcontext`. Not to be confused with :class:`FlaskGroup`. """ def command( # type: ignore[override] self, *args: t.Any, **kwargs: t.Any ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]: """This works exactly like the method of the same name on a regular :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` unless it's disabled by passing ``with_appcontext=False``. """ wrap_for_ctx = kwargs.pop("with_appcontext", True) def decorator(f: t.Callable[..., t.Any]) -> click.Command: if wrap_for_ctx: f = with_appcontext(f) return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return] return decorator def group( # type: ignore[override] self, *args: t.Any, **kwargs: t.Any ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]: """This works exactly like the method of the same name on a regular :class:`click.Group` but it defaults the group class to :class:`AppGroup`. """ kwargs.setdefault("cls", AppGroup) return super().group(*args, **kwargs) # type: ignore[no-any-return] def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: if value is None: return None info = ctx.ensure_object(ScriptInfo) info.app_import_path = value return value # This option is eager so the app will be available if --help is given. # --help is also eager, so --app must be before it in the param list. # no_args_is_help bypasses eager processing, so this option must be # processed manually in that case to ensure FLASK_APP gets picked up. _app_option = click.Option( ["-A", "--app"], metavar="IMPORT", help=( "The Flask application or factory function to load, in the form 'module:name'." " Module can be a dotted import or file path. Name is not required if it is" " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to" " pass arguments." ), is_eager=True, expose_value=False, callback=_set_app, ) def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: # If the flag isn't provided, it will default to False. Don't use # that, let debug be set by env in that case. source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] if source is not None and source in ( ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP, ): return None # Set with env var instead of ScriptInfo.load so that it can be # accessed early during a factory function. os.environ["FLASK_DEBUG"] = "1" if value else "0" return value _debug_option = click.Option( ["--debug/--no-debug"], help="Set debug mode.", expose_value=False, callback=_set_debug, ) def _env_file_callback( ctx: click.Context, param: click.Option, value: str | None ) -> str | None: try: import dotenv # noqa: F401 except ImportError: # Only show an error if a value was passed, otherwise we still want to # call load_dotenv and show a message without exiting. if value is not None: raise click.BadParameter( "python-dotenv must be installed to load an env file.", ctx=ctx, param=param, ) from None # Load if a value was passed, or we want to load default files, or both. if value is not None or ctx.obj.load_dotenv_defaults: load_dotenv(value, load_defaults=ctx.obj.load_dotenv_defaults) return value # This option is eager so env vars are loaded as early as possible to be # used by other options. _env_file_option = click.Option( ["-e", "--env-file"], type=click.Path(exists=True, dir_okay=False), help=( "Load environment variables from this file, taking precedence over" " those set by '.env' and '.flaskenv'. Variables set directly in the" " environment take highest precedence. python-dotenv must be installed." ), is_eager=True, expose_value=False, callback=_env_file_callback, ) class FlaskGroup(AppGroup): """Special subclass of the :class:`AppGroup` group that supports loading more commands from the configured Flask app. Normally a developer does not have to interface with this class but there are some very advanced use cases for which it makes sense to create an instance of this. see :ref:`custom-scripts`. :param add_default_commands: if this is True then the default run and shell commands will be added. :param add_version_option: adds the ``--version`` option. :param create_app: an optional callback that is passed the script info and returns the loaded app. :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` files to set environment variables. Will also change the working directory to the directory containing the first file found. :param set_debug_flag: Set the app's debug flag. .. versionchanged:: 3.1 ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files. .. versionchanged:: 2.2 Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options. .. versionchanged:: 2.2 An app context is pushed when running ``app.cli`` commands, so ``@with_appcontext`` is no longer required for those commands. .. versionchanged:: 1.0 If installed, python-dotenv will be used to load environment variables from :file:`.env` and :file:`.flaskenv` files. """ def __init__( self, add_default_commands: bool = True, create_app: t.Callable[..., Flask] | None = None, add_version_option: bool = True, load_dotenv: bool = True, set_debug_flag: bool = True, **extra: t.Any, ) -> None: params: list[click.Parameter] = list(extra.pop("params", None) or ()) # Processing is done with option callbacks instead of a group # callback. This allows users to make a custom group callback # without losing the behavior. --env-file must come first so # that it is eagerly evaluated before --app. params.extend((_env_file_option, _app_option, _debug_option)) if add_version_option: params.append(version_option) if "context_settings" not in extra: extra["context_settings"] = {} extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK") super().__init__(params=params, **extra) self.create_app = create_app self.load_dotenv = load_dotenv self.set_debug_flag = set_debug_flag if add_default_commands: self.add_command(run_command) self.add_command(shell_command) self.add_command(routes_command) self._loaded_plugin_commands = False def _load_plugin_commands(self) -> None: if self._loaded_plugin_commands: return if sys.version_info >= (3, 10): from importlib import metadata else: # Use a backport on Python < 3.10. We technically have # importlib.metadata on 3.8+, but the API changed in 3.10, # so use the backport for consistency. import importlib_metadata as metadata # pyright: ignore for ep in metadata.entry_points(group="flask.commands"): self.add_command(ep.load(), ep.name) self._loaded_plugin_commands = True def get_command(self, ctx: click.Context, name: str) -> click.Command | None: self._load_plugin_commands() # Look up built-in and plugin commands, which should be # available even if the app fails to load. rv = super().get_command(ctx, name) if rv is not None: return rv info = ctx.ensure_object(ScriptInfo) # Look up commands provided by the app, showing an error and # continuing if the app couldn't be loaded. try: app = info.load_app() except NoAppException as e: click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") return None # Push an app context for the loaded app unless it is already # active somehow. This makes the context available to parameter # and command callbacks without needing @with_appcontext. if not current_app or current_app._get_current_object() is not app: # type: ignore[attr-defined] ctx.with_resource(app.app_context()) return app.cli.get_command(ctx, name) def list_commands(self, ctx: click.Context) -> list[str]: self._load_plugin_commands() # Start with the built-in and plugin commands. rv = set(super().list_commands(ctx)) info = ctx.ensure_object(ScriptInfo) # Add commands provided by the app, showing an error and # continuing if the app couldn't be loaded. try: rv.update(info.load_app().cli.list_commands(ctx)) except NoAppException as e: # When an app couldn't be loaded, show the error message # without the traceback. click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") except Exception: # When any other errors occurred during loading, show the # full traceback. click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") return sorted(rv) def make_context( self, info_name: str | None, args: list[str], parent: click.Context | None = None, **extra: t.Any, ) -> click.Context: # Set a flag to tell app.run to become a no-op. If app.run was # not in a __name__ == __main__ guard, it would start the server # when importing, blocking whatever command is being called. os.environ["FLASK_RUN_FROM_CLI"] = "true" if "obj" not in extra and "obj" not in self.context_settings: extra["obj"] = ScriptInfo( create_app=self.create_app, set_debug_flag=self.set_debug_flag, load_dotenv_defaults=self.load_dotenv, ) return super().make_context(info_name, args, parent=parent, **extra) def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: if (not args and self.no_args_is_help) or ( len(args) == 1 and args[0] in self.get_help_option_names(ctx) ): # Attempt to load --env-file and --app early in case they # were given as env vars. Otherwise no_args_is_help will not # see commands from app.cli. _env_file_option.handle_parse_result(ctx, {}, []) _app_option.handle_parse_result(ctx, {}, []) return super().parse_args(ctx, args) def _path_is_ancestor(path: str, other: str) -> bool: """Take ``other`` and remove the length of ``path`` from it. Then join it to ``path``. If it is the original value, ``path`` is an ancestor of ``other``.""" return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other def load_dotenv( path: str | os.PathLike[str] | None = None, load_defaults: bool = True ) -> bool: """Load "dotenv" files to set environment variables. A given path takes precedence over ``.env``, which takes precedence over ``.flaskenv``. After loading and combining these files, values are only set if the key is not already set in ``os.environ``. This is a no-op if `python-dotenv`_ is not installed. .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme :param path: Load the file at this location. :param load_defaults: Search for and load the default ``.flaskenv`` and ``.env`` files. :return: ``True`` if at least one env var was loaded. .. versionchanged:: 3.1 Added the ``load_defaults`` parameter. A given path takes precedence over default files. .. versionchanged:: 2.0 The current directory is not changed to the location of the loaded file. .. versionchanged:: 2.0 When loading the env files, set the default encoding to UTF-8. .. versionchanged:: 1.1.0 Returns ``False`` when python-dotenv is not installed, or when the given path isn't a file. .. versionadded:: 1.0 """ try: import dotenv except ImportError: if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): click.secho( " * Tip: There are .env files present. Install python-dotenv" " to use them.", fg="yellow", err=True, ) return False data: dict[str, str | None] = {} if load_defaults: for default_name in (".flaskenv", ".env"): if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)): continue data |= dotenv.dotenv_values(default_path, encoding="utf-8") if path is not None and os.path.isfile(path): data |= dotenv.dotenv_values(path, encoding="utf-8") for key, value in data.items(): if key in os.environ or value is None: continue os.environ[key] = value return bool(data) # True if at least one env var was loaded. def show_server_banner(debug: bool, app_import_path: str | None) -> None: """Show extra startup messages the first time the server is run, ignoring the reloader. """ if is_running_from_reloader(): return if app_import_path is not None: click.echo(f" * Serving Flask app '{app_import_path}'") if debug is not None: click.echo(f" * Debug mode: {'on' if debug else 'off'}") class CertParamType(click.ParamType): """Click option type for the ``--cert`` option. Allows either an existing file, the string ``'adhoc'``, or an import for a :class:`~ssl.SSLContext` object. """ name = "path" def __init__(self) -> None: self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) def convert( self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None ) -> t.Any: try: import ssl except ImportError: raise click.BadParameter( 'Using "--cert" requires Python to be compiled with SSL support.', ctx, param, ) from None try: return self.path_type(value, param, ctx) except click.BadParameter: value = click.STRING(value, param, ctx).lower() if value == "adhoc": try: import cryptography # noqa: F401 except ImportError: raise click.BadParameter( "Using ad-hoc certificates requires the cryptography library.", ctx, param, ) from None return value obj = import_string(value, silent=True) if isinstance(obj, ssl.SSLContext): return obj raise def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any: """The ``--key`` option must be specified when ``--cert`` is a file. Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. """ cert = ctx.params.get("cert") is_adhoc = cert == "adhoc" try: import ssl except ImportError: is_context = False else: is_context = isinstance(cert, ssl.SSLContext) if value is not None: if is_adhoc: raise click.BadParameter( 'When "--cert" is "adhoc", "--key" is not used.', ctx, param ) if is_context: raise click.BadParameter( 'When "--cert" is an SSLContext object, "--key" is not used.', ctx, param, ) if not cert: raise click.BadParameter('"--cert" must also be specified.', ctx, param) ctx.params["cert"] = cert, value else: if cert and not (is_adhoc or is_context): raise click.BadParameter('Required when using "--cert".', ctx, param) return value class SeparatedPathType(click.Path): """Click option type that accepts a list of values separated by the OS's path separator (``:``, ``;`` on Windows). Each value is validated as a :class:`click.Path` type. """ def convert( self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None ) -> t.Any: items = self.split_envvar_value(value) # can't call no-arg super() inside list comprehension until Python 3.12 super_convert = super().convert return [super_convert(item, param, ctx) for item in items] @click.command("run", short_help="Run a development server.") @click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") @click.option("--port", "-p", default=5000, help="The port to bind to.") @click.option( "--cert", type=CertParamType(), help="Specify a certificate file to use HTTPS.", is_eager=True, ) @click.option( "--key", type=click.Path(exists=True, dir_okay=False, resolve_path=True), callback=_validate_key, expose_value=False, help="The key file to use when specifying a certificate.", ) @click.option( "--reload/--no-reload", default=None, help="Enable or disable the reloader. By default the reloader " "is active if debug is enabled.", ) @click.option( "--debugger/--no-debugger", default=None, help="Enable or disable the debugger. By default the debugger " "is active if debug is enabled.", ) @click.option( "--with-threads/--without-threads", default=True, help="Enable or disable multithreading.", ) @click.option( "--extra-files", default=None, type=SeparatedPathType(), help=( "Extra files that trigger a reload on change. Multiple paths" f" are separated by {os.path.pathsep!r}." ), ) @click.option( "--exclude-patterns", default=None, type=SeparatedPathType(), help=( "Files matching these fnmatch patterns will not trigger a reload" " on change. Multiple patterns are separated by" f" {os.path.pathsep!r}." ), ) @pass_script_info def run_command( info: ScriptInfo, host: str, port: int, reload: bool, debugger: bool, with_threads: bool, cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None, extra_files: list[str] | None, exclude_patterns: list[str] | None, ) -> None: """Run a local development server. This server is for development purposes only. It does not provide the stability, security, or performance of production WSGI servers. The reloader and debugger are enabled by default with the '--debug' option. """ try: app: WSGIApplication = info.load_app() # pyright: ignore except Exception as e: if is_running_from_reloader(): # When reloading, print out the error immediately, but raise # it later so the debugger or server can handle it. traceback.print_exc() err = e def app( environ: WSGIEnvironment, start_response: StartResponse ) -> cabc.Iterable[bytes]: raise err from None else: # When not reloading, raise the error immediately so the # command fails. raise e from None debug = get_debug_flag() if reload is None: reload = debug if debugger is None: debugger = debug show_server_banner(debug, info.app_import_path) run_simple( host, port, app, use_reloader=reload, use_debugger=debugger, threaded=with_threads, ssl_context=cert, extra_files=extra_files, exclude_patterns=exclude_patterns, ) run_command.params.insert(0, _debug_option) @click.command("shell", short_help="Run a shell in the app context.") @with_appcontext def shell_command() -> None: """Run an interactive Python shell in the context of a given Flask application. The application will populate the default namespace of this shell according to its configuration. This is useful for executing small snippets of management code without having to manually configure the application. """ import code banner = ( f"Python {sys.version} on {sys.platform}\n" f"App: {current_app.import_name}\n" f"Instance: {current_app.instance_path}" ) ctx: dict[str, t.Any] = {} # Support the regular Python interpreter startup script if someone # is using it. startup = os.environ.get("PYTHONSTARTUP") if startup and os.path.isfile(startup): with open(startup) as f: eval(compile(f.read(), startup, "exec"), ctx) ctx.update(current_app.make_shell_context()) # Site, customize, or startup script can set a hook to call when # entering interactive mode. The default one sets up readline with # tab and history completion. interactive_hook = getattr(sys, "__interactivehook__", None) if interactive_hook is not None: try: import readline from rlcompleter import Completer except ImportError: pass else: # rlcompleter uses __main__.__dict__ by default, which is # flask.__main__. Use the shell context instead. readline.set_completer(Completer(ctx).complete) interactive_hook() code.interact(banner=banner, local=ctx) @click.command("routes", short_help="Show the routes for the app.") @click.option( "--sort", "-s", type=click.Choice(("endpoint", "methods", "domain", "rule", "match")), default="endpoint", help=( "Method to sort routes by. 'match' is the order that Flask will match routes" " when dispatching a request." ), ) @click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") @with_appcontext def routes_command(sort: str, all_methods: bool) -> None: """Show all registered routes with endpoints and methods.""" rules = list(current_app.url_map.iter_rules()) if not rules: click.echo("No routes were registered.") return ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"} host_matching = current_app.url_map.host_matching has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules) rows = [] for rule in rules: row = [ rule.endpoint, ", ".join(sorted((rule.methods or set()) - ignored_methods)), ] if has_domain: row.append((rule.host if host_matching else rule.subdomain) or "") row.append(rule.rule) rows.append(row) headers = ["Endpoint", "Methods"] sorts = ["endpoint", "methods"] if has_domain: headers.append("Host" if host_matching else "Subdomain") sorts.append("domain") headers.append("Rule") sorts.append("rule") try: rows.sort(key=itemgetter(sorts.index(sort))) except ValueError: pass rows.insert(0, headers) widths = [max(len(row[i]) for row in rows) for i in range(len(headers))] rows.insert(1, ["-" * w for w in widths]) template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths)) for row in rows: click.echo(template.format(*row)) cli = FlaskGroup( name="flask", help="""\ A general utility script for Flask applications. An application to load must be given with the '--app' option, 'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file in the current directory. """, ) def main() -> None: cli.main() if __name__ == "__main__": main() flask-3.1.1/src/flask/config.py000066400000000000000000000316431501065614000163270ustar00rootroot00000000000000from __future__ import annotations import errno import json import os import types import typing as t from werkzeug.utils import import_string if t.TYPE_CHECKING: import typing_extensions as te from .sansio.app import App T = t.TypeVar("T") class ConfigAttribute(t.Generic[T]): """Makes an attribute forward to the config""" def __init__( self, name: str, get_converter: t.Callable[[t.Any], T] | None = None ) -> None: self.__name__ = name self.get_converter = get_converter @t.overload def __get__(self, obj: None, owner: None) -> te.Self: ... @t.overload def __get__(self, obj: App, owner: type[App]) -> T: ... def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self: if obj is None: return self rv = obj.config[self.__name__] if self.get_converter is not None: rv = self.get_converter(rv) return rv # type: ignore[no-any-return] def __set__(self, obj: App, value: t.Any) -> None: obj.config[self.__name__] = value class Config(dict): # type: ignore[type-arg] """Works exactly like a dict but provides ways to fill it from files or special dictionaries. There are two common patterns to populate the config. Either you can fill the config from a config file:: app.config.from_pyfile('yourconfig.cfg') Or alternatively you can define the configuration options in the module that calls :meth:`from_object` or provide an import path to a module that should be loaded. It is also possible to tell it to use the same module and with that provide the configuration values just before the call:: DEBUG = True SECRET_KEY = 'development key' app.config.from_object(__name__) In both cases (loading from any Python file or loading from modules), only uppercase keys are added to the config. This makes it possible to use lowercase values in the config file for temporary values that are not added to the config or to define the config keys in the same file that implements the application. Probably the most interesting way to load configurations is from an environment variable pointing to a file:: app.config.from_envvar('YOURAPPLICATION_SETTINGS') In this case before launching the application you have to set this environment variable to the file you want to use. On Linux and OS X use the export statement:: export YOURAPPLICATION_SETTINGS='/path/to/config/file' On windows use `set` instead. :param root_path: path to which files are read relative from. When the config object is created by the application, this is the application's :attr:`~flask.Flask.root_path`. :param defaults: an optional dictionary of default values """ def __init__( self, root_path: str | os.PathLike[str], defaults: dict[str, t.Any] | None = None, ) -> None: super().__init__(defaults or {}) self.root_path = root_path def from_envvar(self, variable_name: str, silent: bool = False) -> bool: """Loads a configuration from an environment variable pointing to a configuration file. This is basically just a shortcut with nicer error messages for this line of code:: app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) :param variable_name: name of the environment variable :param silent: set to ``True`` if you want silent failure for missing files. :return: ``True`` if the file was loaded successfully. """ rv = os.environ.get(variable_name) if not rv: if silent: return False raise RuntimeError( f"The environment variable {variable_name!r} is not set" " and as such configuration could not be loaded. Set" " this variable and make it point to a configuration" " file" ) return self.from_pyfile(rv, silent=silent) def from_prefixed_env( self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads ) -> bool: """Load any environment variables that start with ``FLASK_``, dropping the prefix from the env key for the config key. Values are passed through a loading function to attempt to convert them to more specific types than strings. Keys are loaded in :func:`sorted` order. The default loading function attempts to parse values as any valid JSON type, including dicts and lists. Specific items in nested dicts can be set by separating the keys with double underscores (``__``). If an intermediate key doesn't exist, it will be initialized to an empty dict. :param prefix: Load env vars that start with this prefix, separated with an underscore (``_``). :param loads: Pass each string value to this function and use the returned value as the config value. If any error is raised it is ignored and the value remains a string. The default is :func:`json.loads`. .. versionadded:: 2.1 """ prefix = f"{prefix}_" for key in sorted(os.environ): if not key.startswith(prefix): continue value = os.environ[key] key = key.removeprefix(prefix) try: value = loads(value) except Exception: # Keep the value as a string if loading failed. pass if "__" not in key: # A non-nested key, set directly. self[key] = value continue # Traverse nested dictionaries with keys separated by "__". current = self *parts, tail = key.split("__") for part in parts: # If an intermediate dict does not exist, create it. if part not in current: current[part] = {} current = current[part] current[tail] = value return True def from_pyfile( self, filename: str | os.PathLike[str], silent: bool = False ) -> bool: """Updates the values in the config from a Python file. This function behaves as if the file was imported as module with the :meth:`from_object` function. :param filename: the filename of the config. This can either be an absolute filename or a filename relative to the root path. :param silent: set to ``True`` if you want silent failure for missing files. :return: ``True`` if the file was loaded successfully. .. versionadded:: 0.7 `silent` parameter. """ filename = os.path.join(self.root_path, filename) d = types.ModuleType("config") d.__file__ = filename try: with open(filename, mode="rb") as config_file: exec(compile(config_file.read(), filename, "exec"), d.__dict__) except OSError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR): return False e.strerror = f"Unable to load configuration file ({e.strerror})" raise self.from_object(d) return True def from_object(self, obj: object | str) -> None: """Updates the values from the given object. An object can be of one of the following two types: - a string: in this case the object with that name will be imported - an actual object reference: that object is used directly Objects are usually either modules or classes. :meth:`from_object` loads only the uppercase attributes of the module/class. A ``dict`` object will not work with :meth:`from_object` because the keys of a ``dict`` are not attributes of the ``dict`` class. Example of module-based configuration:: app.config.from_object('yourapplication.default_config') from yourapplication import default_config app.config.from_object(default_config) Nothing is done to the object before loading. If the object is a class and has ``@property`` attributes, it needs to be instantiated before being passed to this method. You should not use this function to load the actual configuration but rather configuration defaults. The actual config should be loaded with :meth:`from_pyfile` and ideally from a location not within the package because the package might be installed system wide. See :ref:`config-dev-prod` for an example of class-based configuration using :meth:`from_object`. :param obj: an import name or object """ if isinstance(obj, str): obj = import_string(obj) for key in dir(obj): if key.isupper(): self[key] = getattr(obj, key) def from_file( self, filename: str | os.PathLike[str], load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]], silent: bool = False, text: bool = True, ) -> bool: """Update the values in the config from a file that is loaded using the ``load`` parameter. The loaded data is passed to the :meth:`from_mapping` method. .. code-block:: python import json app.config.from_file("config.json", load=json.load) import tomllib app.config.from_file("config.toml", load=tomllib.load, text=False) :param filename: The path to the data file. This can be an absolute path or relative to the config root path. :param load: A callable that takes a file handle and returns a mapping of loaded data from the file. :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` implements a ``read`` method. :param silent: Ignore the file if it doesn't exist. :param text: Open the file in text or binary mode. :return: ``True`` if the file was loaded successfully. .. versionchanged:: 2.3 The ``text`` parameter was added. .. versionadded:: 2.0 """ filename = os.path.join(self.root_path, filename) try: with open(filename, "r" if text else "rb") as f: obj = load(f) except OSError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR): return False e.strerror = f"Unable to load configuration file ({e.strerror})" raise return self.from_mapping(obj) def from_mapping( self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any ) -> bool: """Updates the config like :meth:`update` ignoring items with non-upper keys. :return: Always returns ``True``. .. versionadded:: 0.11 """ mappings: dict[str, t.Any] = {} if mapping is not None: mappings.update(mapping) mappings.update(kwargs) for key, value in mappings.items(): if key.isupper(): self[key] = value return True def get_namespace( self, namespace: str, lowercase: bool = True, trim_namespace: bool = True ) -> dict[str, t.Any]: """Returns a dictionary containing a subset of configuration options that match the specified namespace/prefix. Example usage:: app.config['IMAGE_STORE_TYPE'] = 'fs' app.config['IMAGE_STORE_PATH'] = '/var/app/images' app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' image_store_config = app.config.get_namespace('IMAGE_STORE_') The resulting dictionary `image_store_config` would look like:: { 'type': 'fs', 'path': '/var/app/images', 'base_url': 'http://img.website.com' } This is often useful when configuration options map directly to keyword arguments in functions or class constructors. :param namespace: a configuration namespace :param lowercase: a flag indicating if the keys of the resulting dictionary should be lowercase :param trim_namespace: a flag indicating if the keys of the resulting dictionary should not include the namespace .. versionadded:: 0.11 """ rv = {} for k, v in self.items(): if not k.startswith(namespace): continue if trim_namespace: key = k[len(namespace) :] else: key = k if lowercase: key = key.lower() rv[key] = v return rv def __repr__(self) -> str: return f"<{type(self).__name__} {dict.__repr__(self)}>" flask-3.1.1/src/flask/ctx.py000066400000000000000000000354201501065614000156550ustar00rootroot00000000000000from __future__ import annotations import contextvars import sys import typing as t from functools import update_wrapper from types import TracebackType from werkzeug.exceptions import HTTPException from . import typing as ft from .globals import _cv_app from .globals import _cv_request from .signals import appcontext_popped from .signals import appcontext_pushed if t.TYPE_CHECKING: # pragma: no cover from _typeshed.wsgi import WSGIEnvironment from .app import Flask from .sessions import SessionMixin from .wrappers import Request # a singleton sentinel value for parameter defaults _sentinel = object() class _AppCtxGlobals: """A plain object. Used as a namespace for storing data during an application context. Creating an app context automatically creates this object, which is made available as the :data:`g` proxy. .. describe:: 'key' in g Check whether an attribute is present. .. versionadded:: 0.10 .. describe:: iter(g) Return an iterator over the attribute names. .. versionadded:: 0.10 """ # Define attr methods to let mypy know this is a namespace object # that has arbitrary attributes. def __getattr__(self, name: str) -> t.Any: try: return self.__dict__[name] except KeyError: raise AttributeError(name) from None def __setattr__(self, name: str, value: t.Any) -> None: self.__dict__[name] = value def __delattr__(self, name: str) -> None: try: del self.__dict__[name] except KeyError: raise AttributeError(name) from None def get(self, name: str, default: t.Any | None = None) -> t.Any: """Get an attribute by name, or a default value. Like :meth:`dict.get`. :param name: Name of attribute to get. :param default: Value to return if the attribute is not present. .. versionadded:: 0.10 """ return self.__dict__.get(name, default) def pop(self, name: str, default: t.Any = _sentinel) -> t.Any: """Get and remove an attribute by name. Like :meth:`dict.pop`. :param name: Name of attribute to pop. :param default: Value to return if the attribute is not present, instead of raising a ``KeyError``. .. versionadded:: 0.11 """ if default is _sentinel: return self.__dict__.pop(name) else: return self.__dict__.pop(name, default) def setdefault(self, name: str, default: t.Any = None) -> t.Any: """Get the value of an attribute if it is present, otherwise set and return a default value. Like :meth:`dict.setdefault`. :param name: Name of attribute to get. :param default: Value to set and return if the attribute is not present. .. versionadded:: 0.11 """ return self.__dict__.setdefault(name, default) def __contains__(self, item: str) -> bool: return item in self.__dict__ def __iter__(self) -> t.Iterator[str]: return iter(self.__dict__) def __repr__(self) -> str: ctx = _cv_app.get(None) if ctx is not None: return f"" return object.__repr__(self) def after_this_request( f: ft.AfterRequestCallable[t.Any], ) -> ft.AfterRequestCallable[t.Any]: """Executes a function after this request. This is useful to modify response objects. The function is passed the response object and has to return the same or a new one. Example:: @app.route('/') def index(): @after_this_request def add_header(response): response.headers['X-Foo'] = 'Parachute' return response return 'Hello World!' This is more useful if a function other than the view function wants to modify a response. For instance think of a decorator that wants to add some headers without converting the return value into a response object. .. versionadded:: 0.9 """ ctx = _cv_request.get(None) if ctx is None: raise RuntimeError( "'after_this_request' can only be used when a request" " context is active, such as in a view function." ) ctx._after_request_functions.append(f) return f F = t.TypeVar("F", bound=t.Callable[..., t.Any]) def copy_current_request_context(f: F) -> F: """A helper function that decorates a function to retain the current request context. This is useful when working with greenlets. The moment the function is decorated a copy of the request context is created and then pushed when the function is called. The current session is also included in the copied request context. Example:: import gevent from flask import copy_current_request_context @app.route('/') def index(): @copy_current_request_context def do_some_work(): # do some work here, it can access flask.request or # flask.session like you would otherwise in the view function. ... gevent.spawn(do_some_work) return 'Regular response' .. versionadded:: 0.10 """ ctx = _cv_request.get(None) if ctx is None: raise RuntimeError( "'copy_current_request_context' can only be used when a" " request context is active, such as in a view function." ) ctx = ctx.copy() def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: with ctx: # type: ignore[union-attr] return ctx.app.ensure_sync(f)(*args, **kwargs) # type: ignore[union-attr] return update_wrapper(wrapper, f) # type: ignore[return-value] def has_request_context() -> bool: """If you have code that wants to test if a request context is there or not this function can be used. For instance, you may want to take advantage of request information if the request object is available, but fail silently if it is unavailable. :: class User(db.Model): def __init__(self, username, remote_addr=None): self.username = username if remote_addr is None and has_request_context(): remote_addr = request.remote_addr self.remote_addr = remote_addr Alternatively you can also just test any of the context bound objects (such as :class:`request` or :class:`g`) for truthness:: class User(db.Model): def __init__(self, username, remote_addr=None): self.username = username if remote_addr is None and request: remote_addr = request.remote_addr self.remote_addr = remote_addr .. versionadded:: 0.7 """ return _cv_request.get(None) is not None def has_app_context() -> bool: """Works like :func:`has_request_context` but for the application context. You can also just do a boolean check on the :data:`current_app` object instead. .. versionadded:: 0.9 """ return _cv_app.get(None) is not None class AppContext: """The app context contains application-specific information. An app context is created and pushed at the beginning of each request if one is not already active. An app context is also pushed when running CLI commands. """ def __init__(self, app: Flask) -> None: self.app = app self.url_adapter = app.create_url_adapter(None) self.g: _AppCtxGlobals = app.app_ctx_globals_class() self._cv_tokens: list[contextvars.Token[AppContext]] = [] def push(self) -> None: """Binds the app context to the current context.""" self._cv_tokens.append(_cv_app.set(self)) appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore """Pops the app context.""" try: if len(self._cv_tokens) == 1: if exc is _sentinel: exc = sys.exc_info()[1] self.app.do_teardown_appcontext(exc) finally: ctx = _cv_app.get() _cv_app.reset(self._cv_tokens.pop()) if ctx is not self: raise AssertionError( f"Popped wrong app context. ({ctx!r} instead of {self!r})" ) appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) def __enter__(self) -> AppContext: self.push() return self def __exit__( self, exc_type: type | None, exc_value: BaseException | None, tb: TracebackType | None, ) -> None: self.pop(exc_value) class RequestContext: """The request context contains per-request information. The Flask app creates and pushes it at the beginning of the request, then pops it at the end of the request. It will create the URL adapter and request object for the WSGI environment provided. Do not attempt to use this class directly, instead use :meth:`~flask.Flask.test_request_context` and :meth:`~flask.Flask.request_context` to create this object. When the request context is popped, it will evaluate all the functions registered on the application for teardown execution (:meth:`~flask.Flask.teardown_request`). The request context is automatically popped at the end of the request. When using the interactive debugger, the context will be restored so ``request`` is still accessible. Similarly, the test client can preserve the context after the request ends. However, teardown functions may already have closed some resources such as database connections. """ def __init__( self, app: Flask, environ: WSGIEnvironment, request: Request | None = None, session: SessionMixin | None = None, ) -> None: self.app = app if request is None: request = app.request_class(environ) request.json_module = app.json self.request: Request = request self.url_adapter = None try: self.url_adapter = app.create_url_adapter(self.request) except HTTPException as e: self.request.routing_exception = e self.flashes: list[tuple[str, str]] | None = None self.session: SessionMixin | None = session # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = [] self._cv_tokens: list[ tuple[contextvars.Token[RequestContext], AppContext | None] ] = [] def copy(self) -> RequestContext: """Creates a copy of this request context with the same request object. This can be used to move a request context to a different greenlet. Because the actual request object is the same this cannot be used to move a request context to a different thread unless access to the request object is locked. .. versionadded:: 0.10 .. versionchanged:: 1.1 The current session object is used instead of reloading the original data. This prevents `flask.session` pointing to an out-of-date object. """ return self.__class__( self.app, environ=self.request.environ, request=self.request, session=self.session, ) def match_request(self) -> None: """Can be overridden by a subclass to hook into the matching of the request. """ try: result = self.url_adapter.match(return_rule=True) # type: ignore self.request.url_rule, self.request.view_args = result # type: ignore except HTTPException as e: self.request.routing_exception = e def push(self) -> None: # Before we push the request context we have to ensure that there # is an application context. app_ctx = _cv_app.get(None) if app_ctx is None or app_ctx.app is not self.app: app_ctx = self.app.app_context() app_ctx.push() else: app_ctx = None self._cv_tokens.append((_cv_request.set(self), app_ctx)) # Open the session at the moment that the request context is available. # This allows a custom open_session method to use the request context. # Only open a new session if this is the first time the request was # pushed, otherwise stream_with_context loses the session. if self.session is None: session_interface = self.app.session_interface self.session = session_interface.open_session(self.app, self.request) if self.session is None: self.session = session_interface.make_null_session(self.app) # Match the request URL after loading the session, so that the # session is available in custom URL converters. if self.url_adapter is not None: self.match_request() def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore """Pops the request context and unbinds it by doing that. This will also trigger the execution of functions registered by the :meth:`~flask.Flask.teardown_request` decorator. .. versionchanged:: 0.9 Added the `exc` argument. """ clear_request = len(self._cv_tokens) == 1 try: if clear_request: if exc is _sentinel: exc = sys.exc_info()[1] self.app.do_teardown_request(exc) request_close = getattr(self.request, "close", None) if request_close is not None: request_close() finally: ctx = _cv_request.get() token, app_ctx = self._cv_tokens.pop() _cv_request.reset(token) # get rid of circular dependencies at the end of the request # so that we don't require the GC to be active. if clear_request: ctx.request.environ["werkzeug.request"] = None if app_ctx is not None: app_ctx.pop(exc) if ctx is not self: raise AssertionError( f"Popped wrong request context. ({ctx!r} instead of {self!r})" ) def __enter__(self) -> RequestContext: self.push() return self def __exit__( self, exc_type: type | None, exc_value: BaseException | None, tb: TracebackType | None, ) -> None: self.pop(exc_value) def __repr__(self) -> str: return ( f"<{type(self).__name__} {self.request.url!r}" f" [{self.request.method}] of {self.app.name}>" ) flask-3.1.1/src/flask/debughelpers.py000066400000000000000000000137001501065614000175250ustar00rootroot00000000000000from __future__ import annotations import typing as t from jinja2.loaders import BaseLoader from werkzeug.routing import RequestRedirect from .blueprints import Blueprint from .globals import request_ctx from .sansio.app import App if t.TYPE_CHECKING: from .sansio.scaffold import Scaffold from .wrappers import Request class UnexpectedUnicodeError(AssertionError, UnicodeError): """Raised in places where we want some better error reporting for unexpected unicode or binary data. """ class DebugFilesKeyError(KeyError, AssertionError): """Raised from request.files during debugging. The idea is that it can provide a better error message than just a generic KeyError/BadRequest. """ def __init__(self, request: Request, key: str) -> None: form_matches = request.form.getlist(key) buf = [ f"You tried to access the file {key!r} in the request.files" " dictionary but it does not exist. The mimetype for the" f" request is {request.mimetype!r} instead of" " 'multipart/form-data' which means that no file contents" " were transmitted. To fix this error you should provide" ' enctype="multipart/form-data" in your form.' ] if form_matches: names = ", ".join(repr(x) for x in form_matches) buf.append( "\n\nThe browser instead transmitted some file names. " f"This was submitted: {names}" ) self.msg = "".join(buf) def __str__(self) -> str: return self.msg class FormDataRoutingRedirect(AssertionError): """This exception is raised in debug mode if a routing redirect would cause the browser to drop the method or body. This happens when method is not GET, HEAD or OPTIONS and the status code is not 307 or 308. """ def __init__(self, request: Request) -> None: exc = request.routing_exception assert isinstance(exc, RequestRedirect) buf = [ f"A request was sent to '{request.url}', but routing issued" f" a redirect to the canonical URL '{exc.new_url}'." ] if f"{request.base_url}/" == exc.new_url.partition("?")[0]: buf.append( " The URL was defined with a trailing slash. Flask" " will redirect to the URL with a trailing slash if it" " was accessed without one." ) buf.append( " Send requests to the canonical URL, or use 307 or 308 for" " routing redirects. Otherwise, browsers will drop form" " data.\n\n" "This exception is only raised in debug mode." ) super().__init__("".join(buf)) def attach_enctype_error_multidict(request: Request) -> None: """Patch ``request.files.__getitem__`` to raise a descriptive error about ``enctype=multipart/form-data``. :param request: The request to patch. :meta private: """ oldcls = request.files.__class__ class newcls(oldcls): # type: ignore[valid-type, misc] def __getitem__(self, key: str) -> t.Any: try: return super().__getitem__(key) except KeyError as e: if key not in request.form: raise raise DebugFilesKeyError(request, key).with_traceback( e.__traceback__ ) from None newcls.__name__ = oldcls.__name__ newcls.__module__ = oldcls.__module__ request.files.__class__ = newcls def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]: yield f"class: {type(loader).__module__}.{type(loader).__name__}" for key, value in sorted(loader.__dict__.items()): if key.startswith("_"): continue if isinstance(value, (tuple, list)): if not all(isinstance(x, str) for x in value): continue yield f"{key}:" for item in value: yield f" - {item}" continue elif not isinstance(value, (str, int, float, bool)): continue yield f"{key}: {value!r}" def explain_template_loading_attempts( app: App, template: str, attempts: list[ tuple[ BaseLoader, Scaffold, tuple[str, str | None, t.Callable[[], bool] | None] | None, ] ], ) -> None: """This should help developers understand what failed""" info = [f"Locating template {template!r}:"] total_found = 0 blueprint = None if request_ctx and request_ctx.request.blueprint is not None: blueprint = request_ctx.request.blueprint for idx, (loader, srcobj, triple) in enumerate(attempts): if isinstance(srcobj, App): src_info = f"application {srcobj.import_name!r}" elif isinstance(srcobj, Blueprint): src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" else: src_info = repr(srcobj) info.append(f"{idx + 1:5}: trying loader of {src_info}") for line in _dump_loader_info(loader): info.append(f" {line}") if triple is None: detail = "no match" else: detail = f"found ({triple[1] or ''!r})" total_found += 1 info.append(f" -> {detail}") seems_fishy = False if total_found == 0: info.append("Error: the template could not be found.") seems_fishy = True elif total_found > 1: info.append("Warning: multiple loaders returned a match for the template.") seems_fishy = True if blueprint is not None and seems_fishy: info.append( " The template was looked up from an endpoint that belongs" f" to the blueprint {blueprint!r}." ) info.append(" Maybe you did not place a template in the right folder?") info.append(" See https://flask.palletsprojects.com/blueprints/#templates") app.logger.info("\n".join(info)) flask-3.1.1/src/flask/globals.py000066400000000000000000000032611501065614000165000ustar00rootroot00000000000000from __future__ import annotations import typing as t from contextvars import ContextVar from werkzeug.local import LocalProxy if t.TYPE_CHECKING: # pragma: no cover from .app import Flask from .ctx import _AppCtxGlobals from .ctx import AppContext from .ctx import RequestContext from .sessions import SessionMixin from .wrappers import Request _no_app_msg = """\ Working outside of application context. This typically means that you attempted to use functionality that needed the current application. To solve this, set up an application context with app.app_context(). See the documentation for more information.\ """ _cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") app_ctx: AppContext = LocalProxy( # type: ignore[assignment] _cv_app, unbound_message=_no_app_msg ) current_app: Flask = LocalProxy( # type: ignore[assignment] _cv_app, "app", unbound_message=_no_app_msg ) g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] _cv_app, "g", unbound_message=_no_app_msg ) _no_req_msg = """\ Working outside of request context. This typically means that you attempted to use functionality that needed an active HTTP request. Consult the documentation on testing for information about how to avoid this problem.\ """ _cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] _cv_request, unbound_message=_no_req_msg ) request: Request = LocalProxy( # type: ignore[assignment] _cv_request, "request", unbound_message=_no_req_msg ) session: SessionMixin = LocalProxy( # type: ignore[assignment] _cv_request, "session", unbound_message=_no_req_msg ) flask-3.1.1/src/flask/helpers.py000066400000000000000000000557411501065614000165310ustar00rootroot00000000000000from __future__ import annotations import importlib.util import os import sys import typing as t from datetime import datetime from functools import cache from functools import update_wrapper import werkzeug.utils from werkzeug.exceptions import abort as _wz_abort from werkzeug.utils import redirect as _wz_redirect from werkzeug.wrappers import Response as BaseResponse from .globals import _cv_request from .globals import current_app from .globals import request from .globals import request_ctx from .globals import session from .signals import message_flashed if t.TYPE_CHECKING: # pragma: no cover from .wrappers import Response def get_debug_flag() -> bool: """Get whether debug mode should be enabled for the app, indicated by the :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. """ val = os.environ.get("FLASK_DEBUG") return bool(val and val.lower() not in {"0", "false", "no"}) def get_load_dotenv(default: bool = True) -> bool: """Get whether the user has disabled loading default dotenv files by setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load the files. :param default: What to return if the env var isn't set. """ val = os.environ.get("FLASK_SKIP_DOTENV") if not val: return default return val.lower() in ("0", "false", "no") @t.overload def stream_with_context( generator_or_function: t.Iterator[t.AnyStr], ) -> t.Iterator[t.AnyStr]: ... @t.overload def stream_with_context( generator_or_function: t.Callable[..., t.Iterator[t.AnyStr]], ) -> t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: ... def stream_with_context( generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]], ) -> t.Iterator[t.AnyStr] | t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: """Request contexts disappear when the response is started on the server. This is done for efficiency reasons and to make it less likely to encounter memory leaks with badly written WSGI middlewares. The downside is that if you are using streamed responses, the generator cannot access request bound information any more. This function however can help you keep the context around for longer:: from flask import stream_with_context, request, Response @app.route('/stream') def streamed_response(): @stream_with_context def generate(): yield 'Hello ' yield request.args['name'] yield '!' return Response(generate()) Alternatively it can also be used around a specific generator:: from flask import stream_with_context, request, Response @app.route('/stream') def streamed_response(): def generate(): yield 'Hello ' yield request.args['name'] yield '!' return Response(stream_with_context(generate())) .. versionadded:: 0.9 """ try: gen = iter(generator_or_function) # type: ignore[arg-type] except TypeError: def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: gen = generator_or_function(*args, **kwargs) # type: ignore[operator] return stream_with_context(gen) return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type] def generator() -> t.Iterator[t.AnyStr | None]: ctx = _cv_request.get(None) if ctx is None: raise RuntimeError( "'stream_with_context' can only be used when a request" " context is active, such as in a view function." ) with ctx: # Dummy sentinel. Has to be inside the context block or we're # not actually keeping the context around. yield None # The try/finally is here so that if someone passes a WSGI level # iterator in we're still running the cleanup logic. Generators # don't need that because they are closed on their destruction # automatically. try: yield from gen finally: if hasattr(gen, "close"): gen.close() # The trick is to start the generator. Then the code execution runs until # the first dummy None is yielded at which point the context was already # pushed. This item is discarded. Then when the iteration continues the # real generator is executed. wrapped_g = generator() next(wrapped_g) return wrapped_g # type: ignore[return-value] def make_response(*args: t.Any) -> Response: """Sometimes it is necessary to set additional headers in a view. Because views do not have to return response objects but can return a value that is converted into a response object by Flask itself, it becomes tricky to add headers to it. This function can be called instead of using a return and you will get a response object which you can use to attach headers. If view looked like this and you want to add a new header:: def index(): return render_template('index.html', foo=42) You can now do something like this:: def index(): response = make_response(render_template('index.html', foo=42)) response.headers['X-Parachutes'] = 'parachutes are cool' return response This function accepts the very same arguments you can return from a view function. This for example creates a response with a 404 error code:: response = make_response(render_template('not_found.html'), 404) The other use case of this function is to force the return value of a view function into a response which is helpful with view decorators:: response = make_response(view_function()) response.headers['X-Parachutes'] = 'parachutes are cool' Internally this function does the following things: - if no arguments are passed, it creates a new response argument - if one argument is passed, :meth:`flask.Flask.make_response` is invoked with it. - if more than one argument is passed, the arguments are passed to the :meth:`flask.Flask.make_response` function as tuple. .. versionadded:: 0.6 """ if not args: return current_app.response_class() if len(args) == 1: args = args[0] return current_app.make_response(args) def url_for( endpoint: str, *, _anchor: str | None = None, _method: str | None = None, _scheme: str | None = None, _external: bool | None = None, **values: t.Any, ) -> str: """Generate a URL to the given endpoint with the given values. This requires an active request or application context, and calls :meth:`current_app.url_for() `. See that method for full documentation. :param endpoint: The endpoint name associated with the URL to generate. If this starts with a ``.``, the current blueprint name (if any) will be used. :param _anchor: If given, append this as ``#anchor`` to the URL. :param _method: If given, generate the URL associated with this method for the endpoint. :param _scheme: If given, the URL will have this scheme if it is external. :param _external: If given, prefer the URL to be internal (False) or require it to be external (True). External URLs include the scheme and domain. When not in an active request, URLs are external by default. :param values: Values to use for the variable parts of the URL rule. Unknown keys are appended as query string arguments, like ``?a=b&c=d``. .. versionchanged:: 2.2 Calls ``current_app.url_for``, allowing an app to override the behavior. .. versionchanged:: 0.10 The ``_scheme`` parameter was added. .. versionchanged:: 0.9 The ``_anchor`` and ``_method`` parameters were added. .. versionchanged:: 0.9 Calls ``app.handle_url_build_error`` on build errors. """ return current_app.url_for( endpoint, _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external, **values, ) def redirect( location: str, code: int = 302, Response: type[BaseResponse] | None = None ) -> BaseResponse: """Create a redirect response object. If :data:`~flask.current_app` is available, it will use its :meth:`~flask.Flask.redirect` method, otherwise it will use :func:`werkzeug.utils.redirect`. :param location: The URL to redirect to. :param code: The status code for the redirect. :param Response: The response class to use. Not used when ``current_app`` is active, which uses ``app.response_class``. .. versionadded:: 2.2 Calls ``current_app.redirect`` if available instead of always using Werkzeug's default ``redirect``. """ if current_app: return current_app.redirect(location, code=code) return _wz_redirect(location, code=code, Response=Response) def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given status code. If :data:`~flask.current_app` is available, it will call its :attr:`~flask.Flask.aborter` object, otherwise it will use :func:`werkzeug.exceptions.abort`. :param code: The status code for the exception, which must be registered in ``app.aborter``. :param args: Passed to the exception. :param kwargs: Passed to the exception. .. versionadded:: 2.2 Calls ``current_app.aborter`` if available instead of always using Werkzeug's default ``abort``. """ if current_app: current_app.aborter(code, *args, **kwargs) _wz_abort(code, *args, **kwargs) def get_template_attribute(template_name: str, attribute: str) -> t.Any: """Loads a macro (or variable) a template exports. This can be used to invoke a macro from within Python code. If you for example have a template named :file:`_cider.html` with the following contents: .. sourcecode:: html+jinja {% macro hello(name) %}Hello {{ name }}!{% endmacro %} You can access this from Python code like this:: hello = get_template_attribute('_cider.html', 'hello') return hello('World') .. versionadded:: 0.2 :param template_name: the name of the template :param attribute: the name of the variable of macro to access """ return getattr(current_app.jinja_env.get_template(template_name).module, attribute) def flash(message: str, category: str = "message") -> None: """Flashes a message to the next request. In order to remove the flashed message from the session and to display it to the user, the template has to call :func:`get_flashed_messages`. .. versionchanged:: 0.3 `category` parameter added. :param message: the message to be flashed. :param category: the category for the message. The following values are recommended: ``'message'`` for any kind of message, ``'error'`` for errors, ``'info'`` for information messages and ``'warning'`` for warnings. However any kind of string can be used as category. """ # Original implementation: # # session.setdefault('_flashes', []).append((category, message)) # # This assumed that changes made to mutable structures in the session are # always in sync with the session object, which is not true for session # implementations that use external storage for keeping their keys/values. flashes = session.get("_flashes", []) flashes.append((category, message)) session["_flashes"] = flashes app = current_app._get_current_object() # type: ignore message_flashed.send( app, _async_wrapper=app.ensure_sync, message=message, category=category, ) def get_flashed_messages( with_categories: bool = False, category_filter: t.Iterable[str] = () ) -> list[str] | list[tuple[str, str]]: """Pulls all flashed messages from the session and returns them. Further calls in the same request to the function will return the same messages. By default just the messages are returned, but when `with_categories` is set to ``True``, the return value will be a list of tuples in the form ``(category, message)`` instead. Filter the flashed messages to one or more categories by providing those categories in `category_filter`. This allows rendering categories in separate html blocks. The `with_categories` and `category_filter` arguments are distinct: * `with_categories` controls whether categories are returned with message text (``True`` gives a tuple, where ``False`` gives just the message text). * `category_filter` filters the messages down to only those matching the provided categories. See :doc:`/patterns/flashing` for examples. .. versionchanged:: 0.3 `with_categories` parameter added. .. versionchanged:: 0.9 `category_filter` parameter added. :param with_categories: set to ``True`` to also receive categories. :param category_filter: filter of categories to limit return values. Only categories in the list will be returned. """ flashes = request_ctx.flashes if flashes is None: flashes = session.pop("_flashes") if "_flashes" in session else [] request_ctx.flashes = flashes if category_filter: flashes = list(filter(lambda f: f[0] in category_filter, flashes)) if not with_categories: return [x[1] for x in flashes] return flashes def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: if kwargs.get("max_age") is None: kwargs["max_age"] = current_app.get_send_file_max_age kwargs.update( environ=request.environ, use_x_sendfile=current_app.config["USE_X_SENDFILE"], response_class=current_app.response_class, _root_path=current_app.root_path, # type: ignore ) return kwargs def send_file( path_or_file: os.PathLike[t.AnyStr] | str | t.BinaryIO, mimetype: str | None = None, as_attachment: bool = False, download_name: str | None = None, conditional: bool = True, etag: bool | str = True, last_modified: datetime | int | float | None = None, max_age: None | (int | t.Callable[[str | None], int | None]) = None, ) -> Response: """Send the contents of a file to the client. The first argument can be a file path or a file-like object. Paths are preferred in most cases because Werkzeug can manage the file and get extra information from the path. Passing a file-like object requires that the file is opened in binary mode, and is mostly useful when building a file in memory with :class:`io.BytesIO`. Never pass file paths provided by a user. The path is assumed to be trusted, so a user could craft a path to access a file you didn't intend. Use :func:`send_from_directory` to safely serve user-requested paths from within a directory. If the WSGI server sets a ``file_wrapper`` in ``environ``, it is used, otherwise Werkzeug's built-in wrapper is used. Alternatively, if the HTTP server supports ``X-Sendfile``, configuring Flask with ``USE_X_SENDFILE = True`` will tell the server to send the given path, which is much more efficient than reading it in Python. :param path_or_file: The path to the file to send, relative to the current working directory if a relative path is given. Alternatively, a file-like object opened in binary mode. Make sure the file pointer is seeked to the start of the data. :param mimetype: The MIME type to send for the file. If not provided, it will try to detect it from the file name. :param as_attachment: Indicate to a browser that it should offer to save the file instead of displaying it. :param download_name: The default name browsers will use when saving the file. Defaults to the passed file name. :param conditional: Enable conditional and range responses based on request headers. Requires passing a file path and ``environ``. :param etag: Calculate an ETag for the file, which requires passing a file path. Can also be a string to use instead. :param last_modified: The last modified time to send for the file, in seconds. If not provided, it will try to detect it from the file path. :param max_age: How long the client should cache the file, in seconds. If set, ``Cache-Control`` will be ``public``, otherwise it will be ``no-cache`` to prefer conditional caching. .. versionchanged:: 2.0 ``download_name`` replaces the ``attachment_filename`` parameter. If ``as_attachment=False``, it is passed with ``Content-Disposition: inline`` instead. .. versionchanged:: 2.0 ``max_age`` replaces the ``cache_timeout`` parameter. ``conditional`` is enabled and ``max_age`` is not set by default. .. versionchanged:: 2.0 ``etag`` replaces the ``add_etags`` parameter. It can be a string to use instead of generating one. .. versionchanged:: 2.0 Passing a file-like object that inherits from :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather than sending an empty file. .. versionadded:: 2.0 Moved the implementation to Werkzeug. This is now a wrapper to pass some Flask-specific arguments. .. versionchanged:: 1.1 ``filename`` may be a :class:`~os.PathLike` object. .. versionchanged:: 1.1 Passing a :class:`~io.BytesIO` object supports range requests. .. versionchanged:: 1.0.3 Filenames are encoded with ASCII instead of Latin-1 for broader compatibility with WSGI servers. .. versionchanged:: 1.0 UTF-8 filenames as specified in :rfc:`2231` are supported. .. versionchanged:: 0.12 The filename is no longer automatically inferred from file objects. If you want to use automatic MIME and etag support, pass a filename via ``filename_or_fp`` or ``attachment_filename``. .. versionchanged:: 0.12 ``attachment_filename`` is preferred over ``filename`` for MIME detection. .. versionchanged:: 0.9 ``cache_timeout`` defaults to :meth:`Flask.get_send_file_max_age`. .. versionchanged:: 0.7 MIME guessing and etag support for file-like objects was removed because it was unreliable. Pass a filename if you are able to, otherwise attach an etag yourself. .. versionchanged:: 0.5 The ``add_etags``, ``cache_timeout`` and ``conditional`` parameters were added. The default behavior is to add etags. .. versionadded:: 0.2 """ return werkzeug.utils.send_file( # type: ignore[return-value] **_prepare_send_file_kwargs( path_or_file=path_or_file, environ=request.environ, mimetype=mimetype, as_attachment=as_attachment, download_name=download_name, conditional=conditional, etag=etag, last_modified=last_modified, max_age=max_age, ) ) def send_from_directory( directory: os.PathLike[str] | str, path: os.PathLike[str] | str, **kwargs: t.Any, ) -> Response: """Send a file from within a directory using :func:`send_file`. .. code-block:: python @app.route("/uploads/") def download_file(name): return send_from_directory( app.config['UPLOAD_FOLDER'], name, as_attachment=True ) This is a secure way to serve files from a folder, such as static files or uploads. Uses :func:`~werkzeug.security.safe_join` to ensure the path coming from the client is not maliciously crafted to point outside the specified directory. If the final path does not point to an existing regular file, raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. :param directory: The directory that ``path`` must be located under, relative to the current application's root path. This *must not* be a value provided by the client, otherwise it becomes insecure. :param path: The path to the file to send, relative to ``directory``. :param kwargs: Arguments to pass to :func:`send_file`. .. versionchanged:: 2.0 ``path`` replaces the ``filename`` parameter. .. versionadded:: 2.0 Moved the implementation to Werkzeug. This is now a wrapper to pass some Flask-specific arguments. .. versionadded:: 0.5 """ return werkzeug.utils.send_from_directory( # type: ignore[return-value] directory, path, **_prepare_send_file_kwargs(**kwargs) ) def get_root_path(import_name: str) -> str: """Find the root path of a package, or the path that contains a module. If it cannot be found, returns the current working directory. Not to be confused with the value returned by :func:`find_package`. :meta private: """ # Module already imported and has a file attribute. Use that first. mod = sys.modules.get(import_name) if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None: return os.path.dirname(os.path.abspath(mod.__file__)) # Next attempt: check the loader. try: spec = importlib.util.find_spec(import_name) if spec is None: raise ValueError except (ImportError, ValueError): loader = None else: loader = spec.loader # Loader does not exist or we're referring to an unloaded main # module or a main module without path (interactive sessions), go # with the current working directory. if loader is None: return os.getcwd() if hasattr(loader, "get_filename"): filepath = loader.get_filename(import_name) # pyright: ignore else: # Fall back to imports. __import__(import_name) mod = sys.modules[import_name] filepath = getattr(mod, "__file__", None) # If we don't have a file path it might be because it is a # namespace package. In this case pick the root path from the # first module that is contained in the package. if filepath is None: raise RuntimeError( "No root path can be found for the provided module" f" {import_name!r}. This can happen because the module" " came from an import hook that does not provide file" " name information or because it's a namespace package." " In this case the root path needs to be explicitly" " provided." ) # filepath is import_name.py for a module, or __init__.py for a package. return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return] @cache def _split_blueprint_path(name: str) -> list[str]: out: list[str] = [name] if "." in name: out.extend(_split_blueprint_path(name.rpartition(".")[0])) return out flask-3.1.1/src/flask/json/000077500000000000000000000000001501065614000154525ustar00rootroot00000000000000flask-3.1.1/src/flask/json/__init__.py000066400000000000000000000127421501065614000175710ustar00rootroot00000000000000from __future__ import annotations import json as _json import typing as t from ..globals import current_app from .provider import _default if t.TYPE_CHECKING: # pragma: no cover from ..wrappers import Response def dumps(obj: t.Any, **kwargs: t.Any) -> str: """Serialize data as JSON. If :data:`~flask.current_app` is available, it will use its :meth:`app.json.dumps() ` method, otherwise it will use :func:`json.dumps`. :param obj: The data to serialize. :param kwargs: Arguments passed to the ``dumps`` implementation. .. versionchanged:: 2.3 The ``app`` parameter was removed. .. versionchanged:: 2.2 Calls ``current_app.json.dumps``, allowing an app to override the behavior. .. versionchanged:: 2.0.2 :class:`decimal.Decimal` is supported by converting to a string. .. versionchanged:: 2.0 ``encoding`` will be removed in Flask 2.1. .. versionchanged:: 1.0.3 ``app`` can be passed directly, rather than requiring an app context for configuration. """ if current_app: return current_app.json.dumps(obj, **kwargs) kwargs.setdefault("default", _default) return _json.dumps(obj, **kwargs) def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: """Serialize data as JSON and write to a file. If :data:`~flask.current_app` is available, it will use its :meth:`app.json.dump() ` method, otherwise it will use :func:`json.dump`. :param obj: The data to serialize. :param fp: A file opened for writing text. Should use the UTF-8 encoding to be valid JSON. :param kwargs: Arguments passed to the ``dump`` implementation. .. versionchanged:: 2.3 The ``app`` parameter was removed. .. versionchanged:: 2.2 Calls ``current_app.json.dump``, allowing an app to override the behavior. .. versionchanged:: 2.0 Writing to a binary file, and the ``encoding`` argument, will be removed in Flask 2.1. """ if current_app: current_app.json.dump(obj, fp, **kwargs) else: kwargs.setdefault("default", _default) _json.dump(obj, fp, **kwargs) def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: """Deserialize data as JSON. If :data:`~flask.current_app` is available, it will use its :meth:`app.json.loads() ` method, otherwise it will use :func:`json.loads`. :param s: Text or UTF-8 bytes. :param kwargs: Arguments passed to the ``loads`` implementation. .. versionchanged:: 2.3 The ``app`` parameter was removed. .. versionchanged:: 2.2 Calls ``current_app.json.loads``, allowing an app to override the behavior. .. versionchanged:: 2.0 ``encoding`` will be removed in Flask 2.1. The data must be a string or UTF-8 bytes. .. versionchanged:: 1.0.3 ``app`` can be passed directly, rather than requiring an app context for configuration. """ if current_app: return current_app.json.loads(s, **kwargs) return _json.loads(s, **kwargs) def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: """Deserialize data as JSON read from a file. If :data:`~flask.current_app` is available, it will use its :meth:`app.json.load() ` method, otherwise it will use :func:`json.load`. :param fp: A file opened for reading text or UTF-8 bytes. :param kwargs: Arguments passed to the ``load`` implementation. .. versionchanged:: 2.3 The ``app`` parameter was removed. .. versionchanged:: 2.2 Calls ``current_app.json.load``, allowing an app to override the behavior. .. versionchanged:: 2.2 The ``app`` parameter will be removed in Flask 2.3. .. versionchanged:: 2.0 ``encoding`` will be removed in Flask 2.1. The file must be text mode, or binary mode with UTF-8 bytes. """ if current_app: return current_app.json.load(fp, **kwargs) return _json.load(fp, **kwargs) def jsonify(*args: t.Any, **kwargs: t.Any) -> Response: """Serialize the given arguments as JSON, and return a :class:`~flask.Response` object with the ``application/json`` mimetype. A dict or list returned from a view will be converted to a JSON response automatically without needing to call this. This requires an active request or application context, and calls :meth:`app.json.response() `. In debug mode, the output is formatted with indentation to make it easier to read. This may also be controlled by the provider. Either positional or keyword arguments can be given, not both. If no arguments are given, ``None`` is serialized. :param args: A single value to serialize, or multiple values to treat as a list to serialize. :param kwargs: Treat as a dict to serialize. .. versionchanged:: 2.2 Calls ``current_app.json.response``, allowing an app to override the behavior. .. versionchanged:: 2.0.2 :class:`decimal.Decimal` is supported by converting to a string. .. versionchanged:: 0.11 Added support for serializing top-level arrays. This was a security risk in ancient browsers. See :ref:`security-json`. .. versionadded:: 0.2 """ return current_app.json.response(*args, **kwargs) # type: ignore[return-value] flask-3.1.1/src/flask/json/provider.py000066400000000000000000000167701501065614000176710ustar00rootroot00000000000000from __future__ import annotations import dataclasses import decimal import json import typing as t import uuid import weakref from datetime import date from werkzeug.http import http_date if t.TYPE_CHECKING: # pragma: no cover from werkzeug.sansio.response import Response from ..sansio.app import App class JSONProvider: """A standard set of JSON operations for an application. Subclasses of this can be used to customize JSON behavior or use different JSON libraries. To implement a provider for a specific library, subclass this base class and implement at least :meth:`dumps` and :meth:`loads`. All other methods have default implementations. To use a different provider, either subclass ``Flask`` and set :attr:`~flask.Flask.json_provider_class` to a provider class, or set :attr:`app.json ` to an instance of the class. :param app: An application instance. This will be stored as a :class:`weakref.proxy` on the :attr:`_app` attribute. .. versionadded:: 2.2 """ def __init__(self, app: App) -> None: self._app: App = weakref.proxy(app) def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: """Serialize data as JSON. :param obj: The data to serialize. :param kwargs: May be passed to the underlying JSON library. """ raise NotImplementedError def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: """Serialize data as JSON and write to a file. :param obj: The data to serialize. :param fp: A file opened for writing text. Should use the UTF-8 encoding to be valid JSON. :param kwargs: May be passed to the underlying JSON library. """ fp.write(self.dumps(obj, **kwargs)) def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: """Deserialize data as JSON. :param s: Text or UTF-8 bytes. :param kwargs: May be passed to the underlying JSON library. """ raise NotImplementedError def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: """Deserialize data as JSON read from a file. :param fp: A file opened for reading text or UTF-8 bytes. :param kwargs: May be passed to the underlying JSON library. """ return self.loads(fp.read(), **kwargs) def _prepare_response_obj( self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] ) -> t.Any: if args and kwargs: raise TypeError("app.json.response() takes either args or kwargs, not both") if not args and not kwargs: return None if len(args) == 1: return args[0] return args or kwargs def response(self, *args: t.Any, **kwargs: t.Any) -> Response: """Serialize the given arguments as JSON, and return a :class:`~flask.Response` object with the ``application/json`` mimetype. The :func:`~flask.json.jsonify` function calls this method for the current application. Either positional or keyword arguments can be given, not both. If no arguments are given, ``None`` is serialized. :param args: A single value to serialize, or multiple values to treat as a list to serialize. :param kwargs: Treat as a dict to serialize. """ obj = self._prepare_response_obj(args, kwargs) return self._app.response_class(self.dumps(obj), mimetype="application/json") def _default(o: t.Any) -> t.Any: if isinstance(o, date): return http_date(o) if isinstance(o, (decimal.Decimal, uuid.UUID)): return str(o) if dataclasses and dataclasses.is_dataclass(o): return dataclasses.asdict(o) # type: ignore[arg-type] if hasattr(o, "__html__"): return str(o.__html__()) raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable") class DefaultJSONProvider(JSONProvider): """Provide JSON operations using Python's built-in :mod:`json` library. Serializes the following additional data types: - :class:`datetime.datetime` and :class:`datetime.date` are serialized to :rfc:`822` strings. This is the same as the HTTP date format. - :class:`uuid.UUID` is serialized to a string. - :class:`dataclasses.dataclass` is passed to :func:`dataclasses.asdict`. - :class:`~markupsafe.Markup` (or any object with a ``__html__`` method) will call the ``__html__`` method to get a string. """ default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment] """Apply this function to any object that :meth:`json.dumps` does not know how to serialize. It should return a valid JSON type or raise a ``TypeError``. """ ensure_ascii = True """Replace non-ASCII characters with escape sequences. This may be more compatible with some clients, but can be disabled for better performance and size. """ sort_keys = True """Sort the keys in any serialized dicts. This may be useful for some caching situations, but can be disabled for better performance. When enabled, keys must all be strings, they are not converted before sorting. """ compact: bool | None = None """If ``True``, or ``None`` out of debug mode, the :meth:`response` output will not add indentation, newlines, or spaces. If ``False``, or ``None`` in debug mode, it will use a non-compact representation. """ mimetype = "application/json" """The mimetype set in :meth:`response`.""" def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: """Serialize data as JSON to a string. Keyword arguments are passed to :func:`json.dumps`. Sets some parameter defaults from the :attr:`default`, :attr:`ensure_ascii`, and :attr:`sort_keys` attributes. :param obj: The data to serialize. :param kwargs: Passed to :func:`json.dumps`. """ kwargs.setdefault("default", self.default) kwargs.setdefault("ensure_ascii", self.ensure_ascii) kwargs.setdefault("sort_keys", self.sort_keys) return json.dumps(obj, **kwargs) def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: """Deserialize data as JSON from a string or bytes. :param s: Text or UTF-8 bytes. :param kwargs: Passed to :func:`json.loads`. """ return json.loads(s, **kwargs) def response(self, *args: t.Any, **kwargs: t.Any) -> Response: """Serialize the given arguments as JSON, and return a :class:`~flask.Response` object with it. The response mimetype will be "application/json" and can be changed with :attr:`mimetype`. If :attr:`compact` is ``False`` or debug mode is enabled, the output will be formatted to be easier to read. Either positional or keyword arguments can be given, not both. If no arguments are given, ``None`` is serialized. :param args: A single value to serialize, or multiple values to treat as a list to serialize. :param kwargs: Treat as a dict to serialize. """ obj = self._prepare_response_obj(args, kwargs) dump_args: dict[str, t.Any] = {} if (self.compact is None and self._app.debug) or self.compact is False: dump_args.setdefault("indent", 2) else: dump_args.setdefault("separators", (",", ":")) return self._app.response_class( f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype ) flask-3.1.1/src/flask/json/tag.py000066400000000000000000000221011501065614000165730ustar00rootroot00000000000000""" Tagged JSON ~~~~~~~~~~~ A compact representation for lossless serialization of non-standard JSON types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this to serialize the session data, but it may be useful in other places. It can be extended to support other types. .. autoclass:: TaggedJSONSerializer :members: .. autoclass:: JSONTag :members: Let's see an example that adds support for :class:`~collections.OrderedDict`. Dicts don't have an order in JSON, so to handle this we will dump the items as a list of ``[key, value]`` pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to identify the type. The session serializer processes dicts first, so insert the new tag at the front of the order since ``OrderedDict`` must be processed before ``dict``. .. code-block:: python from flask.json.tag import JSONTag class TagOrderedDict(JSONTag): __slots__ = ('serializer',) key = ' od' def check(self, value): return isinstance(value, OrderedDict) def to_json(self, value): return [[k, self.serializer.tag(v)] for k, v in iteritems(value)] def to_python(self, value): return OrderedDict(value) app.session_interface.serializer.register(TagOrderedDict, index=0) """ from __future__ import annotations import typing as t from base64 import b64decode from base64 import b64encode from datetime import datetime from uuid import UUID from markupsafe import Markup from werkzeug.http import http_date from werkzeug.http import parse_date from ..json import dumps from ..json import loads class JSONTag: """Base class for defining type tags for :class:`TaggedJSONSerializer`.""" __slots__ = ("serializer",) #: The tag to mark the serialized object with. If empty, this tag is #: only used as an intermediate step during tagging. key: str = "" def __init__(self, serializer: TaggedJSONSerializer) -> None: """Create a tagger for the given serializer.""" self.serializer = serializer def check(self, value: t.Any) -> bool: """Check if the given value should be tagged by this tag.""" raise NotImplementedError def to_json(self, value: t.Any) -> t.Any: """Convert the Python object to an object that is a valid JSON type. The tag will be added later.""" raise NotImplementedError def to_python(self, value: t.Any) -> t.Any: """Convert the JSON representation back to the correct type. The tag will already be removed.""" raise NotImplementedError def tag(self, value: t.Any) -> dict[str, t.Any]: """Convert the value to a valid JSON type and add the tag structure around it.""" return {self.key: self.to_json(value)} class TagDict(JSONTag): """Tag for 1-item dicts whose only key matches a registered tag. Internally, the dict key is suffixed with `__`, and the suffix is removed when deserializing. """ __slots__ = () key = " di" def check(self, value: t.Any) -> bool: return ( isinstance(value, dict) and len(value) == 1 and next(iter(value)) in self.serializer.tags ) def to_json(self, value: t.Any) -> t.Any: key = next(iter(value)) return {f"{key}__": self.serializer.tag(value[key])} def to_python(self, value: t.Any) -> t.Any: key = next(iter(value)) return {key[:-2]: value[key]} class PassDict(JSONTag): __slots__ = () def check(self, value: t.Any) -> bool: return isinstance(value, dict) def to_json(self, value: t.Any) -> t.Any: # JSON objects may only have string keys, so don't bother tagging the # key here. return {k: self.serializer.tag(v) for k, v in value.items()} tag = to_json class TagTuple(JSONTag): __slots__ = () key = " t" def check(self, value: t.Any) -> bool: return isinstance(value, tuple) def to_json(self, value: t.Any) -> t.Any: return [self.serializer.tag(item) for item in value] def to_python(self, value: t.Any) -> t.Any: return tuple(value) class PassList(JSONTag): __slots__ = () def check(self, value: t.Any) -> bool: return isinstance(value, list) def to_json(self, value: t.Any) -> t.Any: return [self.serializer.tag(item) for item in value] tag = to_json class TagBytes(JSONTag): __slots__ = () key = " b" def check(self, value: t.Any) -> bool: return isinstance(value, bytes) def to_json(self, value: t.Any) -> t.Any: return b64encode(value).decode("ascii") def to_python(self, value: t.Any) -> t.Any: return b64decode(value) class TagMarkup(JSONTag): """Serialize anything matching the :class:`~markupsafe.Markup` API by having a ``__html__`` method to the result of that method. Always deserializes to an instance of :class:`~markupsafe.Markup`.""" __slots__ = () key = " m" def check(self, value: t.Any) -> bool: return callable(getattr(value, "__html__", None)) def to_json(self, value: t.Any) -> t.Any: return str(value.__html__()) def to_python(self, value: t.Any) -> t.Any: return Markup(value) class TagUUID(JSONTag): __slots__ = () key = " u" def check(self, value: t.Any) -> bool: return isinstance(value, UUID) def to_json(self, value: t.Any) -> t.Any: return value.hex def to_python(self, value: t.Any) -> t.Any: return UUID(value) class TagDateTime(JSONTag): __slots__ = () key = " d" def check(self, value: t.Any) -> bool: return isinstance(value, datetime) def to_json(self, value: t.Any) -> t.Any: return http_date(value) def to_python(self, value: t.Any) -> t.Any: return parse_date(value) class TaggedJSONSerializer: """Serializer that uses a tag system to compactly represent objects that are not JSON types. Passed as the intermediate serializer to :class:`itsdangerous.Serializer`. The following extra types are supported: * :class:`dict` * :class:`tuple` * :class:`bytes` * :class:`~markupsafe.Markup` * :class:`~uuid.UUID` * :class:`~datetime.datetime` """ __slots__ = ("tags", "order") #: Tag classes to bind when creating the serializer. Other tags can be #: added later using :meth:`~register`. default_tags = [ TagDict, PassDict, TagTuple, PassList, TagBytes, TagMarkup, TagUUID, TagDateTime, ] def __init__(self) -> None: self.tags: dict[str, JSONTag] = {} self.order: list[JSONTag] = [] for cls in self.default_tags: self.register(cls) def register( self, tag_class: type[JSONTag], force: bool = False, index: int | None = None, ) -> None: """Register a new tag with this serializer. :param tag_class: tag class to register. Will be instantiated with this serializer instance. :param force: overwrite an existing tag. If false (default), a :exc:`KeyError` is raised. :param index: index to insert the new tag in the tag order. Useful when the new tag is a special case of an existing tag. If ``None`` (default), the tag is appended to the end of the order. :raise KeyError: if the tag key is already registered and ``force`` is not true. """ tag = tag_class(self) key = tag.key if key: if not force and key in self.tags: raise KeyError(f"Tag '{key}' is already registered.") self.tags[key] = tag if index is None: self.order.append(tag) else: self.order.insert(index, tag) def tag(self, value: t.Any) -> t.Any: """Convert a value to a tagged representation if necessary.""" for tag in self.order: if tag.check(value): return tag.tag(value) return value def untag(self, value: dict[str, t.Any]) -> t.Any: """Convert a tagged representation back to the original type.""" if len(value) != 1: return value key = next(iter(value)) if key not in self.tags: return value return self.tags[key].to_python(value[key]) def _untag_scan(self, value: t.Any) -> t.Any: if isinstance(value, dict): # untag each item recursively value = {k: self._untag_scan(v) for k, v in value.items()} # untag the dict itself value = self.untag(value) elif isinstance(value, list): # untag each item recursively value = [self._untag_scan(item) for item in value] return value def dumps(self, value: t.Any) -> str: """Tag the value and dump it to a compact JSON string.""" return dumps(self.tag(value), separators=(",", ":")) def loads(self, value: str) -> t.Any: """Load data from a JSON string and deserialized any tagged objects.""" return self._untag_scan(loads(value)) flask-3.1.1/src/flask/logging.py000066400000000000000000000045111501065614000165020ustar00rootroot00000000000000from __future__ import annotations import logging import sys import typing as t from werkzeug.local import LocalProxy from .globals import request if t.TYPE_CHECKING: # pragma: no cover from .sansio.app import App @LocalProxy def wsgi_errors_stream() -> t.TextIO: """Find the most appropriate error stream for the application. If a request is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``. If you configure your own :class:`logging.StreamHandler`, you may want to use this for the stream. If you are using file or dict configuration and can't import this directly, you can refer to it as ``ext://flask.logging.wsgi_errors_stream``. """ if request: return request.environ["wsgi.errors"] # type: ignore[no-any-return] return sys.stderr def has_level_handler(logger: logging.Logger) -> bool: """Check if there is a handler in the logging chain that will handle the given logger's :meth:`effective level <~logging.Logger.getEffectiveLevel>`. """ level = logger.getEffectiveLevel() current = logger while current: if any(handler.level <= level for handler in current.handlers): return True if not current.propagate: break current = current.parent # type: ignore return False #: Log messages to :func:`~flask.logging.wsgi_errors_stream` with the format #: ``[%(asctime)s] %(levelname)s in %(module)s: %(message)s``. default_handler = logging.StreamHandler(wsgi_errors_stream) # type: ignore default_handler.setFormatter( logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s") ) def create_logger(app: App) -> logging.Logger: """Get the Flask app's logger and configure it if needed. The logger name will be the same as :attr:`app.import_name `. When :attr:`~flask.Flask.debug` is enabled, set the logger level to :data:`logging.DEBUG` if it is not set. If there is no handler for the logger's effective level, add a :class:`~logging.StreamHandler` for :func:`~flask.logging.wsgi_errors_stream` with a basic format. """ logger = logging.getLogger(app.name) if app.debug and not logger.level: logger.setLevel(logging.DEBUG) if not has_level_handler(logger): logger.addHandler(default_handler) return logger flask-3.1.1/src/flask/py.typed000066400000000000000000000000001501065614000161660ustar00rootroot00000000000000flask-3.1.1/src/flask/sansio/000077500000000000000000000000001501065614000157755ustar00rootroot00000000000000flask-3.1.1/src/flask/sansio/README.md000066400000000000000000000003441501065614000172550ustar00rootroot00000000000000# Sansio This folder contains code that can be used by alternative Flask implementations, for example Quart. The code therefore cannot do any IO, nor be part of a likely IO path. Finally this code cannot use the Flask globals. flask-3.1.1/src/flask/sansio/app.py000066400000000000000000001123441501065614000171340ustar00rootroot00000000000000from __future__ import annotations import logging import os import sys import typing as t from datetime import timedelta from itertools import chain from werkzeug.exceptions import Aborter from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequestKeyError from werkzeug.routing import BuildError from werkzeug.routing import Map from werkzeug.routing import Rule from werkzeug.sansio.response import Response from werkzeug.utils import cached_property from werkzeug.utils import redirect as _wz_redirect from .. import typing as ft from ..config import Config from ..config import ConfigAttribute from ..ctx import _AppCtxGlobals from ..helpers import _split_blueprint_path from ..helpers import get_debug_flag from ..json.provider import DefaultJSONProvider from ..json.provider import JSONProvider from ..logging import create_logger from ..templating import DispatchingJinjaLoader from ..templating import Environment from .scaffold import _endpoint_from_view_func from .scaffold import find_package from .scaffold import Scaffold from .scaffold import setupmethod if t.TYPE_CHECKING: # pragma: no cover from werkzeug.wrappers import Response as BaseResponse from ..testing import FlaskClient from ..testing import FlaskCliRunner from .blueprints import Blueprint T_shell_context_processor = t.TypeVar( "T_shell_context_processor", bound=ft.ShellContextProcessorCallable ) T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) def _make_timedelta(value: timedelta | int | None) -> timedelta | None: if value is None or isinstance(value, timedelta): return value return timedelta(seconds=value) class App(Scaffold): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the application. Once it is created it will act as a central registry for the view functions, the URL rules, template configuration and much more. The name of the package is used to resolve resources from inside the package or the folder the module is contained in depending on if the package parameter resolves to an actual python package (a folder with an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). For more information about resource loading, see :func:`open_resource`. Usually you create a :class:`Flask` instance in your main module or in the :file:`__init__.py` file of your package like this:: from flask import Flask app = Flask(__name__) .. admonition:: About the First Parameter The idea of the first parameter is to give Flask an idea of what belongs to your application. This name is used to find resources on the filesystem, can be used by extensions to improve debugging information and a lot more. So it's important what you provide there. If you are using a single module, `__name__` is always the correct value. If you however are using a package, it's usually recommended to hardcode the name of your package there. For example if your application is defined in :file:`yourapplication/app.py` you should create it with one of the two versions below:: app = Flask('yourapplication') app = Flask(__name__.split('.')[0]) Why is that? The application will work even with `__name__`, thanks to how resources are looked up. However it will make debugging more painful. Certain extensions can make assumptions based on the import name of your application. For example the Flask-SQLAlchemy extension will look for the code in your application that triggered an SQL query in debug mode. If the import name is not properly set up, that debugging information is lost. (For example it would only pick up SQL queries in `yourapplication.app` and not `yourapplication.views.frontend`) .. versionadded:: 0.7 The `static_url_path`, `static_folder`, and `template_folder` parameters were added. .. versionadded:: 0.8 The `instance_path` and `instance_relative_config` parameters were added. .. versionadded:: 0.11 The `root_path` parameter was added. .. versionadded:: 1.0 The ``host_matching`` and ``static_host`` parameters were added. .. versionadded:: 1.0 The ``subdomain_matching`` parameter was added. Subdomain matching needs to be enabled manually now. Setting :data:`SERVER_NAME` does not implicitly enable it. :param import_name: the name of the application package :param static_url_path: can be used to specify a different path for the static files on the web. Defaults to the name of the `static_folder` folder. :param static_folder: The folder with static files that is served at ``static_url_path``. Relative to the application ``root_path`` or an absolute path. Defaults to ``'static'``. :param static_host: the host to use when adding the static route. Defaults to None. Required when using ``host_matching=True`` with a ``static_folder`` configured. :param host_matching: set ``url_map.host_matching`` attribute. Defaults to False. :param subdomain_matching: consider the subdomain relative to :data:`SERVER_NAME` when matching routes. Defaults to False. :param template_folder: the folder that contains the templates that should be used by the application. Defaults to ``'templates'`` folder in the root path of the application. :param instance_path: An alternative instance path for the application. By default the folder ``'instance'`` next to the package or module is assumed to be the instance path. :param instance_relative_config: if set to ``True`` relative filenames for loading the config are assumed to be relative to the instance path instead of the application root. :param root_path: The path to the root of the application files. This should only be set manually when it can't be detected automatically, such as for namespace packages. """ #: The class of the object assigned to :attr:`aborter`, created by #: :meth:`create_aborter`. That object is called by #: :func:`flask.abort` to raise HTTP errors, and can be #: called directly as well. #: #: Defaults to :class:`werkzeug.exceptions.Aborter`. #: #: .. versionadded:: 2.2 aborter_class = Aborter #: The class that is used for the Jinja environment. #: #: .. versionadded:: 0.11 jinja_environment = Environment #: The class that is used for the :data:`~flask.g` instance. #: #: Example use cases for a custom class: #: #: 1. Store arbitrary attributes on flask.g. #: 2. Add a property for lazy per-request database connectors. #: 3. Return None instead of AttributeError on unexpected attributes. #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. #: #: In Flask 0.9 this property was called `request_globals_class` but it #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the #: flask.g object is now application context scoped. #: #: .. versionadded:: 0.10 app_ctx_globals_class = _AppCtxGlobals #: The class that is used for the ``config`` attribute of this app. #: Defaults to :class:`~flask.Config`. #: #: Example use cases for a custom class: #: #: 1. Default values for certain config options. #: 2. Access to config values through attributes in addition to keys. #: #: .. versionadded:: 0.11 config_class = Config #: The testing flag. Set this to ``True`` to enable the test mode of #: Flask extensions (and in the future probably also Flask itself). #: For example this might activate test helpers that have an #: additional runtime cost which should not be enabled by default. #: #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the #: default it's implicitly enabled. #: #: This attribute can also be configured from the config with the #: ``TESTING`` configuration key. Defaults to ``False``. testing = ConfigAttribute[bool]("TESTING") #: If a secret key is set, cryptographic components can use this to #: sign cookies and other things. Set this to a complex random value #: when you want to use the secure cookie for instance. #: #: This attribute can also be configured from the config with the #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. secret_key = ConfigAttribute[t.Union[str, bytes, None]]("SECRET_KEY") #: A :class:`~datetime.timedelta` which is used to set the expiration #: date of a permanent session. The default is 31 days which makes a #: permanent session survive for roughly one month. #: #: This attribute can also be configured from the config with the #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to #: ``timedelta(days=31)`` permanent_session_lifetime = ConfigAttribute[timedelta]( "PERMANENT_SESSION_LIFETIME", get_converter=_make_timedelta, # type: ignore[arg-type] ) json_provider_class: type[JSONProvider] = DefaultJSONProvider """A subclass of :class:`~flask.json.provider.JSONProvider`. An instance is created and assigned to :attr:`app.json` when creating the app. The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses Python's built-in :mod:`json` library. A different provider can use a different JSON library. .. versionadded:: 2.2 """ #: Options that are passed to the Jinja environment in #: :meth:`create_jinja_environment`. Changing these options after #: the environment is created (accessing :attr:`jinja_env`) will #: have no effect. #: #: .. versionchanged:: 1.1.0 #: This is a ``dict`` instead of an ``ImmutableDict`` to allow #: easier configuration. #: jinja_options: dict[str, t.Any] = {} #: The rule object to use for URL rules created. This is used by #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. #: #: .. versionadded:: 0.7 url_rule_class = Rule #: The map object to use for storing the URL rules and routing #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`. #: #: .. versionadded:: 1.1.0 url_map_class = Map #: The :meth:`test_client` method creates an instance of this test #: client class. Defaults to :class:`~flask.testing.FlaskClient`. #: #: .. versionadded:: 0.7 test_client_class: type[FlaskClient] | None = None #: The :class:`~click.testing.CliRunner` subclass, by default #: :class:`~flask.testing.FlaskCliRunner` that is used by #: :meth:`test_cli_runner`. Its ``__init__`` method should take a #: Flask app object as the first argument. #: #: .. versionadded:: 1.0 test_cli_runner_class: type[FlaskCliRunner] | None = None default_config: dict[str, t.Any] response_class: type[Response] def __init__( self, import_name: str, static_url_path: str | None = None, static_folder: str | os.PathLike[str] | None = "static", static_host: str | None = None, host_matching: bool = False, subdomain_matching: bool = False, template_folder: str | os.PathLike[str] | None = "templates", instance_path: str | None = None, instance_relative_config: bool = False, root_path: str | None = None, ) -> None: super().__init__( import_name=import_name, static_folder=static_folder, static_url_path=static_url_path, template_folder=template_folder, root_path=root_path, ) if instance_path is None: instance_path = self.auto_find_instance_path() elif not os.path.isabs(instance_path): raise ValueError( "If an instance path is provided it must be absolute." " A relative path was given instead." ) #: Holds the path to the instance folder. #: #: .. versionadded:: 0.8 self.instance_path = instance_path #: The configuration dictionary as :class:`Config`. This behaves #: exactly like a regular dictionary but supports additional methods #: to load a config from files. self.config = self.make_config(instance_relative_config) #: An instance of :attr:`aborter_class` created by #: :meth:`make_aborter`. This is called by :func:`flask.abort` #: to raise HTTP errors, and can be called directly as well. #: #: .. versionadded:: 2.2 #: Moved from ``flask.abort``, which calls this object. self.aborter = self.make_aborter() self.json: JSONProvider = self.json_provider_class(self) """Provides access to JSON methods. Functions in ``flask.json`` will call methods on this provider when the application context is active. Used for handling JSON requests and responses. An instance of :attr:`json_provider_class`. Can be customized by changing that attribute on a subclass, or by assigning to this attribute afterwards. The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses Python's built-in :mod:`json` library. A different provider can use a different JSON library. .. versionadded:: 2.2 """ #: A list of functions that are called by #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a #: :exc:`~werkzeug.routing.BuildError`. Each function is called #: with ``error``, ``endpoint`` and ``values``. If a function #: returns ``None`` or raises a ``BuildError``, it is skipped. #: Otherwise, its return value is returned by ``url_for``. #: #: .. versionadded:: 0.9 self.url_build_error_handlers: list[ t.Callable[[Exception, str, dict[str, t.Any]], str] ] = [] #: A list of functions that are called when the application context #: is destroyed. Since the application context is also torn down #: if the request ends this is the place to store code that disconnects #: from databases. #: #: .. versionadded:: 0.9 self.teardown_appcontext_funcs: list[ft.TeardownCallable] = [] #: A list of shell context processor functions that should be run #: when a shell context is created. #: #: .. versionadded:: 0.11 self.shell_context_processors: list[ft.ShellContextProcessorCallable] = [] #: Maps registered blueprint names to blueprint objects. The #: dict retains the order the blueprints were registered in. #: Blueprints can be registered multiple times, this dict does #: not track how often they were attached. #: #: .. versionadded:: 0.7 self.blueprints: dict[str, Blueprint] = {} #: a place where extensions can store application specific state. For #: example this is where an extension could store database engines and #: similar things. #: #: The key must match the name of the extension module. For example in #: case of a "Flask-Foo" extension in `flask_foo`, the key would be #: ``'foo'``. #: #: .. versionadded:: 0.7 self.extensions: dict[str, t.Any] = {} #: The :class:`~werkzeug.routing.Map` for this instance. You can use #: this to change the routing converters after the class was created #: but before any routes are connected. Example:: #: #: from werkzeug.routing import BaseConverter #: #: class ListConverter(BaseConverter): #: def to_python(self, value): #: return value.split(',') #: def to_url(self, values): #: return ','.join(super(ListConverter, self).to_url(value) #: for value in values) #: #: app = Flask(__name__) #: app.url_map.converters['list'] = ListConverter self.url_map = self.url_map_class(host_matching=host_matching) self.subdomain_matching = subdomain_matching # tracks internally if the application already handled at least one # request. self._got_first_request = False def _check_setup_finished(self, f_name: str) -> None: if self._got_first_request: raise AssertionError( f"The setup method '{f_name}' can no longer be called" " on the application. It has already handled its first" " request, any changes will not be applied" " consistently.\n" "Make sure all imports, decorators, functions, etc." " needed to set up the application are done before" " running it." ) @cached_property def name(self) -> str: # type: ignore """The name of the application. This is usually the import name with the difference that it's guessed from the run file if the import name is main. This name is used as a display name when Flask needs the name of the application. It can be set and overridden to change the value. .. versionadded:: 0.8 """ if self.import_name == "__main__": fn: str | None = getattr(sys.modules["__main__"], "__file__", None) if fn is None: return "__main__" return os.path.splitext(os.path.basename(fn))[0] return self.import_name @cached_property def logger(self) -> logging.Logger: """A standard Python :class:`~logging.Logger` for the app, with the same name as :attr:`name`. In debug mode, the logger's :attr:`~logging.Logger.level` will be set to :data:`~logging.DEBUG`. If there are no handlers configured, a default handler will be added. See :doc:`/logging` for more information. .. versionchanged:: 1.1.0 The logger takes the same name as :attr:`name` rather than hard-coding ``"flask.app"``. .. versionchanged:: 1.0.0 Behavior was simplified. The logger is always named ``"flask.app"``. The level is only set during configuration, it doesn't check ``app.debug`` each time. Only one format is used, not different ones depending on ``app.debug``. No handlers are removed, and a handler is only added if no handlers are already configured. .. versionadded:: 0.3 """ return create_logger(self) @cached_property def jinja_env(self) -> Environment: """The Jinja environment used to load templates. The environment is created the first time this property is accessed. Changing :attr:`jinja_options` after that will have no effect. """ return self.create_jinja_environment() def create_jinja_environment(self) -> Environment: raise NotImplementedError() def make_config(self, instance_relative: bool = False) -> Config: """Used to create the config attribute by the Flask constructor. The `instance_relative` parameter is passed in from the constructor of Flask (there named `instance_relative_config`) and indicates if the config should be relative to the instance path or the root path of the application. .. versionadded:: 0.8 """ root_path = self.root_path if instance_relative: root_path = self.instance_path defaults = dict(self.default_config) defaults["DEBUG"] = get_debug_flag() return self.config_class(root_path, defaults) def make_aborter(self) -> Aborter: """Create the object to assign to :attr:`aborter`. That object is called by :func:`flask.abort` to raise HTTP errors, and can be called directly as well. By default, this creates an instance of :attr:`aborter_class`, which defaults to :class:`werkzeug.exceptions.Aborter`. .. versionadded:: 2.2 """ return self.aborter_class() def auto_find_instance_path(self) -> str: """Tries to locate the instance path if it was not provided to the constructor of the application class. It will basically calculate the path to a folder named ``instance`` next to your main file or the package. .. versionadded:: 0.8 """ prefix, package_path = find_package(self.import_name) if prefix is None: return os.path.join(package_path, "instance") return os.path.join(prefix, "var", f"{self.name}-instance") def create_global_jinja_loader(self) -> DispatchingJinjaLoader: """Creates the loader for the Jinja2 environment. Can be used to override just the loader and keeping the rest unchanged. It's discouraged to override this function. Instead one should override the :meth:`jinja_loader` function instead. The global loader dispatches between the loaders of the application and the individual blueprints. .. versionadded:: 0.7 """ return DispatchingJinjaLoader(self) def select_jinja_autoescape(self, filename: str) -> bool: """Returns ``True`` if autoescaping should be active for the given template name. If no template name is given, returns `True`. .. versionchanged:: 2.2 Autoescaping is now enabled by default for ``.svg`` files. .. versionadded:: 0.5 """ if filename is None: return True return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) @property def debug(self) -> bool: """Whether debug mode is enabled. When using ``flask run`` to start the development server, an interactive debugger will be shown for unhandled exceptions, and the server will be reloaded when code changes. This maps to the :data:`DEBUG` config key. It may not behave as expected if set late. **Do not enable debug mode when deploying in production.** Default: ``False`` """ return self.config["DEBUG"] # type: ignore[no-any-return] @debug.setter def debug(self, value: bool) -> None: self.config["DEBUG"] = value if self.config["TEMPLATES_AUTO_RELOAD"] is None: self.jinja_env.auto_reload = value @setupmethod def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: """Register a :class:`~flask.Blueprint` on the application. Keyword arguments passed to this method will override the defaults set on the blueprint. Calls the blueprint's :meth:`~flask.Blueprint.register` method after recording the blueprint in the application's :attr:`blueprints`. :param blueprint: The blueprint to register. :param url_prefix: Blueprint routes will be prefixed with this. :param subdomain: Blueprint routes will match on this subdomain. :param url_defaults: Blueprint routes will use these default values for view arguments. :param options: Additional keyword arguments are passed to :class:`~flask.blueprints.BlueprintSetupState`. They can be accessed in :meth:`~flask.Blueprint.record` callbacks. .. versionchanged:: 2.0.1 The ``name`` option can be used to change the (pre-dotted) name the blueprint is registered with. This allows the same blueprint to be registered multiple times with unique names for ``url_for``. .. versionadded:: 0.7 """ blueprint.register(self, options) def iter_blueprints(self) -> t.ValuesView[Blueprint]: """Iterates over all blueprints by the order they were registered. .. versionadded:: 0.11 """ return self.blueprints.values() @setupmethod def add_url_rule( self, rule: str, endpoint: str | None = None, view_func: ft.RouteCallable | None = None, provide_automatic_options: bool | None = None, **options: t.Any, ) -> None: if endpoint is None: endpoint = _endpoint_from_view_func(view_func) # type: ignore options["endpoint"] = endpoint methods = options.pop("methods", None) # if the methods are not given and the view_func object knows its # methods we can use that instead. If neither exists, we go with # a tuple of only ``GET`` as default. if methods is None: methods = getattr(view_func, "methods", None) or ("GET",) if isinstance(methods, str): raise TypeError( "Allowed methods must be a list of strings, for" ' example: @app.route(..., methods=["POST"])' ) methods = {item.upper() for item in methods} # Methods that should always be added required_methods: set[str] = set(getattr(view_func, "required_methods", ())) # starting with Flask 0.8 the view_func object can disable and # force-enable the automatic options handling. if provide_automatic_options is None: provide_automatic_options = getattr( view_func, "provide_automatic_options", None ) if provide_automatic_options is None: if "OPTIONS" not in methods and self.config["PROVIDE_AUTOMATIC_OPTIONS"]: provide_automatic_options = True required_methods.add("OPTIONS") else: provide_automatic_options = False # Add the required methods now. methods |= required_methods rule_obj = self.url_rule_class(rule, methods=methods, **options) rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined] self.url_map.add(rule_obj) if view_func is not None: old_func = self.view_functions.get(endpoint) if old_func is not None and old_func != view_func: raise AssertionError( "View function mapping is overwriting an existing" f" endpoint function: {endpoint}" ) self.view_functions[endpoint] = view_func @setupmethod def template_filter( self, name: str | None = None ) -> t.Callable[[T_template_filter], T_template_filter]: """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function name will be used. Example:: @app.template_filter() def reverse(s): return s[::-1] :param name: the optional name of the filter, otherwise the function name will be used. """ def decorator(f: T_template_filter) -> T_template_filter: self.add_template_filter(f, name=name) return f return decorator @setupmethod def add_template_filter( self, f: ft.TemplateFilterCallable, name: str | None = None ) -> None: """Register a custom template filter. Works exactly like the :meth:`template_filter` decorator. :param name: the optional name of the filter, otherwise the function name will be used. """ self.jinja_env.filters[name or f.__name__] = f @setupmethod def template_test( self, name: str | None = None ) -> t.Callable[[T_template_test], T_template_test]: """A decorator that is used to register custom template test. You can specify a name for the test, otherwise the function name will be used. Example:: @app.template_test() def is_prime(n): if n == 2: return True for i in range(2, int(math.ceil(math.sqrt(n))) + 1): if n % i == 0: return False return True .. versionadded:: 0.10 :param name: the optional name of the test, otherwise the function name will be used. """ def decorator(f: T_template_test) -> T_template_test: self.add_template_test(f, name=name) return f return decorator @setupmethod def add_template_test( self, f: ft.TemplateTestCallable, name: str | None = None ) -> None: """Register a custom template test. Works exactly like the :meth:`template_test` decorator. .. versionadded:: 0.10 :param name: the optional name of the test, otherwise the function name will be used. """ self.jinja_env.tests[name or f.__name__] = f @setupmethod def template_global( self, name: str | None = None ) -> t.Callable[[T_template_global], T_template_global]: """A decorator that is used to register a custom template global function. You can specify a name for the global function, otherwise the function name will be used. Example:: @app.template_global() def double(n): return 2 * n .. versionadded:: 0.10 :param name: the optional name of the global function, otherwise the function name will be used. """ def decorator(f: T_template_global) -> T_template_global: self.add_template_global(f, name=name) return f return decorator @setupmethod def add_template_global( self, f: ft.TemplateGlobalCallable, name: str | None = None ) -> None: """Register a custom template global function. Works exactly like the :meth:`template_global` decorator. .. versionadded:: 0.10 :param name: the optional name of the global function, otherwise the function name will be used. """ self.jinja_env.globals[name or f.__name__] = f @setupmethod def teardown_appcontext(self, f: T_teardown) -> T_teardown: """Registers a function to be called when the application context is popped. The application context is typically popped after the request context for each request, at the end of CLI commands, or after a manually pushed context ends. .. code-block:: python with app.app_context(): ... When the ``with`` block exits (or ``ctx.pop()`` is called), the teardown functions are called just before the app context is made inactive. Since a request context typically also manages an application context it would also be called when you pop a request context. When a teardown function was called because of an unhandled exception it will be passed an error object. If an :meth:`errorhandler` is registered, it will handle the exception and the teardown will not receive it. Teardown functions must avoid raising exceptions. If they execute code that might fail they must surround that code with a ``try``/``except`` block and log any errors. The return values of teardown functions are ignored. .. versionadded:: 0.9 """ self.teardown_appcontext_funcs.append(f) return f @setupmethod def shell_context_processor( self, f: T_shell_context_processor ) -> T_shell_context_processor: """Registers a shell context processor function. .. versionadded:: 0.11 """ self.shell_context_processors.append(f) return f def _find_error_handler( self, e: Exception, blueprints: list[str] ) -> ft.ErrorHandlerCallable | None: """Return a registered error handler for an exception in this order: blueprint handler for a specific code, app handler for a specific code, blueprint handler for an exception class, app handler for an exception class, or ``None`` if a suitable handler is not found. """ exc_class, code = self._get_exc_class_and_code(type(e)) names = (*blueprints, None) for c in (code, None) if code is not None else (None,): for name in names: handler_map = self.error_handler_spec[name][c] if not handler_map: continue for cls in exc_class.__mro__: handler = handler_map.get(cls) if handler is not None: return handler return None def trap_http_exception(self, e: Exception) -> bool: """Checks if an HTTP exception should be trapped or not. By default this will return ``False`` for all exceptions except for a bad request key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``. This is called for all HTTP exceptions raised by a view function. If it returns ``True`` for any exception the error handler for this exception is not called and it shows up as regular exception in the traceback. This is helpful for debugging implicitly raised HTTP exceptions. .. versionchanged:: 1.0 Bad request errors are not trapped by default in debug mode. .. versionadded:: 0.8 """ if self.config["TRAP_HTTP_EXCEPTIONS"]: return True trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"] # if unset, trap key errors in debug mode if ( trap_bad_request is None and self.debug and isinstance(e, BadRequestKeyError) ): return True if trap_bad_request: return isinstance(e, BadRequest) return False def should_ignore_error(self, error: BaseException | None) -> bool: """This is called to figure out if an error should be ignored or not as far as the teardown system is concerned. If this function returns ``True`` then the teardown handlers will not be passed the error. .. versionadded:: 0.10 """ return False def redirect(self, location: str, code: int = 302) -> BaseResponse: """Create a redirect response object. This is called by :func:`flask.redirect`, and can be called directly as well. :param location: The URL to redirect to. :param code: The status code for the redirect. .. versionadded:: 2.2 Moved from ``flask.redirect``, which calls this method. """ return _wz_redirect( location, code=code, Response=self.response_class, # type: ignore[arg-type] ) def inject_url_defaults(self, endpoint: str, values: dict[str, t.Any]) -> None: """Injects the URL defaults for the given endpoint directly into the values dictionary passed. This is used internally and automatically called on URL building. .. versionadded:: 0.7 """ names: t.Iterable[str | None] = (None,) # url_for may be called outside a request context, parse the # passed endpoint instead of using request.blueprints. if "." in endpoint: names = chain( names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) ) for name in names: if name in self.url_default_functions: for func in self.url_default_functions[name]: func(endpoint, values) def handle_url_build_error( self, error: BuildError, endpoint: str, values: dict[str, t.Any] ) -> str: """Called by :meth:`.url_for` if a :exc:`~werkzeug.routing.BuildError` was raised. If this returns a value, it will be returned by ``url_for``, otherwise the error will be re-raised. Each function in :attr:`url_build_error_handlers` is called with ``error``, ``endpoint`` and ``values``. If a function returns ``None`` or raises a ``BuildError``, it is skipped. Otherwise, its return value is returned by ``url_for``. :param error: The active ``BuildError`` being handled. :param endpoint: The endpoint being built. :param values: The keyword arguments passed to ``url_for``. """ for handler in self.url_build_error_handlers: try: rv = handler(error, endpoint, values) except BuildError as e: # make error available outside except block error = e else: if rv is not None: return rv # Re-raise if called with an active exception, otherwise raise # the passed in exception. if error is sys.exc_info()[1]: raise raise error flask-3.1.1/src/flask/sansio/blueprints.py000066400000000000000000000600751501065614000205460ustar00rootroot00000000000000from __future__ import annotations import os import typing as t from collections import defaultdict from functools import update_wrapper from .. import typing as ft from .scaffold import _endpoint_from_view_func from .scaffold import _sentinel from .scaffold import Scaffold from .scaffold import setupmethod if t.TYPE_CHECKING: # pragma: no cover from .app import App DeferredSetupFunction = t.Callable[["BlueprintSetupState"], None] T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) T_template_context_processor = t.TypeVar( "T_template_context_processor", bound=ft.TemplateContextProcessorCallable ) T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) T_url_value_preprocessor = t.TypeVar( "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable ) class BlueprintSetupState: """Temporary holder object for registering a blueprint with the application. An instance of this class is created by the :meth:`~flask.Blueprint.make_setup_state` method and later passed to all register callback functions. """ def __init__( self, blueprint: Blueprint, app: App, options: t.Any, first_registration: bool, ) -> None: #: a reference to the current application self.app = app #: a reference to the blueprint that created this setup state. self.blueprint = blueprint #: a dictionary with all options that were passed to the #: :meth:`~flask.Flask.register_blueprint` method. self.options = options #: as blueprints can be registered multiple times with the #: application and not everything wants to be registered #: multiple times on it, this attribute can be used to figure #: out if the blueprint was registered in the past already. self.first_registration = first_registration subdomain = self.options.get("subdomain") if subdomain is None: subdomain = self.blueprint.subdomain #: The subdomain that the blueprint should be active for, ``None`` #: otherwise. self.subdomain = subdomain url_prefix = self.options.get("url_prefix") if url_prefix is None: url_prefix = self.blueprint.url_prefix #: The prefix that should be used for all URLs defined on the #: blueprint. self.url_prefix = url_prefix self.name = self.options.get("name", blueprint.name) self.name_prefix = self.options.get("name_prefix", "") #: A dictionary with URL defaults that is added to each and every #: URL that was defined with the blueprint. self.url_defaults = dict(self.blueprint.url_values_defaults) self.url_defaults.update(self.options.get("url_defaults", ())) def add_url_rule( self, rule: str, endpoint: str | None = None, view_func: ft.RouteCallable | None = None, **options: t.Any, ) -> None: """A helper method to register a rule (and optionally a view function) to the application. The endpoint is automatically prefixed with the blueprint's name. """ if self.url_prefix is not None: if rule: rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) else: rule = self.url_prefix options.setdefault("subdomain", self.subdomain) if endpoint is None: endpoint = _endpoint_from_view_func(view_func) # type: ignore defaults = self.url_defaults if "defaults" in options: defaults = dict(defaults, **options.pop("defaults")) self.app.add_url_rule( rule, f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), view_func, defaults=defaults, **options, ) class Blueprint(Scaffold): """Represents a blueprint, a collection of routes and other app-related functions that can be registered on a real application later. A blueprint is an object that allows defining application functions without requiring an application object ahead of time. It uses the same decorators as :class:`~flask.Flask`, but defers the need for an application by recording them for later registration. Decorating a function with a blueprint creates a deferred function that is called with :class:`~flask.blueprints.BlueprintSetupState` when the blueprint is registered on an application. See :doc:`/blueprints` for more information. :param name: The name of the blueprint. Will be prepended to each endpoint name. :param import_name: The name of the blueprint package, usually ``__name__``. This helps locate the ``root_path`` for the blueprint. :param static_folder: A folder with static files that should be served by the blueprint's static route. The path is relative to the blueprint's root path. Blueprint static files are disabled by default. :param static_url_path: The url to serve static files from. Defaults to ``static_folder``. If the blueprint does not have a ``url_prefix``, the app's static route will take precedence, and the blueprint's static files won't be accessible. :param template_folder: A folder with templates that should be added to the app's template search path. The path is relative to the blueprint's root path. Blueprint templates are disabled by default. Blueprint templates have a lower precedence than those in the app's templates folder. :param url_prefix: A path to prepend to all of the blueprint's URLs, to make them distinct from the rest of the app's routes. :param subdomain: A subdomain that blueprint routes will match on by default. :param url_defaults: A dict of default values that blueprint routes will receive by default. :param root_path: By default, the blueprint will automatically set this based on ``import_name``. In certain situations this automatic detection can fail, so the path can be specified manually instead. .. versionchanged:: 1.1.0 Blueprints have a ``cli`` group to register nested CLI commands. The ``cli_group`` parameter controls the name of the group under the ``flask`` command. .. versionadded:: 0.7 """ _got_registered_once = False def __init__( self, name: str, import_name: str, static_folder: str | os.PathLike[str] | None = None, static_url_path: str | None = None, template_folder: str | os.PathLike[str] | None = None, url_prefix: str | None = None, subdomain: str | None = None, url_defaults: dict[str, t.Any] | None = None, root_path: str | None = None, cli_group: str | None = _sentinel, # type: ignore[assignment] ): super().__init__( import_name=import_name, static_folder=static_folder, static_url_path=static_url_path, template_folder=template_folder, root_path=root_path, ) if not name: raise ValueError("'name' may not be empty.") if "." in name: raise ValueError("'name' may not contain a dot '.' character.") self.name = name self.url_prefix = url_prefix self.subdomain = subdomain self.deferred_functions: list[DeferredSetupFunction] = [] if url_defaults is None: url_defaults = {} self.url_values_defaults = url_defaults self.cli_group = cli_group self._blueprints: list[tuple[Blueprint, dict[str, t.Any]]] = [] def _check_setup_finished(self, f_name: str) -> None: if self._got_registered_once: raise AssertionError( f"The setup method '{f_name}' can no longer be called on the blueprint" f" '{self.name}'. It has already been registered at least once, any" " changes will not be applied consistently.\n" "Make sure all imports, decorators, functions, etc. needed to set up" " the blueprint are done before registering it." ) @setupmethod def record(self, func: DeferredSetupFunction) -> None: """Registers a function that is called when the blueprint is registered on the application. This function is called with the state as argument as returned by the :meth:`make_setup_state` method. """ self.deferred_functions.append(func) @setupmethod def record_once(self, func: DeferredSetupFunction) -> None: """Works like :meth:`record` but wraps the function in another function that will ensure the function is only called once. If the blueprint is registered a second time on the application, the function passed is not called. """ def wrapper(state: BlueprintSetupState) -> None: if state.first_registration: func(state) self.record(update_wrapper(wrapper, func)) def make_setup_state( self, app: App, options: dict[str, t.Any], first_registration: bool = False ) -> BlueprintSetupState: """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` object that is later passed to the register callback functions. Subclasses can override this to return a subclass of the setup state. """ return BlueprintSetupState(self, app, options, first_registration) @setupmethod def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: """Register a :class:`~flask.Blueprint` on this blueprint. Keyword arguments passed to this method will override the defaults set on the blueprint. .. versionchanged:: 2.0.1 The ``name`` option can be used to change the (pre-dotted) name the blueprint is registered with. This allows the same blueprint to be registered multiple times with unique names for ``url_for``. .. versionadded:: 2.0 """ if blueprint is self: raise ValueError("Cannot register a blueprint on itself") self._blueprints.append((blueprint, options)) def register(self, app: App, options: dict[str, t.Any]) -> None: """Called by :meth:`Flask.register_blueprint` to register all views and callbacks registered on the blueprint with the application. Creates a :class:`.BlueprintSetupState` and calls each :meth:`record` callback with it. :param app: The application this blueprint is being registered with. :param options: Keyword arguments forwarded from :meth:`~Flask.register_blueprint`. .. versionchanged:: 2.3 Nested blueprints now correctly apply subdomains. .. versionchanged:: 2.1 Registering the same blueprint with the same name multiple times is an error. .. versionchanged:: 2.0.1 Nested blueprints are registered with their dotted name. This allows different blueprints with the same name to be nested at different locations. .. versionchanged:: 2.0.1 The ``name`` option can be used to change the (pre-dotted) name the blueprint is registered with. This allows the same blueprint to be registered multiple times with unique names for ``url_for``. """ name_prefix = options.get("name_prefix", "") self_name = options.get("name", self.name) name = f"{name_prefix}.{self_name}".lstrip(".") if name in app.blueprints: bp_desc = "this" if app.blueprints[name] is self else "a different" existing_at = f" '{name}'" if self_name != name else "" raise ValueError( f"The name '{self_name}' is already registered for" f" {bp_desc} blueprint{existing_at}. Use 'name=' to" f" provide a unique name." ) first_bp_registration = not any(bp is self for bp in app.blueprints.values()) first_name_registration = name not in app.blueprints app.blueprints[name] = self self._got_registered_once = True state = self.make_setup_state(app, options, first_bp_registration) if self.has_static_folder: state.add_url_rule( f"{self.static_url_path}/", view_func=self.send_static_file, # type: ignore[attr-defined] endpoint="static", ) # Merge blueprint data into parent. if first_bp_registration or first_name_registration: self._merge_blueprint_funcs(app, name) for deferred in self.deferred_functions: deferred(state) cli_resolved_group = options.get("cli_group", self.cli_group) if self.cli.commands: if cli_resolved_group is None: app.cli.commands.update(self.cli.commands) elif cli_resolved_group is _sentinel: self.cli.name = name app.cli.add_command(self.cli) else: self.cli.name = cli_resolved_group app.cli.add_command(self.cli) for blueprint, bp_options in self._blueprints: bp_options = bp_options.copy() bp_url_prefix = bp_options.get("url_prefix") bp_subdomain = bp_options.get("subdomain") if bp_subdomain is None: bp_subdomain = blueprint.subdomain if state.subdomain is not None and bp_subdomain is not None: bp_options["subdomain"] = bp_subdomain + "." + state.subdomain elif bp_subdomain is not None: bp_options["subdomain"] = bp_subdomain elif state.subdomain is not None: bp_options["subdomain"] = state.subdomain if bp_url_prefix is None: bp_url_prefix = blueprint.url_prefix if state.url_prefix is not None and bp_url_prefix is not None: bp_options["url_prefix"] = ( state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") ) elif bp_url_prefix is not None: bp_options["url_prefix"] = bp_url_prefix elif state.url_prefix is not None: bp_options["url_prefix"] = state.url_prefix bp_options["name_prefix"] = name blueprint.register(app, bp_options) def _merge_blueprint_funcs(self, app: App, name: str) -> None: def extend( bp_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], parent_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], ) -> None: for key, values in bp_dict.items(): key = name if key is None else f"{name}.{key}" parent_dict[key].extend(values) for key, value in self.error_handler_spec.items(): key = name if key is None else f"{name}.{key}" value = defaultdict( dict, { code: {exc_class: func for exc_class, func in code_values.items()} for code, code_values in value.items() }, ) app.error_handler_spec[key] = value for endpoint, func in self.view_functions.items(): app.view_functions[endpoint] = func extend(self.before_request_funcs, app.before_request_funcs) extend(self.after_request_funcs, app.after_request_funcs) extend( self.teardown_request_funcs, app.teardown_request_funcs, ) extend(self.url_default_functions, app.url_default_functions) extend(self.url_value_preprocessors, app.url_value_preprocessors) extend(self.template_context_processors, app.template_context_processors) @setupmethod def add_url_rule( self, rule: str, endpoint: str | None = None, view_func: ft.RouteCallable | None = None, provide_automatic_options: bool | None = None, **options: t.Any, ) -> None: """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for full documentation. The URL rule is prefixed with the blueprint's URL prefix. The endpoint name, used with :func:`url_for`, is prefixed with the blueprint's name. """ if endpoint and "." in endpoint: raise ValueError("'endpoint' may not contain a dot '.' character.") if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: raise ValueError("'view_func' name may not contain a dot '.' character.") self.record( lambda s: s.add_url_rule( rule, endpoint, view_func, provide_automatic_options=provide_automatic_options, **options, ) ) @setupmethod def app_template_filter( self, name: str | None = None ) -> t.Callable[[T_template_filter], T_template_filter]: """Register a template filter, available in any template rendered by the application. Equivalent to :meth:`.Flask.template_filter`. :param name: the optional name of the filter, otherwise the function name will be used. """ def decorator(f: T_template_filter) -> T_template_filter: self.add_app_template_filter(f, name=name) return f return decorator @setupmethod def add_app_template_filter( self, f: ft.TemplateFilterCallable, name: str | None = None ) -> None: """Register a template filter, available in any template rendered by the application. Works like the :meth:`app_template_filter` decorator. Equivalent to :meth:`.Flask.add_template_filter`. :param name: the optional name of the filter, otherwise the function name will be used. """ def register_template(state: BlueprintSetupState) -> None: state.app.jinja_env.filters[name or f.__name__] = f self.record_once(register_template) @setupmethod def app_template_test( self, name: str | None = None ) -> t.Callable[[T_template_test], T_template_test]: """Register a template test, available in any template rendered by the application. Equivalent to :meth:`.Flask.template_test`. .. versionadded:: 0.10 :param name: the optional name of the test, otherwise the function name will be used. """ def decorator(f: T_template_test) -> T_template_test: self.add_app_template_test(f, name=name) return f return decorator @setupmethod def add_app_template_test( self, f: ft.TemplateTestCallable, name: str | None = None ) -> None: """Register a template test, available in any template rendered by the application. Works like the :meth:`app_template_test` decorator. Equivalent to :meth:`.Flask.add_template_test`. .. versionadded:: 0.10 :param name: the optional name of the test, otherwise the function name will be used. """ def register_template(state: BlueprintSetupState) -> None: state.app.jinja_env.tests[name or f.__name__] = f self.record_once(register_template) @setupmethod def app_template_global( self, name: str | None = None ) -> t.Callable[[T_template_global], T_template_global]: """Register a template global, available in any template rendered by the application. Equivalent to :meth:`.Flask.template_global`. .. versionadded:: 0.10 :param name: the optional name of the global, otherwise the function name will be used. """ def decorator(f: T_template_global) -> T_template_global: self.add_app_template_global(f, name=name) return f return decorator @setupmethod def add_app_template_global( self, f: ft.TemplateGlobalCallable, name: str | None = None ) -> None: """Register a template global, available in any template rendered by the application. Works like the :meth:`app_template_global` decorator. Equivalent to :meth:`.Flask.add_template_global`. .. versionadded:: 0.10 :param name: the optional name of the global, otherwise the function name will be used. """ def register_template(state: BlueprintSetupState) -> None: state.app.jinja_env.globals[name or f.__name__] = f self.record_once(register_template) @setupmethod def before_app_request(self, f: T_before_request) -> T_before_request: """Like :meth:`before_request`, but before every request, not only those handled by the blueprint. Equivalent to :meth:`.Flask.before_request`. """ self.record_once( lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) ) return f @setupmethod def after_app_request(self, f: T_after_request) -> T_after_request: """Like :meth:`after_request`, but after every request, not only those handled by the blueprint. Equivalent to :meth:`.Flask.after_request`. """ self.record_once( lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) ) return f @setupmethod def teardown_app_request(self, f: T_teardown) -> T_teardown: """Like :meth:`teardown_request`, but after every request, not only those handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`. """ self.record_once( lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) ) return f @setupmethod def app_context_processor( self, f: T_template_context_processor ) -> T_template_context_processor: """Like :meth:`context_processor`, but for templates rendered by every view, not only by the blueprint. Equivalent to :meth:`.Flask.context_processor`. """ self.record_once( lambda s: s.app.template_context_processors.setdefault(None, []).append(f) ) return f @setupmethod def app_errorhandler( self, code: type[Exception] | int ) -> t.Callable[[T_error_handler], T_error_handler]: """Like :meth:`errorhandler`, but for every request, not only those handled by the blueprint. Equivalent to :meth:`.Flask.errorhandler`. """ def decorator(f: T_error_handler) -> T_error_handler: def from_blueprint(state: BlueprintSetupState) -> None: state.app.errorhandler(code)(f) self.record_once(from_blueprint) return f return decorator @setupmethod def app_url_value_preprocessor( self, f: T_url_value_preprocessor ) -> T_url_value_preprocessor: """Like :meth:`url_value_preprocessor`, but for every request, not only those handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`. """ self.record_once( lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) ) return f @setupmethod def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: """Like :meth:`url_defaults`, but for every request, not only those handled by the blueprint. Equivalent to :meth:`.Flask.url_defaults`. """ self.record_once( lambda s: s.app.url_default_functions.setdefault(None, []).append(f) ) return f flask-3.1.1/src/flask/sansio/scaffold.py000066400000000000000000000732631501065614000201430ustar00rootroot00000000000000from __future__ import annotations import importlib.util import os import pathlib import sys import typing as t from collections import defaultdict from functools import update_wrapper from jinja2 import BaseLoader from jinja2 import FileSystemLoader from werkzeug.exceptions import default_exceptions from werkzeug.exceptions import HTTPException from werkzeug.utils import cached_property from .. import typing as ft from ..helpers import get_root_path from ..templating import _default_template_ctx_processor if t.TYPE_CHECKING: # pragma: no cover from click import Group # a singleton sentinel value for parameter defaults _sentinel = object() F = t.TypeVar("F", bound=t.Callable[..., t.Any]) T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) T_template_context_processor = t.TypeVar( "T_template_context_processor", bound=ft.TemplateContextProcessorCallable ) T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) T_url_value_preprocessor = t.TypeVar( "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable ) T_route = t.TypeVar("T_route", bound=ft.RouteCallable) def setupmethod(f: F) -> F: f_name = f.__name__ def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any: self._check_setup_finished(f_name) return f(self, *args, **kwargs) return t.cast(F, update_wrapper(wrapper_func, f)) class Scaffold: """Common behavior shared between :class:`~flask.Flask` and :class:`~flask.blueprints.Blueprint`. :param import_name: The import name of the module where this object is defined. Usually :attr:`__name__` should be used. :param static_folder: Path to a folder of static files to serve. If this is set, a static route will be added. :param static_url_path: URL prefix for the static route. :param template_folder: Path to a folder containing template files. for rendering. If this is set, a Jinja loader will be added. :param root_path: The path that static, template, and resource files are relative to. Typically not set, it is discovered based on the ``import_name``. .. versionadded:: 2.0 """ cli: Group name: str _static_folder: str | None = None _static_url_path: str | None = None def __init__( self, import_name: str, static_folder: str | os.PathLike[str] | None = None, static_url_path: str | None = None, template_folder: str | os.PathLike[str] | None = None, root_path: str | None = None, ): #: The name of the package or module that this object belongs #: to. Do not change this once it is set by the constructor. self.import_name = import_name self.static_folder = static_folder # type: ignore self.static_url_path = static_url_path #: The path to the templates folder, relative to #: :attr:`root_path`, to add to the template loader. ``None`` if #: templates should not be added. self.template_folder = template_folder if root_path is None: root_path = get_root_path(self.import_name) #: Absolute path to the package on the filesystem. Used to look #: up resources contained in the package. self.root_path = root_path #: A dictionary mapping endpoint names to view functions. #: #: To register a view function, use the :meth:`route` decorator. #: #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.view_functions: dict[str, ft.RouteCallable] = {} #: A data structure of registered error handlers, in the format #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is #: the name of a blueprint the handlers are active for, or #: ``None`` for all requests. The ``code`` key is the HTTP #: status code for ``HTTPException``, or ``None`` for #: other exceptions. The innermost dictionary maps exception #: classes to handler functions. #: #: To register an error handler, use the :meth:`errorhandler` #: decorator. #: #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.error_handler_spec: dict[ ft.AppOrBlueprintKey, dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]], ] = defaultdict(lambda: defaultdict(dict)) #: A data structure of functions to call at the beginning of #: each request, in the format ``{scope: [functions]}``. The #: ``scope`` key is the name of a blueprint the functions are #: active for, or ``None`` for all requests. #: #: To register a function, use the :meth:`before_request` #: decorator. #: #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.before_request_funcs: dict[ ft.AppOrBlueprintKey, list[ft.BeforeRequestCallable] ] = defaultdict(list) #: A data structure of functions to call at the end of each #: request, in the format ``{scope: [functions]}``. The #: ``scope`` key is the name of a blueprint the functions are #: active for, or ``None`` for all requests. #: #: To register a function, use the :meth:`after_request` #: decorator. #: #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.after_request_funcs: dict[ ft.AppOrBlueprintKey, list[ft.AfterRequestCallable[t.Any]] ] = defaultdict(list) #: A data structure of functions to call at the end of each #: request even if an exception is raised, in the format #: ``{scope: [functions]}``. The ``scope`` key is the name of a #: blueprint the functions are active for, or ``None`` for all #: requests. #: #: To register a function, use the :meth:`teardown_request` #: decorator. #: #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.teardown_request_funcs: dict[ ft.AppOrBlueprintKey, list[ft.TeardownCallable] ] = defaultdict(list) #: A data structure of functions to call to pass extra context #: values when rendering templates, in the format #: ``{scope: [functions]}``. The ``scope`` key is the name of a #: blueprint the functions are active for, or ``None`` for all #: requests. #: #: To register a function, use the :meth:`context_processor` #: decorator. #: #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.template_context_processors: dict[ ft.AppOrBlueprintKey, list[ft.TemplateContextProcessorCallable] ] = defaultdict(list, {None: [_default_template_ctx_processor]}) #: A data structure of functions to call to modify the keyword #: arguments passed to the view function, in the format #: ``{scope: [functions]}``. The ``scope`` key is the name of a #: blueprint the functions are active for, or ``None`` for all #: requests. #: #: To register a function, use the #: :meth:`url_value_preprocessor` decorator. #: #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.url_value_preprocessors: dict[ ft.AppOrBlueprintKey, list[ft.URLValuePreprocessorCallable], ] = defaultdict(list) #: A data structure of functions to call to modify the keyword #: arguments when generating URLs, in the format #: ``{scope: [functions]}``. The ``scope`` key is the name of a #: blueprint the functions are active for, or ``None`` for all #: requests. #: #: To register a function, use the :meth:`url_defaults` #: decorator. #: #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.url_default_functions: dict[ ft.AppOrBlueprintKey, list[ft.URLDefaultCallable] ] = defaultdict(list) def __repr__(self) -> str: return f"<{type(self).__name__} {self.name!r}>" def _check_setup_finished(self, f_name: str) -> None: raise NotImplementedError @property def static_folder(self) -> str | None: """The absolute path to the configured static folder. ``None`` if no static folder is set. """ if self._static_folder is not None: return os.path.join(self.root_path, self._static_folder) else: return None @static_folder.setter def static_folder(self, value: str | os.PathLike[str] | None) -> None: if value is not None: value = os.fspath(value).rstrip(r"\/") self._static_folder = value @property def has_static_folder(self) -> bool: """``True`` if :attr:`static_folder` is set. .. versionadded:: 0.5 """ return self.static_folder is not None @property def static_url_path(self) -> str | None: """The URL prefix that the static route will be accessible from. If it was not configured during init, it is derived from :attr:`static_folder`. """ if self._static_url_path is not None: return self._static_url_path if self.static_folder is not None: basename = os.path.basename(self.static_folder) return f"/{basename}".rstrip("/") return None @static_url_path.setter def static_url_path(self, value: str | None) -> None: if value is not None: value = value.rstrip("/") self._static_url_path = value @cached_property def jinja_loader(self) -> BaseLoader | None: """The Jinja loader for this object's templates. By default this is a class :class:`jinja2.loaders.FileSystemLoader` to :attr:`template_folder` if it is set. .. versionadded:: 0.5 """ if self.template_folder is not None: return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) else: return None def _method_route( self, method: str, rule: str, options: dict[str, t.Any], ) -> t.Callable[[T_route], T_route]: if "methods" in options: raise TypeError("Use the 'route' decorator to use the 'methods' argument.") return self.route(rule, methods=[method], **options) @setupmethod def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Shortcut for :meth:`route` with ``methods=["GET"]``. .. versionadded:: 2.0 """ return self._method_route("GET", rule, options) @setupmethod def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Shortcut for :meth:`route` with ``methods=["POST"]``. .. versionadded:: 2.0 """ return self._method_route("POST", rule, options) @setupmethod def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Shortcut for :meth:`route` with ``methods=["PUT"]``. .. versionadded:: 2.0 """ return self._method_route("PUT", rule, options) @setupmethod def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Shortcut for :meth:`route` with ``methods=["DELETE"]``. .. versionadded:: 2.0 """ return self._method_route("DELETE", rule, options) @setupmethod def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Shortcut for :meth:`route` with ``methods=["PATCH"]``. .. versionadded:: 2.0 """ return self._method_route("PATCH", rule, options) @setupmethod def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Decorate a view function to register it with the given URL rule and options. Calls :meth:`add_url_rule`, which has more details about the implementation. .. code-block:: python @app.route("/") def index(): return "Hello, World!" See :ref:`url-route-registrations`. The endpoint name for the route defaults to the name of the view function if the ``endpoint`` parameter isn't passed. The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and ``OPTIONS`` are added automatically. :param rule: The URL rule string. :param options: Extra options passed to the :class:`~werkzeug.routing.Rule` object. """ def decorator(f: T_route) -> T_route: endpoint = options.pop("endpoint", None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator @setupmethod def add_url_rule( self, rule: str, endpoint: str | None = None, view_func: ft.RouteCallable | None = None, provide_automatic_options: bool | None = None, **options: t.Any, ) -> None: """Register a rule for routing incoming requests and building URLs. The :meth:`route` decorator is a shortcut to call this with the ``view_func`` argument. These are equivalent: .. code-block:: python @app.route("/") def index(): ... .. code-block:: python def index(): ... app.add_url_rule("/", view_func=index) See :ref:`url-route-registrations`. The endpoint name for the route defaults to the name of the view function if the ``endpoint`` parameter isn't passed. An error will be raised if a function has already been registered for the endpoint. The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is always added automatically, and ``OPTIONS`` is added automatically by default. ``view_func`` does not necessarily need to be passed, but if the rule should participate in routing an endpoint name must be associated with a view function at some point with the :meth:`endpoint` decorator. .. code-block:: python app.add_url_rule("/", endpoint="index") @app.endpoint("index") def index(): ... If ``view_func`` has a ``required_methods`` attribute, those methods are added to the passed and automatic methods. If it has a ``provide_automatic_methods`` attribute, it is used as the default if the parameter is not passed. :param rule: The URL rule string. :param endpoint: The endpoint name to associate with the rule and view function. Used when routing and building URLs. Defaults to ``view_func.__name__``. :param view_func: The view function to associate with the endpoint name. :param provide_automatic_options: Add the ``OPTIONS`` method and respond to ``OPTIONS`` requests automatically. :param options: Extra options passed to the :class:`~werkzeug.routing.Rule` object. """ raise NotImplementedError @setupmethod def endpoint(self, endpoint: str) -> t.Callable[[F], F]: """Decorate a view function to register it for the given endpoint. Used if a rule is added without a ``view_func`` with :meth:`add_url_rule`. .. code-block:: python app.add_url_rule("/ex", endpoint="example") @app.endpoint("example") def example(): ... :param endpoint: The endpoint name to associate with the view function. """ def decorator(f: F) -> F: self.view_functions[endpoint] = f return f return decorator @setupmethod def before_request(self, f: T_before_request) -> T_before_request: """Register a function to run before each request. For example, this can be used to open a database connection, or to load the logged in user from the session. .. code-block:: python @app.before_request def load_user(): if "user_id" in session: g.user = db.session.get(session["user_id"]) The function will be called without any arguments. If it returns a non-``None`` value, the value is handled as if it was the return value from the view, and further request handling is stopped. This is available on both app and blueprint objects. When used on an app, this executes before every request. When used on a blueprint, this executes before every request that the blueprint handles. To register with a blueprint and execute before every request, use :meth:`.Blueprint.before_app_request`. """ self.before_request_funcs.setdefault(None, []).append(f) return f @setupmethod def after_request(self, f: T_after_request) -> T_after_request: """Register a function to run after each request to this object. The function is called with the response object, and must return a response object. This allows the functions to modify or replace the response before it is sent. If a function raises an exception, any remaining ``after_request`` functions will not be called. Therefore, this should not be used for actions that must execute, such as to close resources. Use :meth:`teardown_request` for that. This is available on both app and blueprint objects. When used on an app, this executes after every request. When used on a blueprint, this executes after every request that the blueprint handles. To register with a blueprint and execute after every request, use :meth:`.Blueprint.after_app_request`. """ self.after_request_funcs.setdefault(None, []).append(f) return f @setupmethod def teardown_request(self, f: T_teardown) -> T_teardown: """Register a function to be called when the request context is popped. Typically this happens at the end of each request, but contexts may be pushed manually as well during testing. .. code-block:: python with app.test_request_context(): ... When the ``with`` block exits (or ``ctx.pop()`` is called), the teardown functions are called just before the request context is made inactive. When a teardown function was called because of an unhandled exception it will be passed an error object. If an :meth:`errorhandler` is registered, it will handle the exception and the teardown will not receive it. Teardown functions must avoid raising exceptions. If they execute code that might fail they must surround that code with a ``try``/``except`` block and log any errors. The return values of teardown functions are ignored. This is available on both app and blueprint objects. When used on an app, this executes after every request. When used on a blueprint, this executes after every request that the blueprint handles. To register with a blueprint and execute after every request, use :meth:`.Blueprint.teardown_app_request`. """ self.teardown_request_funcs.setdefault(None, []).append(f) return f @setupmethod def context_processor( self, f: T_template_context_processor, ) -> T_template_context_processor: """Registers a template context processor function. These functions run before rendering a template. The keys of the returned dict are added as variables available in the template. This is available on both app and blueprint objects. When used on an app, this is called for every rendered template. When used on a blueprint, this is called for templates rendered from the blueprint's views. To register with a blueprint and affect every template, use :meth:`.Blueprint.app_context_processor`. """ self.template_context_processors[None].append(f) return f @setupmethod def url_value_preprocessor( self, f: T_url_value_preprocessor, ) -> T_url_value_preprocessor: """Register a URL value preprocessor function for all view functions in the application. These functions will be called before the :meth:`before_request` functions. The function can modify the values captured from the matched url before they are passed to the view. For example, this can be used to pop a common language code value and place it in ``g`` rather than pass it to every view. The function is passed the endpoint name and values dict. The return value is ignored. This is available on both app and blueprint objects. When used on an app, this is called for every request. When used on a blueprint, this is called for requests that the blueprint handles. To register with a blueprint and affect every request, use :meth:`.Blueprint.app_url_value_preprocessor`. """ self.url_value_preprocessors[None].append(f) return f @setupmethod def url_defaults(self, f: T_url_defaults) -> T_url_defaults: """Callback function for URL defaults for all view functions of the application. It's called with the endpoint and values and should update the values passed in place. This is available on both app and blueprint objects. When used on an app, this is called for every request. When used on a blueprint, this is called for requests that the blueprint handles. To register with a blueprint and affect every request, use :meth:`.Blueprint.app_url_defaults`. """ self.url_default_functions[None].append(f) return f @setupmethod def errorhandler( self, code_or_exception: type[Exception] | int ) -> t.Callable[[T_error_handler], T_error_handler]: """Register a function to handle errors by code or exception class. A decorator that is used to register a function given an error code. Example:: @app.errorhandler(404) def page_not_found(error): return 'This page does not exist', 404 You can also register handlers for arbitrary exceptions:: @app.errorhandler(DatabaseError) def special_exception_handler(error): return 'Database connection failed', 500 This is available on both app and blueprint objects. When used on an app, this can handle errors from every request. When used on a blueprint, this can handle errors from requests that the blueprint handles. To register with a blueprint and affect every request, use :meth:`.Blueprint.app_errorhandler`. .. versionadded:: 0.7 Use :meth:`register_error_handler` instead of modifying :attr:`error_handler_spec` directly, for application wide error handlers. .. versionadded:: 0.7 One can now additionally also register custom exception types that do not necessarily have to be a subclass of the :class:`~werkzeug.exceptions.HTTPException` class. :param code_or_exception: the code as integer for the handler, or an arbitrary exception """ def decorator(f: T_error_handler) -> T_error_handler: self.register_error_handler(code_or_exception, f) return f return decorator @setupmethod def register_error_handler( self, code_or_exception: type[Exception] | int, f: ft.ErrorHandlerCallable, ) -> None: """Alternative error attach function to the :meth:`errorhandler` decorator that is more straightforward to use for non decorator usage. .. versionadded:: 0.7 """ exc_class, code = self._get_exc_class_and_code(code_or_exception) self.error_handler_spec[None][code][exc_class] = f @staticmethod def _get_exc_class_and_code( exc_class_or_code: type[Exception] | int, ) -> tuple[type[Exception], int | None]: """Get the exception class being handled. For HTTP status codes or ``HTTPException`` subclasses, return both the exception and status code. :param exc_class_or_code: Any exception class, or an HTTP status code as an integer. """ exc_class: type[Exception] if isinstance(exc_class_or_code, int): try: exc_class = default_exceptions[exc_class_or_code] except KeyError: raise ValueError( f"'{exc_class_or_code}' is not a recognized HTTP" " error code. Use a subclass of HTTPException with" " that code instead." ) from None else: exc_class = exc_class_or_code if isinstance(exc_class, Exception): raise TypeError( f"{exc_class!r} is an instance, not a class. Handlers" " can only be registered for Exception classes or HTTP" " error codes." ) if not issubclass(exc_class, Exception): raise ValueError( f"'{exc_class.__name__}' is not a subclass of Exception." " Handlers can only be registered for Exception classes" " or HTTP error codes." ) if issubclass(exc_class, HTTPException): return exc_class, exc_class.code else: return exc_class, None def _endpoint_from_view_func(view_func: ft.RouteCallable) -> str: """Internal helper that returns the default endpoint for a given function. This always is the function name. """ assert view_func is not None, "expected view func if endpoint is not provided." return view_func.__name__ def _find_package_path(import_name: str) -> str: """Find the path that contains the package or module.""" root_mod_name, _, _ = import_name.partition(".") try: root_spec = importlib.util.find_spec(root_mod_name) if root_spec is None: raise ValueError("not found") except (ImportError, ValueError): # ImportError: the machinery told us it does not exist # ValueError: # - the module name was invalid # - the module name is __main__ # - we raised `ValueError` due to `root_spec` being `None` return os.getcwd() if root_spec.submodule_search_locations: if root_spec.origin is None or root_spec.origin == "namespace": # namespace package package_spec = importlib.util.find_spec(import_name) if package_spec is not None and package_spec.submodule_search_locations: # Pick the path in the namespace that contains the submodule. package_path = pathlib.Path( os.path.commonpath(package_spec.submodule_search_locations) ) search_location = next( location for location in root_spec.submodule_search_locations if package_path.is_relative_to(location) ) else: # Pick the first path. search_location = root_spec.submodule_search_locations[0] return os.path.dirname(search_location) else: # package with __init__.py return os.path.dirname(os.path.dirname(root_spec.origin)) else: # module return os.path.dirname(root_spec.origin) # type: ignore[type-var, return-value] def find_package(import_name: str) -> tuple[str | None, str]: """Find the prefix that a package is installed under, and the path that it would be imported from. The prefix is the directory containing the standard directory hierarchy (lib, bin, etc.). If the package is not installed to the system (:attr:`sys.prefix`) or a virtualenv (``site-packages``), ``None`` is returned. The path is the entry in :attr:`sys.path` that contains the package for import. If the package is not installed, it's assumed that the package was imported from the current working directory. """ package_path = _find_package_path(import_name) py_prefix = os.path.abspath(sys.prefix) # installed to the system if pathlib.PurePath(package_path).is_relative_to(py_prefix): return py_prefix, package_path site_parent, site_folder = os.path.split(package_path) # installed to a virtualenv if site_folder.lower() == "site-packages": parent, folder = os.path.split(site_parent) # Windows (prefix/lib/site-packages) if folder.lower() == "lib": return parent, package_path # Unix (prefix/lib/pythonX.Y/site-packages) if os.path.basename(parent).lower() == "lib": return os.path.dirname(parent), package_path # something else (prefix/site-packages) return site_parent, package_path # not installed return None, package_path flask-3.1.1/src/flask/sessions.py000066400000000000000000000362071501065614000167310ustar00rootroot00000000000000from __future__ import annotations import collections.abc as c import hashlib import typing as t from collections.abc import MutableMapping from datetime import datetime from datetime import timezone from itsdangerous import BadSignature from itsdangerous import URLSafeTimedSerializer from werkzeug.datastructures import CallbackDict from .json.tag import TaggedJSONSerializer if t.TYPE_CHECKING: # pragma: no cover import typing_extensions as te from .app import Flask from .wrappers import Request from .wrappers import Response class SessionMixin(MutableMapping[str, t.Any]): """Expands a basic dictionary with session attributes.""" @property def permanent(self) -> bool: """This reflects the ``'_permanent'`` key in the dict.""" return self.get("_permanent", False) @permanent.setter def permanent(self, value: bool) -> None: self["_permanent"] = bool(value) #: Some implementations can detect whether a session is newly #: created, but that is not guaranteed. Use with caution. The mixin # default is hard-coded ``False``. new = False #: Some implementations can detect changes to the session and set #: this when that happens. The mixin default is hard coded to #: ``True``. modified = True #: Some implementations can detect when session data is read or #: written and set this when that happens. The mixin default is hard #: coded to ``True``. accessed = True class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): """Base class for sessions based on signed cookies. This session backend will set the :attr:`modified` and :attr:`accessed` attributes. It cannot reliably track whether a session is new (vs. empty), so :attr:`new` remains hard coded to ``False``. """ #: When data is changed, this is set to ``True``. Only the session #: dictionary itself is tracked; if the session contains mutable #: data (for example a nested dict) then this must be set to #: ``True`` manually when modifying that data. The session cookie #: will only be written to the response if this is ``True``. modified = False #: When data is read or written, this is set to ``True``. Used by # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` #: header, which allows caching proxies to cache different pages for #: different users. accessed = False def __init__( self, initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None, ) -> None: def on_update(self: te.Self) -> None: self.modified = True self.accessed = True super().__init__(initial, on_update) def __getitem__(self, key: str) -> t.Any: self.accessed = True return super().__getitem__(key) def get(self, key: str, default: t.Any = None) -> t.Any: self.accessed = True return super().get(key, default) def setdefault(self, key: str, default: t.Any = None) -> t.Any: self.accessed = True return super().setdefault(key, default) class NullSession(SecureCookieSession): """Class used to generate nicer error messages if sessions are not available. Will still allow read-only access to the empty session but fail on setting. """ def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: raise RuntimeError( "The session is unavailable because no secret " "key was set. Set the secret_key on the " "application to something unique and secret." ) __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950 del _fail class SessionInterface: """The basic interface you have to implement in order to replace the default session interface which uses werkzeug's securecookie implementation. The only methods you have to implement are :meth:`open_session` and :meth:`save_session`, the others have useful defaults which you don't need to change. The session object returned by the :meth:`open_session` method has to provide a dictionary like interface plus the properties and methods from the :class:`SessionMixin`. We recommend just subclassing a dict and adding that mixin:: class Session(dict, SessionMixin): pass If :meth:`open_session` returns ``None`` Flask will call into :meth:`make_null_session` to create a session that acts as replacement if the session support cannot work because some requirement is not fulfilled. The default :class:`NullSession` class that is created will complain that the secret key was not set. To replace the session interface on an application all you have to do is to assign :attr:`flask.Flask.session_interface`:: app = Flask(__name__) app.session_interface = MySessionInterface() Multiple requests with the same session may be sent and handled concurrently. When implementing a new session interface, consider whether reads or writes to the backing store must be synchronized. There is no guarantee on the order in which the session for each request is opened or saved, it will occur in the order that requests begin and end processing. .. versionadded:: 0.8 """ #: :meth:`make_null_session` will look here for the class that should #: be created when a null session is requested. Likewise the #: :meth:`is_null_session` method will perform a typecheck against #: this type. null_session_class = NullSession #: A flag that indicates if the session interface is pickle based. #: This can be used by Flask extensions to make a decision in regards #: to how to deal with the session object. #: #: .. versionadded:: 0.10 pickle_based = False def make_null_session(self, app: Flask) -> NullSession: """Creates a null session which acts as a replacement object if the real session support could not be loaded due to a configuration error. This mainly aids the user experience because the job of the null session is to still support lookup without complaining but modifications are answered with a helpful error message of what failed. This creates an instance of :attr:`null_session_class` by default. """ return self.null_session_class() def is_null_session(self, obj: object) -> bool: """Checks if a given object is a null session. Null sessions are not asked to be saved. This checks if the object is an instance of :attr:`null_session_class` by default. """ return isinstance(obj, self.null_session_class) def get_cookie_name(self, app: Flask) -> str: """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return] def get_cookie_domain(self, app: Flask) -> str | None: """The value of the ``Domain`` parameter on the session cookie. If not set, browsers will only send the cookie to the exact domain it was set from. Otherwise, they will send it to any subdomain of the given value as well. Uses the :data:`SESSION_COOKIE_DOMAIN` config. .. versionchanged:: 2.3 Not set by default, does not fall back to ``SERVER_NAME``. """ return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return] def get_cookie_path(self, app: Flask) -> str: """Returns the path for which the cookie should be valid. The default implementation uses the value from the ``SESSION_COOKIE_PATH`` config var if it's set, and falls back to ``APPLICATION_ROOT`` or uses ``/`` if it's ``None``. """ return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return] def get_cookie_httponly(self, app: Flask) -> bool: """Returns True if the session cookie should be httponly. This currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` config var. """ return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return] def get_cookie_secure(self, app: Flask) -> bool: """Returns True if the cookie should be secure. This currently just returns the value of the ``SESSION_COOKIE_SECURE`` setting. """ return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return] def get_cookie_samesite(self, app: Flask) -> str | None: """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the ``SameSite`` attribute. This currently just returns the value of the :data:`SESSION_COOKIE_SAMESITE` setting. """ return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return] def get_cookie_partitioned(self, app: Flask) -> bool: """Returns True if the cookie should be partitioned. By default, uses the value of :data:`SESSION_COOKIE_PARTITIONED`. .. versionadded:: 3.1 """ return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return] def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None: """A helper method that returns an expiration date for the session or ``None`` if the session is linked to the browser session. The default implementation returns now + the permanent session lifetime configured on the application. """ if session.permanent: return datetime.now(timezone.utc) + app.permanent_session_lifetime return None def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool: """Used by session backends to determine if a ``Set-Cookie`` header should be set for this session cookie for this response. If the session has been modified, the cookie is set. If the session is permanent and the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is always set. This check is usually skipped if the session was deleted. .. versionadded:: 0.11 """ return session.modified or ( session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] ) def open_session(self, app: Flask, request: Request) -> SessionMixin | None: """This is called at the beginning of each request, after pushing the request context, before matching the URL. This must return an object which implements a dictionary-like interface as well as the :class:`SessionMixin` interface. This will return ``None`` to indicate that loading failed in some way that is not immediately an error. The request context will fall back to using :meth:`make_null_session` in this case. """ raise NotImplementedError() def save_session( self, app: Flask, session: SessionMixin, response: Response ) -> None: """This is called at the end of each request, after generating a response, before removing the request context. It is skipped if :meth:`is_null_session` returns ``True``. """ raise NotImplementedError() session_json_serializer = TaggedJSONSerializer() def _lazy_sha1(string: bytes = b"") -> t.Any: """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include SHA-1, in which case the import and use as a default would fail before the developer can configure something else. """ return hashlib.sha1(string) class SecureCookieSessionInterface(SessionInterface): """The default session interface that stores sessions in signed cookies through the :mod:`itsdangerous` module. """ #: the salt that should be applied on top of the secret key for the #: signing of cookie based sessions. salt = "cookie-session" #: the hash function to use for the signature. The default is sha1 digest_method = staticmethod(_lazy_sha1) #: the name of the itsdangerous supported key derivation. The default #: is hmac. key_derivation = "hmac" #: A python serializer for the payload. The default is a compact #: JSON derived serializer with support for some extra Python types #: such as datetime objects or tuples. serializer = session_json_serializer session_class = SecureCookieSession def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None: if not app.secret_key: return None keys: list[str | bytes] = [] if fallbacks := app.config["SECRET_KEY_FALLBACKS"]: keys.extend(fallbacks) keys.append(app.secret_key) # itsdangerous expects current key at top return URLSafeTimedSerializer( keys, # type: ignore[arg-type] salt=self.salt, serializer=self.serializer, signer_kwargs={ "key_derivation": self.key_derivation, "digest_method": self.digest_method, }, ) def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None: s = self.get_signing_serializer(app) if s is None: return None val = request.cookies.get(self.get_cookie_name(app)) if not val: return self.session_class() max_age = int(app.permanent_session_lifetime.total_seconds()) try: data = s.loads(val, max_age=max_age) return self.session_class(data) except BadSignature: return self.session_class() def save_session( self, app: Flask, session: SessionMixin, response: Response ) -> None: name = self.get_cookie_name(app) domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) secure = self.get_cookie_secure(app) partitioned = self.get_cookie_partitioned(app) samesite = self.get_cookie_samesite(app) httponly = self.get_cookie_httponly(app) # Add a "Vary: Cookie" header if the session was accessed at all. if session.accessed: response.vary.add("Cookie") # If the session is modified to be empty, remove the cookie. # If the session is empty, return without setting the cookie. if not session: if session.modified: response.delete_cookie( name, domain=domain, path=path, secure=secure, partitioned=partitioned, samesite=samesite, httponly=httponly, ) response.vary.add("Cookie") return if not self.should_set_cookie(app, session): return expires = self.get_expiration_time(app, session) val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr] response.set_cookie( name, val, expires=expires, httponly=httponly, domain=domain, path=path, secure=secure, partitioned=partitioned, samesite=samesite, ) response.vary.add("Cookie") flask-3.1.1/src/flask/signals.py000066400000000000000000000013561501065614000165200ustar00rootroot00000000000000from __future__ import annotations from blinker import Namespace # This namespace is only for signals provided by Flask itself. _signals = Namespace() template_rendered = _signals.signal("template-rendered") before_render_template = _signals.signal("before-render-template") request_started = _signals.signal("request-started") request_finished = _signals.signal("request-finished") request_tearing_down = _signals.signal("request-tearing-down") got_request_exception = _signals.signal("got-request-exception") appcontext_tearing_down = _signals.signal("appcontext-tearing-down") appcontext_pushed = _signals.signal("appcontext-pushed") appcontext_popped = _signals.signal("appcontext-popped") message_flashed = _signals.signal("message-flashed") flask-3.1.1/src/flask/templating.py000066400000000000000000000165611501065614000172300ustar00rootroot00000000000000from __future__ import annotations import typing as t from jinja2 import BaseLoader from jinja2 import Environment as BaseEnvironment from jinja2 import Template from jinja2 import TemplateNotFound from .globals import _cv_app from .globals import _cv_request from .globals import current_app from .globals import request from .helpers import stream_with_context from .signals import before_render_template from .signals import template_rendered if t.TYPE_CHECKING: # pragma: no cover from .app import Flask from .sansio.app import App from .sansio.scaffold import Scaffold def _default_template_ctx_processor() -> dict[str, t.Any]: """Default template context processor. Injects `request`, `session` and `g`. """ appctx = _cv_app.get(None) reqctx = _cv_request.get(None) rv: dict[str, t.Any] = {} if appctx is not None: rv["g"] = appctx.g if reqctx is not None: rv["request"] = reqctx.request rv["session"] = reqctx.session return rv class Environment(BaseEnvironment): """Works like a regular Jinja2 environment but has some additional knowledge of how Flask's blueprint works so that it can prepend the name of the blueprint to referenced templates if necessary. """ def __init__(self, app: App, **options: t.Any) -> None: if "loader" not in options: options["loader"] = app.create_global_jinja_loader() BaseEnvironment.__init__(self, **options) self.app = app class DispatchingJinjaLoader(BaseLoader): """A loader that looks for templates in the application and all the blueprint folders. """ def __init__(self, app: App) -> None: self.app = app def get_source( self, environment: BaseEnvironment, template: str ) -> tuple[str, str | None, t.Callable[[], bool] | None]: if self.app.config["EXPLAIN_TEMPLATE_LOADING"]: return self._get_source_explained(environment, template) return self._get_source_fast(environment, template) def _get_source_explained( self, environment: BaseEnvironment, template: str ) -> tuple[str, str | None, t.Callable[[], bool] | None]: attempts = [] rv: tuple[str, str | None, t.Callable[[], bool] | None] | None trv: None | (tuple[str, str | None, t.Callable[[], bool] | None]) = None for srcobj, loader in self._iter_loaders(template): try: rv = loader.get_source(environment, template) if trv is None: trv = rv except TemplateNotFound: rv = None attempts.append((loader, srcobj, rv)) from .debughelpers import explain_template_loading_attempts explain_template_loading_attempts(self.app, template, attempts) if trv is not None: return trv raise TemplateNotFound(template) def _get_source_fast( self, environment: BaseEnvironment, template: str ) -> tuple[str, str | None, t.Callable[[], bool] | None]: for _srcobj, loader in self._iter_loaders(template): try: return loader.get_source(environment, template) except TemplateNotFound: continue raise TemplateNotFound(template) def _iter_loaders(self, template: str) -> t.Iterator[tuple[Scaffold, BaseLoader]]: loader = self.app.jinja_loader if loader is not None: yield self.app, loader for blueprint in self.app.iter_blueprints(): loader = blueprint.jinja_loader if loader is not None: yield blueprint, loader def list_templates(self) -> list[str]: result = set() loader = self.app.jinja_loader if loader is not None: result.update(loader.list_templates()) for blueprint in self.app.iter_blueprints(): loader = blueprint.jinja_loader if loader is not None: for template in loader.list_templates(): result.add(template) return list(result) def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str: app.update_template_context(context) before_render_template.send( app, _async_wrapper=app.ensure_sync, template=template, context=context ) rv = template.render(context) template_rendered.send( app, _async_wrapper=app.ensure_sync, template=template, context=context ) return rv def render_template( template_name_or_list: str | Template | list[str | Template], **context: t.Any, ) -> str: """Render a template by name with the given context. :param template_name_or_list: The name of the template to render. If a list is given, the first name to exist will be rendered. :param context: The variables to make available in the template. """ app = current_app._get_current_object() # type: ignore[attr-defined] template = app.jinja_env.get_or_select_template(template_name_or_list) return _render(app, template, context) def render_template_string(source: str, **context: t.Any) -> str: """Render a template from the given source string with the given context. :param source: The source code of the template to render. :param context: The variables to make available in the template. """ app = current_app._get_current_object() # type: ignore[attr-defined] template = app.jinja_env.from_string(source) return _render(app, template, context) def _stream( app: Flask, template: Template, context: dict[str, t.Any] ) -> t.Iterator[str]: app.update_template_context(context) before_render_template.send( app, _async_wrapper=app.ensure_sync, template=template, context=context ) def generate() -> t.Iterator[str]: yield from template.generate(context) template_rendered.send( app, _async_wrapper=app.ensure_sync, template=template, context=context ) rv = generate() # If a request context is active, keep it while generating. if request: rv = stream_with_context(rv) return rv def stream_template( template_name_or_list: str | Template | list[str | Template], **context: t.Any, ) -> t.Iterator[str]: """Render a template by name with the given context as a stream. This returns an iterator of strings, which can be used as a streaming response from a view. :param template_name_or_list: The name of the template to render. If a list is given, the first name to exist will be rendered. :param context: The variables to make available in the template. .. versionadded:: 2.2 """ app = current_app._get_current_object() # type: ignore[attr-defined] template = app.jinja_env.get_or_select_template(template_name_or_list) return _stream(app, template, context) def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]: """Render a template from the given source string with the given context as a stream. This returns an iterator of strings, which can be used as a streaming response from a view. :param source: The source code of the template to render. :param context: The variables to make available in the template. .. versionadded:: 2.2 """ app = current_app._get_current_object() # type: ignore[attr-defined] template = app.jinja_env.from_string(source) return _stream(app, template, context) flask-3.1.1/src/flask/testing.py000066400000000000000000000236301501065614000165340ustar00rootroot00000000000000from __future__ import annotations import importlib.metadata import typing as t from contextlib import contextmanager from contextlib import ExitStack from copy import copy from types import TracebackType from urllib.parse import urlsplit import werkzeug.test from click.testing import CliRunner from click.testing import Result from werkzeug.test import Client from werkzeug.wrappers import Request as BaseRequest from .cli import ScriptInfo from .sessions import SessionMixin if t.TYPE_CHECKING: # pragma: no cover from _typeshed.wsgi import WSGIEnvironment from werkzeug.test import TestResponse from .app import Flask class EnvironBuilder(werkzeug.test.EnvironBuilder): """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the application. :param app: The Flask application to configure the environment from. :param path: URL path being requested. :param base_url: Base URL where the app is being served, which ``path`` is relative to. If not given, built from :data:`PREFERRED_URL_SCHEME`, ``subdomain``, :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. :param subdomain: Subdomain name to append to :data:`SERVER_NAME`. :param url_scheme: Scheme to use instead of :data:`PREFERRED_URL_SCHEME`. :param json: If given, this is serialized as JSON and passed as ``data``. Also defaults ``content_type`` to ``application/json``. :param args: other positional arguments passed to :class:`~werkzeug.test.EnvironBuilder`. :param kwargs: other keyword arguments passed to :class:`~werkzeug.test.EnvironBuilder`. """ def __init__( self, app: Flask, path: str = "/", base_url: str | None = None, subdomain: str | None = None, url_scheme: str | None = None, *args: t.Any, **kwargs: t.Any, ) -> None: assert not (base_url or subdomain or url_scheme) or ( base_url is not None ) != bool(subdomain or url_scheme), ( 'Cannot pass "subdomain" or "url_scheme" with "base_url".' ) if base_url is None: http_host = app.config.get("SERVER_NAME") or "localhost" app_root = app.config["APPLICATION_ROOT"] if subdomain: http_host = f"{subdomain}.{http_host}" if url_scheme is None: url_scheme = app.config["PREFERRED_URL_SCHEME"] url = urlsplit(path) base_url = ( f"{url.scheme or url_scheme}://{url.netloc or http_host}" f"/{app_root.lstrip('/')}" ) path = url.path if url.query: path = f"{path}?{url.query}" self.app = app super().__init__(path, base_url, *args, **kwargs) def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore """Serialize ``obj`` to a JSON-formatted string. The serialization will be configured according to the config associated with this EnvironBuilder's ``app``. """ return self.app.json.dumps(obj, **kwargs) _werkzeug_version = "" def _get_werkzeug_version() -> str: global _werkzeug_version if not _werkzeug_version: _werkzeug_version = importlib.metadata.version("werkzeug") return _werkzeug_version class FlaskClient(Client): """Works like a regular Werkzeug test client but has knowledge about Flask's contexts to defer the cleanup of the request context until the end of a ``with`` block. For general information about how to use this class refer to :class:`werkzeug.test.Client`. .. versionchanged:: 0.12 `app.test_client()` includes preset default environment, which can be set after instantiation of the `app.test_client()` object in `client.environ_base`. Basic usage is outlined in the :doc:`/testing` chapter. """ application: Flask def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: super().__init__(*args, **kwargs) self.preserve_context = False self._new_contexts: list[t.ContextManager[t.Any]] = [] self._context_stack = ExitStack() self.environ_base = { "REMOTE_ADDR": "127.0.0.1", "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}", } @contextmanager def session_transaction( self, *args: t.Any, **kwargs: t.Any ) -> t.Iterator[SessionMixin]: """When used in combination with a ``with`` statement this opens a session transaction. This can be used to modify the session that the test client uses. Once the ``with`` block is left the session is stored back. :: with client.session_transaction() as session: session['value'] = 42 Internally this is implemented by going through a temporary test request context and since session handling could depend on request variables this function accepts the same arguments as :meth:`~flask.Flask.test_request_context` which are directly passed through. """ if self._cookies is None: raise TypeError( "Cookies are disabled. Create a client with 'use_cookies=True'." ) app = self.application ctx = app.test_request_context(*args, **kwargs) self._add_cookies_to_wsgi(ctx.request.environ) with ctx: sess = app.session_interface.open_session(app, ctx.request) if sess is None: raise RuntimeError("Session backend did not open a session.") yield sess resp = app.response_class() if app.session_interface.is_null_session(sess): return with ctx: app.session_interface.save_session(app, sess, resp) self._update_cookies_from_response( ctx.request.host.partition(":")[0], ctx.request.path, resp.headers.getlist("Set-Cookie"), ) def _copy_environ(self, other: WSGIEnvironment) -> WSGIEnvironment: out = {**self.environ_base, **other} if self.preserve_context: out["werkzeug.debug.preserve_context"] = self._new_contexts.append return out def _request_from_builder_args( self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] ) -> BaseRequest: kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {})) builder = EnvironBuilder(self.application, *args, **kwargs) try: return builder.get_request() finally: builder.close() def open( self, *args: t.Any, buffered: bool = False, follow_redirects: bool = False, **kwargs: t.Any, ) -> TestResponse: if args and isinstance( args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest) ): if isinstance(args[0], werkzeug.test.EnvironBuilder): builder = copy(args[0]) builder.environ_base = self._copy_environ(builder.environ_base or {}) # type: ignore[arg-type] request = builder.get_request() elif isinstance(args[0], dict): request = EnvironBuilder.from_environ( args[0], app=self.application, environ_base=self._copy_environ({}) ).get_request() else: # isinstance(args[0], BaseRequest) request = copy(args[0]) request.environ = self._copy_environ(request.environ) else: # request is None request = self._request_from_builder_args(args, kwargs) # Pop any previously preserved contexts. This prevents contexts # from being preserved across redirects or multiple requests # within a single block. self._context_stack.close() response = super().open( request, buffered=buffered, follow_redirects=follow_redirects, ) response.json_module = self.application.json # type: ignore[assignment] # Re-push contexts that were preserved during the request. while self._new_contexts: cm = self._new_contexts.pop() self._context_stack.enter_context(cm) return response def __enter__(self) -> FlaskClient: if self.preserve_context: raise RuntimeError("Cannot nest client invocations") self.preserve_context = True return self def __exit__( self, exc_type: type | None, exc_value: BaseException | None, tb: TracebackType | None, ) -> None: self.preserve_context = False self._context_stack.close() class FlaskCliRunner(CliRunner): """A :class:`~click.testing.CliRunner` for testing a Flask app's CLI commands. Typically created using :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`. """ def __init__(self, app: Flask, **kwargs: t.Any) -> None: self.app = app super().__init__(**kwargs) def invoke( # type: ignore self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any ) -> Result: """Invokes a CLI command in an isolated environment. See :meth:`CliRunner.invoke ` for full method documentation. See :ref:`testing-cli` for examples. If the ``obj`` argument is not given, passes an instance of :class:`~flask.cli.ScriptInfo` that knows how to load the Flask app being tested. :param cli: Command object to invoke. Default is the app's :attr:`~flask.app.Flask.cli` group. :param args: List of strings to invoke the command with. :return: a :class:`~click.testing.Result` object. """ if cli is None: cli = self.app.cli if "obj" not in kwargs: kwargs["obj"] = ScriptInfo(create_app=lambda: self.app) return super().invoke(cli, args, **kwargs) flask-3.1.1/src/flask/typing.py000066400000000000000000000063351501065614000163740ustar00rootroot00000000000000from __future__ import annotations import collections.abc as cabc import typing as t if t.TYPE_CHECKING: # pragma: no cover from _typeshed.wsgi import WSGIApplication # noqa: F401 from werkzeug.datastructures import Headers # noqa: F401 from werkzeug.sansio.response import Response # noqa: F401 # The possible types that are directly convertible or are a Response object. ResponseValue = t.Union[ "Response", str, bytes, list[t.Any], # Only dict is actually accepted, but Mapping allows for TypedDict. t.Mapping[str, t.Any], t.Iterator[str], t.Iterator[bytes], cabc.AsyncIterable[str], # for Quart, until App is generic. cabc.AsyncIterable[bytes], ] # the possible types for an individual HTTP header # This should be a Union, but mypy doesn't pass unless it's a TypeVar. HeaderValue = t.Union[str, list[str], tuple[str, ...]] # the possible types for HTTP headers HeadersValue = t.Union[ "Headers", t.Mapping[str, HeaderValue], t.Sequence[tuple[str, HeaderValue]], ] # The possible types returned by a route function. ResponseReturnValue = t.Union[ ResponseValue, tuple[ResponseValue, HeadersValue], tuple[ResponseValue, int], tuple[ResponseValue, int, HeadersValue], "WSGIApplication", ] # Allow any subclass of werkzeug.Response, such as the one from Flask, # as a callback argument. Using werkzeug.Response directly makes a # callback annotated with flask.Response fail type checking. ResponseClass = t.TypeVar("ResponseClass", bound="Response") AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named AfterRequestCallable = t.Union[ t.Callable[[ResponseClass], ResponseClass], t.Callable[[ResponseClass], t.Awaitable[ResponseClass]], ] BeforeFirstRequestCallable = t.Union[ t.Callable[[], None], t.Callable[[], t.Awaitable[None]] ] BeforeRequestCallable = t.Union[ t.Callable[[], t.Optional[ResponseReturnValue]], t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]], ] ShellContextProcessorCallable = t.Callable[[], dict[str, t.Any]] TeardownCallable = t.Union[ t.Callable[[t.Optional[BaseException]], None], t.Callable[[t.Optional[BaseException]], t.Awaitable[None]], ] TemplateContextProcessorCallable = t.Union[ t.Callable[[], dict[str, t.Any]], t.Callable[[], t.Awaitable[dict[str, t.Any]]], ] TemplateFilterCallable = t.Callable[..., t.Any] TemplateGlobalCallable = t.Callable[..., t.Any] TemplateTestCallable = t.Callable[..., bool] URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None] URLValuePreprocessorCallable = t.Callable[ [t.Optional[str], t.Optional[dict[str, t.Any]]], None ] # This should take Exception, but that either breaks typing the argument # with a specific exception, or decorating multiple times with different # exceptions (and using a union type on the argument). # https://github.com/pallets/flask/issues/4095 # https://github.com/pallets/flask/issues/4295 # https://github.com/pallets/flask/issues/4297 ErrorHandlerCallable = t.Union[ t.Callable[[t.Any], ResponseReturnValue], t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]], ] RouteCallable = t.Union[ t.Callable[..., ResponseReturnValue], t.Callable[..., t.Awaitable[ResponseReturnValue]], ] flask-3.1.1/src/flask/views.py000066400000000000000000000154621501065614000162200ustar00rootroot00000000000000from __future__ import annotations import typing as t from . import typing as ft from .globals import current_app from .globals import request F = t.TypeVar("F", bound=t.Callable[..., t.Any]) http_method_funcs = frozenset( ["get", "post", "head", "options", "delete", "put", "trace", "patch"] ) class View: """Subclass this class and override :meth:`dispatch_request` to create a generic class-based view. Call :meth:`as_view` to create a view function that creates an instance of the class with the given arguments and calls its ``dispatch_request`` method with any URL variables. See :doc:`views` for a detailed guide. .. code-block:: python class Hello(View): init_every_request = False def dispatch_request(self, name): return f"Hello, {name}!" app.add_url_rule( "/hello/", view_func=Hello.as_view("hello") ) Set :attr:`methods` on the class to change what methods the view accepts. Set :attr:`decorators` on the class to apply a list of decorators to the generated view function. Decorators applied to the class itself will not be applied to the generated view function! Set :attr:`init_every_request` to ``False`` for efficiency, unless you need to store request-global data on ``self``. """ #: The methods this view is registered for. Uses the same default #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and #: ``add_url_rule`` by default. methods: t.ClassVar[t.Collection[str] | None] = None #: Control whether the ``OPTIONS`` method is handled automatically. #: Uses the same default (``True``) as ``route`` and #: ``add_url_rule`` by default. provide_automatic_options: t.ClassVar[bool | None] = None #: A list of decorators to apply, in order, to the generated view #: function. Remember that ``@decorator`` syntax is applied bottom #: to top, so the first decorator in the list would be the bottom #: decorator. #: #: .. versionadded:: 0.8 decorators: t.ClassVar[list[t.Callable[..., t.Any]]] = [] #: Create a new instance of this view class for every request by #: default. If a view subclass sets this to ``False``, the same #: instance is used for every request. #: #: A single instance is more efficient, especially if complex setup #: is done during init. However, storing data on ``self`` is no #: longer safe across requests, and :data:`~flask.g` should be used #: instead. #: #: .. versionadded:: 2.2 init_every_request: t.ClassVar[bool] = True def dispatch_request(self) -> ft.ResponseReturnValue: """The actual view function behavior. Subclasses must override this and return a valid response. Any variables from the URL rule are passed as keyword arguments. """ raise NotImplementedError() @classmethod def as_view( cls, name: str, *class_args: t.Any, **class_kwargs: t.Any ) -> ft.RouteCallable: """Convert the class into a view function that can be registered for a route. By default, the generated view will create a new instance of the view class for every request and call its :meth:`dispatch_request` method. If the view class sets :attr:`init_every_request` to ``False``, the same instance will be used for every request. Except for ``name``, all other arguments passed to this method are forwarded to the view class ``__init__`` method. .. versionchanged:: 2.2 Added the ``init_every_request`` class attribute. """ if cls.init_every_request: def view(**kwargs: t.Any) -> ft.ResponseReturnValue: self = view.view_class( # type: ignore[attr-defined] *class_args, **class_kwargs ) return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] else: self = cls(*class_args, **class_kwargs) # pyright: ignore def view(**kwargs: t.Any) -> ft.ResponseReturnValue: return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] if cls.decorators: view.__name__ = name view.__module__ = cls.__module__ for decorator in cls.decorators: view = decorator(view) # We attach the view class to the view function for two reasons: # first of all it allows us to easily figure out what class-based # view this thing came from, secondly it's also used for instantiating # the view class so you can actually replace it with something else # for testing purposes and debugging. view.view_class = cls # type: ignore view.__name__ = name view.__doc__ = cls.__doc__ view.__module__ = cls.__module__ view.methods = cls.methods # type: ignore view.provide_automatic_options = cls.provide_automatic_options # type: ignore return view class MethodView(View): """Dispatches request methods to the corresponding instance methods. For example, if you implement a ``get`` method, it will be used to handle ``GET`` requests. This can be useful for defining a REST API. :attr:`methods` is automatically set based on the methods defined on the class. See :doc:`views` for a detailed guide. .. code-block:: python class CounterAPI(MethodView): def get(self): return str(session.get("counter", 0)) def post(self): session["counter"] = session.get("counter", 0) + 1 return redirect(url_for("counter")) app.add_url_rule( "/counter", view_func=CounterAPI.as_view("counter") ) """ def __init_subclass__(cls, **kwargs: t.Any) -> None: super().__init_subclass__(**kwargs) if "methods" not in cls.__dict__: methods = set() for base in cls.__bases__: if getattr(base, "methods", None): methods.update(base.methods) # type: ignore[attr-defined] for key in http_method_funcs: if hasattr(cls, key): methods.add(key.upper()) if methods: cls.methods = methods def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue: meth = getattr(self, request.method.lower(), None) # If the request method is HEAD and we don't have a handler for it # retry with GET. if meth is None and request.method == "HEAD": meth = getattr(self, "get", None) assert meth is not None, f"Unimplemented method {request.method!r}" return current_app.ensure_sync(meth)(**kwargs) # type: ignore[no-any-return] flask-3.1.1/src/flask/wrappers.py000066400000000000000000000222761501065614000167270ustar00rootroot00000000000000from __future__ import annotations import typing as t from werkzeug.exceptions import BadRequest from werkzeug.exceptions import HTTPException from werkzeug.wrappers import Request as RequestBase from werkzeug.wrappers import Response as ResponseBase from . import json from .globals import current_app from .helpers import _split_blueprint_path if t.TYPE_CHECKING: # pragma: no cover from werkzeug.routing import Rule class Request(RequestBase): """The request object used by default in Flask. Remembers the matched endpoint and view arguments. It is what ends up as :class:`~flask.request`. If you want to replace the request object used you can subclass this and set :attr:`~flask.Flask.request_class` to your subclass. The request object is a :class:`~werkzeug.wrappers.Request` subclass and provides all of the attributes Werkzeug defines plus a few Flask specific ones. """ json_module: t.Any = json #: The internal URL rule that matched the request. This can be #: useful to inspect which methods are allowed for the URL from #: a before/after handler (``request.url_rule.methods``) etc. #: Though if the request's method was invalid for the URL rule, #: the valid list is available in ``routing_exception.valid_methods`` #: instead (an attribute of the Werkzeug exception #: :exc:`~werkzeug.exceptions.MethodNotAllowed`) #: because the request was never internally bound. #: #: .. versionadded:: 0.6 url_rule: Rule | None = None #: A dict of view arguments that matched the request. If an exception #: happened when matching, this will be ``None``. view_args: dict[str, t.Any] | None = None #: If matching the URL failed, this is the exception that will be #: raised / was raised as part of the request handling. This is #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or #: something similar. routing_exception: HTTPException | None = None _max_content_length: int | None = None _max_form_memory_size: int | None = None _max_form_parts: int | None = None @property def max_content_length(self) -> int | None: """The maximum number of bytes that will be read during this request. If this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it is set to ``None``, no limit is enforced at the Flask application level. However, if it is ``None`` and the request has no ``Content-Length`` header and the WSGI server does not indicate that it terminates the stream, then no data is read to avoid an infinite stream. Each request defaults to the :data:`MAX_CONTENT_LENGTH` config, which defaults to ``None``. It can be set on a specific ``request`` to apply the limit to that specific view. This should be set appropriately based on an application's or view's specific needs. .. versionchanged:: 3.1 This can be set per-request. .. versionchanged:: 0.6 This is configurable through Flask config. """ if self._max_content_length is not None: return self._max_content_length if not current_app: return super().max_content_length return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return] @max_content_length.setter def max_content_length(self, value: int | None) -> None: self._max_content_length = value @property def max_form_memory_size(self) -> int | None: """The maximum size in bytes any non-file form field may be in a ``multipart/form-data`` body. If this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it is set to ``None``, no limit is enforced at the Flask application level. Each request defaults to the :data:`MAX_FORM_MEMORY_SIZE` config, which defaults to ``500_000``. It can be set on a specific ``request`` to apply the limit to that specific view. This should be set appropriately based on an application's or view's specific needs. .. versionchanged:: 3.1 This is configurable through Flask config. """ if self._max_form_memory_size is not None: return self._max_form_memory_size if not current_app: return super().max_form_memory_size return current_app.config["MAX_FORM_MEMORY_SIZE"] # type: ignore[no-any-return] @max_form_memory_size.setter def max_form_memory_size(self, value: int | None) -> None: self._max_form_memory_size = value @property # type: ignore[override] def max_form_parts(self) -> int | None: """The maximum number of fields that may be present in a ``multipart/form-data`` body. If this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it is set to ``None``, no limit is enforced at the Flask application level. Each request defaults to the :data:`MAX_FORM_PARTS` config, which defaults to ``1_000``. It can be set on a specific ``request`` to apply the limit to that specific view. This should be set appropriately based on an application's or view's specific needs. .. versionchanged:: 3.1 This is configurable through Flask config. """ if self._max_form_parts is not None: return self._max_form_parts if not current_app: return super().max_form_parts return current_app.config["MAX_FORM_PARTS"] # type: ignore[no-any-return] @max_form_parts.setter def max_form_parts(self, value: int | None) -> None: self._max_form_parts = value @property def endpoint(self) -> str | None: """The endpoint that matched the request URL. This will be ``None`` if matching failed or has not been performed yet. This in combination with :attr:`view_args` can be used to reconstruct the same URL or a modified URL. """ if self.url_rule is not None: return self.url_rule.endpoint # type: ignore[no-any-return] return None @property def blueprint(self) -> str | None: """The registered name of the current blueprint. This will be ``None`` if the endpoint is not part of a blueprint, or if URL matching failed or has not been performed yet. This does not necessarily match the name the blueprint was created with. It may have been nested, or registered with a different name. """ endpoint = self.endpoint if endpoint is not None and "." in endpoint: return endpoint.rpartition(".")[0] return None @property def blueprints(self) -> list[str]: """The registered names of the current blueprint upwards through parent blueprints. This will be an empty list if there is no current blueprint, or if URL matching failed. .. versionadded:: 2.0.1 """ name = self.blueprint if name is None: return [] return _split_blueprint_path(name) def _load_form_data(self) -> None: super()._load_form_data() # In debug mode we're replacing the files multidict with an ad-hoc # subclass that raises a different error for key errors. if ( current_app and current_app.debug and self.mimetype != "multipart/form-data" and not self.files ): from .debughelpers import attach_enctype_error_multidict attach_enctype_error_multidict(self) def on_json_loading_failed(self, e: ValueError | None) -> t.Any: try: return super().on_json_loading_failed(e) except BadRequest as ebr: if current_app and current_app.debug: raise raise BadRequest() from ebr class Response(ResponseBase): """The response object that is used by default in Flask. Works like the response object from Werkzeug but is set to have an HTML mimetype by default. Quite often you don't have to create this object yourself because :meth:`~flask.Flask.make_response` will take care of that for you. If you want to replace the response object used you can subclass this and set :attr:`~flask.Flask.response_class` to your subclass. .. versionchanged:: 1.0 JSON support is added to the response, like the request. This is useful when testing to get the test client response data as JSON. .. versionchanged:: 1.0 Added :attr:`max_cookie_size`. """ default_mimetype: str | None = "text/html" json_module = json autocorrect_location_header = False @property def max_cookie_size(self) -> int: # type: ignore """Read-only view of the :data:`MAX_COOKIE_SIZE` config key. See :attr:`~werkzeug.wrappers.Response.max_cookie_size` in Werkzeug's docs. """ if current_app: return current_app.config["MAX_COOKIE_SIZE"] # type: ignore[no-any-return] # return Werkzeug's default when not in an app context return super().max_cookie_size flask-3.1.1/tests/000077500000000000000000000000001501065614000137545ustar00rootroot00000000000000flask-3.1.1/tests/conftest.py000066400000000000000000000063251501065614000161610ustar00rootroot00000000000000import os import sys import pytest from _pytest import monkeypatch from flask import Flask from flask.globals import request_ctx @pytest.fixture(scope="session", autouse=True) def _standard_os_environ(): """Set up ``os.environ`` at the start of the test session to have standard values. Returns a list of operations that is used by :func:`._reset_os_environ` after each test. """ mp = monkeypatch.MonkeyPatch() out = ( (os.environ, "FLASK_ENV_FILE", monkeypatch.notset), (os.environ, "FLASK_APP", monkeypatch.notset), (os.environ, "FLASK_DEBUG", monkeypatch.notset), (os.environ, "FLASK_RUN_FROM_CLI", monkeypatch.notset), (os.environ, "WERKZEUG_RUN_MAIN", monkeypatch.notset), ) for _, key, value in out: if value is monkeypatch.notset: mp.delenv(key, False) else: mp.setenv(key, value) yield out mp.undo() @pytest.fixture(autouse=True) def _reset_os_environ(monkeypatch, _standard_os_environ): """Reset ``os.environ`` to the standard environ after each test, in case a test changed something without cleaning up. """ monkeypatch._setitem.extend(_standard_os_environ) @pytest.fixture def app(): app = Flask("flask_test", root_path=os.path.dirname(__file__)) app.config.update( TESTING=True, SECRET_KEY="test key", ) return app @pytest.fixture def app_ctx(app): with app.app_context() as ctx: yield ctx @pytest.fixture def req_ctx(app): with app.test_request_context() as ctx: yield ctx @pytest.fixture def client(app): return app.test_client() @pytest.fixture def test_apps(monkeypatch): monkeypatch.syspath_prepend(os.path.join(os.path.dirname(__file__), "test_apps")) original_modules = set(sys.modules.keys()) yield # Remove any imports cached during the test. Otherwise "import app" # will work in the next test even though it's no longer on the path. for key in sys.modules.keys() - original_modules: sys.modules.pop(key) @pytest.fixture(autouse=True) def leak_detector(): yield # make sure we're not leaking a request context since we are # testing flask internally in debug mode in a few cases leaks = [] while request_ctx: leaks.append(request_ctx._get_current_object()) request_ctx.pop() assert leaks == [] @pytest.fixture def modules_tmp_path(tmp_path, monkeypatch): """A temporary directory added to sys.path.""" rv = tmp_path / "modules_tmp" rv.mkdir() monkeypatch.syspath_prepend(os.fspath(rv)) return rv @pytest.fixture def modules_tmp_path_prefix(modules_tmp_path, monkeypatch): monkeypatch.setattr(sys, "prefix", os.fspath(modules_tmp_path)) return modules_tmp_path @pytest.fixture def site_packages(modules_tmp_path, monkeypatch): """Create a fake site-packages.""" py_dir = f"python{sys.version_info.major}.{sys.version_info.minor}" rv = modules_tmp_path / "lib" / py_dir / "site-packages" rv.mkdir(parents=True) monkeypatch.syspath_prepend(os.fspath(rv)) return rv @pytest.fixture def purge_module(request): def inner(name): request.addfinalizer(lambda: sys.modules.pop(name, None)) return inner flask-3.1.1/tests/static/000077500000000000000000000000001501065614000152435ustar00rootroot00000000000000flask-3.1.1/tests/static/config.json000066400000000000000000000000661501065614000174050ustar00rootroot00000000000000{ "TEST_KEY": "foo", "SECRET_KEY": "config" } flask-3.1.1/tests/static/config.toml000066400000000000000000000000431501065614000174020ustar00rootroot00000000000000TEST_KEY="foo" SECRET_KEY="config" flask-3.1.1/tests/static/index.html000066400000000000000000000000261501065614000172360ustar00rootroot00000000000000

Hello World!

flask-3.1.1/tests/templates/000077500000000000000000000000001501065614000157525ustar00rootroot00000000000000flask-3.1.1/tests/templates/_macro.html000066400000000000000000000000671501065614000201030ustar00rootroot00000000000000{% macro hello(name) %}Hello {{ name }}!{% endmacro %} flask-3.1.1/tests/templates/context_template.html000066400000000000000000000000441501065614000222150ustar00rootroot00000000000000

{{ value }}|{{ injected_value }} flask-3.1.1/tests/templates/escaping_template.html000066400000000000000000000002231501065614000223210ustar00rootroot00000000000000{{ text }} {{ html }} {% autoescape false %}{{ text }} {{ html }}{% endautoescape %} {% autoescape true %}{{ text }} {{ html }}{% endautoescape %} flask-3.1.1/tests/templates/mail.txt000066400000000000000000000000161501065614000174320ustar00rootroot00000000000000{{ foo}} Mail flask-3.1.1/tests/templates/nested/000077500000000000000000000000001501065614000172345ustar00rootroot00000000000000flask-3.1.1/tests/templates/nested/nested.txt000066400000000000000000000000131501065614000212510ustar00rootroot00000000000000I'm nested flask-3.1.1/tests/templates/non_escaping_template.txt000066400000000000000000000002511501065614000230470ustar00rootroot00000000000000{{ text }} {{ html }} {% autoescape false %}{{ text }} {{ html }}{% endautoescape %} {% autoescape true %}{{ text }} {{ html }}{% endautoescape %} {{ text }} {{ html }} flask-3.1.1/tests/templates/simple_template.html000066400000000000000000000000271501065614000220230ustar00rootroot00000000000000

{{ whiskey }}

flask-3.1.1/tests/templates/template_filter.html000066400000000000000000000000321501065614000220130ustar00rootroot00000000000000{{ value|super_reverse }} flask-3.1.1/tests/templates/template_test.html000066400000000000000000000000631501065614000215110ustar00rootroot00000000000000{% if value is boolean %} Success! {% endif %} flask-3.1.1/tests/test_appctx.py000066400000000000000000000115621501065614000166710ustar00rootroot00000000000000import pytest import flask from flask.globals import app_ctx from flask.globals import request_ctx def test_basic_url_generation(app): app.config["SERVER_NAME"] = "localhost" app.config["PREFERRED_URL_SCHEME"] = "https" @app.route("/") def index(): pass with app.app_context(): rv = flask.url_for("index") assert rv == "https://localhost/" def test_url_generation_requires_server_name(app): with app.app_context(): with pytest.raises(RuntimeError): flask.url_for("index") def test_url_generation_without_context_fails(): with pytest.raises(RuntimeError): flask.url_for("index") def test_request_context_means_app_context(app): with app.test_request_context(): assert flask.current_app._get_current_object() is app assert not flask.current_app def test_app_context_provides_current_app(app): with app.app_context(): assert flask.current_app._get_current_object() is app assert not flask.current_app def test_app_tearing_down(app): cleanup_stuff = [] @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) with app.app_context(): pass assert cleanup_stuff == [None] def test_app_tearing_down_with_previous_exception(app): cleanup_stuff = [] @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) try: raise Exception("dummy") except Exception: pass with app.app_context(): pass assert cleanup_stuff == [None] def test_app_tearing_down_with_handled_exception_by_except_block(app): cleanup_stuff = [] @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) with app.app_context(): try: raise Exception("dummy") except Exception: pass assert cleanup_stuff == [None] def test_app_tearing_down_with_handled_exception_by_app_handler(app, client): app.config["PROPAGATE_EXCEPTIONS"] = True cleanup_stuff = [] @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) @app.route("/") def index(): raise Exception("dummy") @app.errorhandler(Exception) def handler(f): return flask.jsonify(str(f)) with app.app_context(): client.get("/") assert cleanup_stuff == [None] def test_app_tearing_down_with_unhandled_exception(app, client): app.config["PROPAGATE_EXCEPTIONS"] = True cleanup_stuff = [] @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) @app.route("/") def index(): raise ValueError("dummy") with pytest.raises(ValueError, match="dummy"): with app.app_context(): client.get("/") assert len(cleanup_stuff) == 1 assert isinstance(cleanup_stuff[0], ValueError) assert str(cleanup_stuff[0]) == "dummy" def test_app_ctx_globals_methods(app, app_ctx): # get assert flask.g.get("foo") is None assert flask.g.get("foo", "bar") == "bar" # __contains__ assert "foo" not in flask.g flask.g.foo = "bar" assert "foo" in flask.g # setdefault flask.g.setdefault("bar", "the cake is a lie") flask.g.setdefault("bar", "hello world") assert flask.g.bar == "the cake is a lie" # pop assert flask.g.pop("bar") == "the cake is a lie" with pytest.raises(KeyError): flask.g.pop("bar") assert flask.g.pop("bar", "more cake") == "more cake" # __iter__ assert list(flask.g) == ["foo"] # __repr__ assert repr(flask.g) == "" def test_custom_app_ctx_globals_class(app): class CustomRequestGlobals: def __init__(self): self.spam = "eggs" app.app_ctx_globals_class = CustomRequestGlobals with app.app_context(): assert flask.render_template_string("{{ g.spam }}") == "eggs" def test_context_refcounts(app, client): called = [] @app.teardown_request def teardown_req(error=None): called.append("request") @app.teardown_appcontext def teardown_app(error=None): called.append("app") @app.route("/") def index(): with app_ctx: with request_ctx: pass assert flask.request.environ["werkzeug.request"] is not None return "" res = client.get("/") assert res.status_code == 200 assert res.data == b"" assert called == ["request", "app"] def test_clean_pop(app): app.testing = False called = [] @app.teardown_request def teardown_req(error=None): raise ZeroDivisionError @app.teardown_appcontext def teardown_app(error=None): called.append("TEARDOWN") with app.app_context(): called.append(flask.current_app.name) assert called == ["flask_test", "TEARDOWN"] assert not flask.current_app flask-3.1.1/tests/test_apps/000077500000000000000000000000001501065614000157565ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/.env000066400000000000000000000000411501065614000165420ustar00rootroot00000000000000FOO=env SPAM=1 EGGS=2 HAM=ç«è…¿ flask-3.1.1/tests/test_apps/.flaskenv000066400000000000000000000000341501065614000175650ustar00rootroot00000000000000FOO=flaskenv BAR=bar EGGS=0 flask-3.1.1/tests/test_apps/blueprintapp/000077500000000000000000000000001501065614000204635ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/__init__.py000066400000000000000000000004001501065614000225660ustar00rootroot00000000000000from flask import Flask app = Flask(__name__) app.config["DEBUG"] = True from blueprintapp.apps.admin import admin # noqa: E402 from blueprintapp.apps.frontend import frontend # noqa: E402 app.register_blueprint(admin) app.register_blueprint(frontend) flask-3.1.1/tests/test_apps/blueprintapp/apps/000077500000000000000000000000001501065614000214265ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/apps/__init__.py000066400000000000000000000000001501065614000235250ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/apps/admin/000077500000000000000000000000001501065614000225165ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/apps/admin/__init__.py000066400000000000000000000005561501065614000246350ustar00rootroot00000000000000from flask import Blueprint from flask import render_template admin = Blueprint( "admin", __name__, url_prefix="/admin", template_folder="templates", static_folder="static", ) @admin.route("/") def index(): return render_template("admin/index.html") @admin.route("/index2") def index2(): return render_template("./admin/index.html") flask-3.1.1/tests/test_apps/blueprintapp/apps/admin/static/000077500000000000000000000000001501065614000240055ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/apps/admin/static/css/000077500000000000000000000000001501065614000245755ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/apps/admin/static/css/test.css000066400000000000000000000000221501065614000262600ustar00rootroot00000000000000/* nested file */ flask-3.1.1/tests/test_apps/blueprintapp/apps/admin/static/test.txt000066400000000000000000000000131501065614000255170ustar00rootroot00000000000000Admin File flask-3.1.1/tests/test_apps/blueprintapp/apps/admin/templates/000077500000000000000000000000001501065614000245145ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/apps/admin/templates/admin/000077500000000000000000000000001501065614000256045ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html000066400000000000000000000000251501065614000275760ustar00rootroot00000000000000Hello from the Admin flask-3.1.1/tests/test_apps/blueprintapp/apps/frontend/000077500000000000000000000000001501065614000232455ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/apps/frontend/__init__.py000066400000000000000000000005071501065614000253600ustar00rootroot00000000000000from flask import Blueprint from flask import render_template frontend = Blueprint("frontend", __name__, template_folder="templates") @frontend.route("/") def index(): return render_template("frontend/index.html") @frontend.route("/missing") def missing_template(): return render_template("missing_template.html") flask-3.1.1/tests/test_apps/blueprintapp/apps/frontend/templates/000077500000000000000000000000001501065614000252435ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/000077500000000000000000000000001501065614000270625ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html000066400000000000000000000000301501065614000310500ustar00rootroot00000000000000Hello from the Frontend flask-3.1.1/tests/test_apps/cliapp/000077500000000000000000000000001501065614000172265ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/cliapp/__init__.py000066400000000000000000000000001501065614000213250ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/cliapp/app.py000066400000000000000000000000641501065614000203600ustar00rootroot00000000000000from flask import Flask testapp = Flask("testapp") flask-3.1.1/tests/test_apps/cliapp/factory.py000066400000000000000000000002511501065614000212450ustar00rootroot00000000000000from flask import Flask def create_app(): return Flask("app") def create_app2(foo, bar): return Flask("_".join(["app2", foo, bar])) def no_app(): pass flask-3.1.1/tests/test_apps/cliapp/importerrorapp.py000066400000000000000000000001111501065614000226560ustar00rootroot00000000000000from flask import Flask raise ImportError() testapp = Flask("testapp") flask-3.1.1/tests/test_apps/cliapp/inner1/000077500000000000000000000000001501065614000204225ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/cliapp/inner1/__init__.py000066400000000000000000000000671501065614000225360ustar00rootroot00000000000000from flask import Flask application = Flask(__name__) flask-3.1.1/tests/test_apps/cliapp/inner1/inner2/000077500000000000000000000000001501065614000216175ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/cliapp/inner1/inner2/__init__.py000066400000000000000000000000001501065614000237160ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/cliapp/inner1/inner2/flask.py000066400000000000000000000000571501065614000232730ustar00rootroot00000000000000from flask import Flask app = Flask(__name__) flask-3.1.1/tests/test_apps/cliapp/message.txt000066400000000000000000000000461501065614000214130ustar00rootroot00000000000000So long, and thanks for all the fish. flask-3.1.1/tests/test_apps/cliapp/multiapp.py000066400000000000000000000001031501065614000214250ustar00rootroot00000000000000from flask import Flask app1 = Flask("app1") app2 = Flask("app2") flask-3.1.1/tests/test_apps/helloworld/000077500000000000000000000000001501065614000201315ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/helloworld/hello.py000066400000000000000000000001501501065614000216020ustar00rootroot00000000000000from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello World!" flask-3.1.1/tests/test_apps/helloworld/wsgi.py000066400000000000000000000000441501065614000214520ustar00rootroot00000000000000from hello import app # noqa: F401 flask-3.1.1/tests/test_apps/subdomaintestmodule/000077500000000000000000000000001501065614000220455ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/subdomaintestmodule/__init__.py000066400000000000000000000001111501065614000241470ustar00rootroot00000000000000from flask import Module mod = Module(__name__, "foo", subdomain="foo") flask-3.1.1/tests/test_apps/subdomaintestmodule/static/000077500000000000000000000000001501065614000233345ustar00rootroot00000000000000flask-3.1.1/tests/test_apps/subdomaintestmodule/static/hello.txt000066400000000000000000000000201501065614000251700ustar00rootroot00000000000000Hello Subdomain flask-3.1.1/tests/test_async.py000066400000000000000000000064071501065614000165110ustar00rootroot00000000000000import asyncio import pytest from flask import Blueprint from flask import Flask from flask import request from flask.views import MethodView from flask.views import View pytest.importorskip("asgiref") class AppError(Exception): pass class BlueprintError(Exception): pass class AsyncView(View): methods = ["GET", "POST"] async def dispatch_request(self): await asyncio.sleep(0) return request.method class AsyncMethodView(MethodView): async def get(self): await asyncio.sleep(0) return "GET" async def post(self): await asyncio.sleep(0) return "POST" @pytest.fixture(name="async_app") def _async_app(): app = Flask(__name__) @app.route("/", methods=["GET", "POST"]) @app.route("/home", methods=["GET", "POST"]) async def index(): await asyncio.sleep(0) return request.method @app.errorhandler(AppError) async def handle(_): return "", 412 @app.route("/error") async def error(): raise AppError() blueprint = Blueprint("bp", __name__) @blueprint.route("/", methods=["GET", "POST"]) async def bp_index(): await asyncio.sleep(0) return request.method @blueprint.errorhandler(BlueprintError) async def bp_handle(_): return "", 412 @blueprint.route("/error") async def bp_error(): raise BlueprintError() app.register_blueprint(blueprint, url_prefix="/bp") app.add_url_rule("/view", view_func=AsyncView.as_view("view")) app.add_url_rule("/methodview", view_func=AsyncMethodView.as_view("methodview")) return app @pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"]) def test_async_route(path, async_app): test_client = async_app.test_client() response = test_client.get(path) assert b"GET" in response.get_data() response = test_client.post(path) assert b"POST" in response.get_data() @pytest.mark.parametrize("path", ["/error", "/bp/error"]) def test_async_error_handler(path, async_app): test_client = async_app.test_client() response = test_client.get(path) assert response.status_code == 412 def test_async_before_after_request(): app_before_called = False app_after_called = False bp_before_called = False bp_after_called = False app = Flask(__name__) @app.route("/") def index(): return "" @app.before_request async def before(): nonlocal app_before_called app_before_called = True @app.after_request async def after(response): nonlocal app_after_called app_after_called = True return response blueprint = Blueprint("bp", __name__) @blueprint.route("/") def bp_index(): return "" @blueprint.before_request async def bp_before(): nonlocal bp_before_called bp_before_called = True @blueprint.after_request async def bp_after(response): nonlocal bp_after_called bp_after_called = True return response app.register_blueprint(blueprint, url_prefix="/bp") test_client = app.test_client() test_client.get("/") assert app_before_called assert app_after_called test_client.get("/bp/") assert bp_before_called assert bp_after_called flask-3.1.1/tests/test_basic.py000066400000000000000000001505431501065614000164560ustar00rootroot00000000000000import gc import re import typing as t import uuid import warnings import weakref from contextlib import nullcontext from datetime import datetime from datetime import timezone from platform import python_implementation import pytest import werkzeug.serving from markupsafe import Markup from werkzeug.exceptions import BadRequest from werkzeug.exceptions import Forbidden from werkzeug.exceptions import NotFound from werkzeug.http import parse_date from werkzeug.routing import BuildError from werkzeug.routing import RequestRedirect import flask require_cpython_gc = pytest.mark.skipif( python_implementation() != "CPython", reason="Requires CPython GC behavior", ) def test_options_work(app, client): @app.route("/", methods=["GET", "POST"]) def index(): return "Hello World" rv = client.open("/", method="OPTIONS") assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] assert rv.data == b"" def test_options_on_multiple_rules(app, client): @app.route("/", methods=["GET", "POST"]) def index(): return "Hello World" @app.route("/", methods=["PUT"]) def index_put(): return "Aha!" rv = client.open("/", method="OPTIONS") assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST", "PUT"] @pytest.mark.parametrize("method", ["get", "post", "put", "delete", "patch"]) def test_method_route(app, client, method): method_route = getattr(app, method) client_method = getattr(client, method) @method_route("/") def hello(): return "Hello" assert client_method("/").data == b"Hello" def test_method_route_no_methods(app): with pytest.raises(TypeError): app.get("/", methods=["GET", "POST"]) def test_provide_automatic_options_attr(): app = flask.Flask(__name__) def index(): return "Hello World!" index.provide_automatic_options = False app.route("/")(index) rv = app.test_client().open("/", method="OPTIONS") assert rv.status_code == 405 app = flask.Flask(__name__) def index2(): return "Hello World!" index2.provide_automatic_options = True app.route("/", methods=["OPTIONS"])(index2) rv = app.test_client().open("/", method="OPTIONS") assert sorted(rv.allow) == ["OPTIONS"] def test_provide_automatic_options_kwarg(app, client): def index(): return flask.request.method def more(): return flask.request.method app.add_url_rule("/", view_func=index, provide_automatic_options=False) app.add_url_rule( "/more", view_func=more, methods=["GET", "POST"], provide_automatic_options=False, ) assert client.get("/").data == b"GET" rv = client.post("/") assert rv.status_code == 405 assert sorted(rv.allow) == ["GET", "HEAD"] rv = client.open("/", method="OPTIONS") assert rv.status_code == 405 rv = client.head("/") assert rv.status_code == 200 assert not rv.data # head truncates assert client.post("/more").data == b"POST" assert client.get("/more").data == b"GET" rv = client.delete("/more") assert rv.status_code == 405 assert sorted(rv.allow) == ["GET", "HEAD", "POST"] rv = client.open("/more", method="OPTIONS") assert rv.status_code == 405 def test_request_dispatching(app, client): @app.route("/") def index(): return flask.request.method @app.route("/more", methods=["GET", "POST"]) def more(): return flask.request.method assert client.get("/").data == b"GET" rv = client.post("/") assert rv.status_code == 405 assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS"] rv = client.head("/") assert rv.status_code == 200 assert not rv.data # head truncates assert client.post("/more").data == b"POST" assert client.get("/more").data == b"GET" rv = client.delete("/more") assert rv.status_code == 405 assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] def test_disallow_string_for_allowed_methods(app): with pytest.raises(TypeError): app.add_url_rule("/", methods="GET POST", endpoint="test") def test_url_mapping(app, client): random_uuid4 = "7eb41166-9ebf-4d26-b771-ea3f54f8b383" def index(): return flask.request.method def more(): return flask.request.method def options(): return random_uuid4 app.add_url_rule("/", "index", index) app.add_url_rule("/more", "more", more, methods=["GET", "POST"]) # Issue 1288: Test that automatic options are not added # when non-uppercase 'options' in methods app.add_url_rule("/options", "options", options, methods=["options"]) assert client.get("/").data == b"GET" rv = client.post("/") assert rv.status_code == 405 assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS"] rv = client.head("/") assert rv.status_code == 200 assert not rv.data # head truncates assert client.post("/more").data == b"POST" assert client.get("/more").data == b"GET" rv = client.delete("/more") assert rv.status_code == 405 assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] rv = client.open("/options", method="OPTIONS") assert rv.status_code == 200 assert random_uuid4 in rv.data.decode("utf-8") def test_werkzeug_routing(app, client): from werkzeug.routing import Rule from werkzeug.routing import Submount app.url_map.add( Submount("/foo", [Rule("/bar", endpoint="bar"), Rule("/", endpoint="index")]) ) def bar(): return "bar" def index(): return "index" app.view_functions["bar"] = bar app.view_functions["index"] = index assert client.get("/foo/").data == b"index" assert client.get("/foo/bar").data == b"bar" def test_endpoint_decorator(app, client): from werkzeug.routing import Rule from werkzeug.routing import Submount app.url_map.add( Submount("/foo", [Rule("/bar", endpoint="bar"), Rule("/", endpoint="index")]) ) @app.endpoint("bar") def bar(): return "bar" @app.endpoint("index") def index(): return "index" assert client.get("/foo/").data == b"index" assert client.get("/foo/bar").data == b"bar" def test_session(app, client): @app.route("/set", methods=["POST"]) def set(): assert not flask.session.accessed assert not flask.session.modified flask.session["value"] = flask.request.form["value"] assert flask.session.accessed assert flask.session.modified return "value set" @app.route("/get") def get(): assert not flask.session.accessed assert not flask.session.modified v = flask.session.get("value", "None") assert flask.session.accessed assert not flask.session.modified return v assert client.post("/set", data={"value": "42"}).data == b"value set" assert client.get("/get").data == b"42" def test_session_path(app, client): app.config.update(APPLICATION_ROOT="/foo") @app.route("/") def index(): flask.session["testing"] = 42 return "Hello World" rv = client.get("/", "http://example.com:8080/foo") assert "path=/foo" in rv.headers["set-cookie"].lower() def test_session_using_application_root(app, client): class PrefixPathMiddleware: def __init__(self, app, prefix): self.app = app self.prefix = prefix def __call__(self, environ, start_response): environ["SCRIPT_NAME"] = self.prefix return self.app(environ, start_response) app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, "/bar") app.config.update(APPLICATION_ROOT="/bar") @app.route("/") def index(): flask.session["testing"] = 42 return "Hello World" rv = client.get("/", "http://example.com:8080/") assert "path=/bar" in rv.headers["set-cookie"].lower() def test_session_using_session_settings(app, client): app.config.update( SERVER_NAME="www.example.com:8080", APPLICATION_ROOT="/test", SESSION_COOKIE_DOMAIN=".example.com", SESSION_COOKIE_HTTPONLY=False, SESSION_COOKIE_SECURE=True, SESSION_COOKIE_PARTITIONED=True, SESSION_COOKIE_SAMESITE="Lax", SESSION_COOKIE_PATH="/", ) @app.route("/") def index(): flask.session["testing"] = 42 return "Hello World" @app.route("/clear") def clear(): flask.session.pop("testing", None) return "Goodbye World" rv = client.get("/", "http://www.example.com:8080/test/") cookie = rv.headers["set-cookie"].lower() # or condition for Werkzeug < 2.3 assert "domain=example.com" in cookie or "domain=.example.com" in cookie assert "path=/" in cookie assert "secure" in cookie assert "httponly" not in cookie assert "samesite" in cookie assert "partitioned" in cookie rv = client.get("/clear", "http://www.example.com:8080/test/") cookie = rv.headers["set-cookie"].lower() assert "session=;" in cookie # or condition for Werkzeug < 2.3 assert "domain=example.com" in cookie or "domain=.example.com" in cookie assert "path=/" in cookie assert "secure" in cookie assert "samesite" in cookie assert "partitioned" in cookie def test_session_using_samesite_attribute(app, client): @app.route("/") def index(): flask.session["testing"] = 42 return "Hello World" app.config.update(SESSION_COOKIE_SAMESITE="invalid") with pytest.raises(ValueError): client.get("/") app.config.update(SESSION_COOKIE_SAMESITE=None) rv = client.get("/") cookie = rv.headers["set-cookie"].lower() assert "samesite" not in cookie app.config.update(SESSION_COOKIE_SAMESITE="Strict") rv = client.get("/") cookie = rv.headers["set-cookie"].lower() assert "samesite=strict" in cookie app.config.update(SESSION_COOKIE_SAMESITE="Lax") rv = client.get("/") cookie = rv.headers["set-cookie"].lower() assert "samesite=lax" in cookie def test_missing_session(app): app.secret_key = None def expect_exception(f, *args, **kwargs): e = pytest.raises(RuntimeError, f, *args, **kwargs) assert e.value.args and "session is unavailable" in e.value.args[0] with app.test_request_context(): assert flask.session.get("missing_key") is None expect_exception(flask.session.__setitem__, "foo", 42) expect_exception(flask.session.pop, "foo") def test_session_secret_key_fallbacks(app, client) -> None: @app.post("/") def set_session() -> str: flask.session["a"] = 1 return "" @app.get("/") def get_session() -> dict[str, t.Any]: return dict(flask.session) # Set session with initial secret key, and two valid expiring keys app.secret_key, app.config["SECRET_KEY_FALLBACKS"] = ( "0 key", ["-1 key", "-2 key"], ) client.post() assert client.get().json == {"a": 1} # Change secret key, session can't be loaded and appears empty app.secret_key = "? key" assert client.get().json == {} # Rotate the valid keys, session can be loaded app.secret_key, app.config["SECRET_KEY_FALLBACKS"] = ( "+1 key", ["0 key", "-1 key"], ) assert client.get().json == {"a": 1} def test_session_expiration(app, client): permanent = True @app.route("/") def index(): flask.session["test"] = 42 flask.session.permanent = permanent return "" @app.route("/test") def test(): return str(flask.session.permanent) rv = client.get("/") assert "set-cookie" in rv.headers match = re.search(r"(?i)\bexpires=([^;]+)", rv.headers["set-cookie"]) expires = parse_date(match.group()) expected = datetime.now(timezone.utc) + app.permanent_session_lifetime assert expires.year == expected.year assert expires.month == expected.month assert expires.day == expected.day rv = client.get("/test") assert rv.data == b"True" permanent = False rv = client.get("/") assert "set-cookie" in rv.headers match = re.search(r"\bexpires=([^;]+)", rv.headers["set-cookie"]) assert match is None def test_session_stored_last(app, client): @app.after_request def modify_session(response): flask.session["foo"] = 42 return response @app.route("/") def dump_session_contents(): return repr(flask.session.get("foo")) assert client.get("/").data == b"None" assert client.get("/").data == b"42" def test_session_special_types(app, client): now = datetime.now(timezone.utc).replace(microsecond=0) the_uuid = uuid.uuid4() @app.route("/") def dump_session_contents(): flask.session["t"] = (1, 2, 3) flask.session["b"] = b"\xff" flask.session["m"] = Markup("") flask.session["u"] = the_uuid flask.session["d"] = now flask.session["t_tag"] = {" t": "not-a-tuple"} flask.session["di_t_tag"] = {" t__": "not-a-tuple"} flask.session["di_tag"] = {" di": "not-a-dict"} return "", 204 with client: client.get("/") s = flask.session assert s["t"] == (1, 2, 3) assert type(s["b"]) is bytes # noqa: E721 assert s["b"] == b"\xff" assert type(s["m"]) is Markup # noqa: E721 assert s["m"] == Markup("") assert s["u"] == the_uuid assert s["d"] == now assert s["t_tag"] == {" t": "not-a-tuple"} assert s["di_t_tag"] == {" t__": "not-a-tuple"} assert s["di_tag"] == {" di": "not-a-dict"} def test_session_cookie_setting(app): is_permanent = True @app.route("/bump") def bump(): rv = flask.session["foo"] = flask.session.get("foo", 0) + 1 flask.session.permanent = is_permanent return str(rv) @app.route("/read") def read(): return str(flask.session.get("foo", 0)) def run_test(expect_header): with app.test_client() as c: assert c.get("/bump").data == b"1" assert c.get("/bump").data == b"2" assert c.get("/bump").data == b"3" rv = c.get("/read") set_cookie = rv.headers.get("set-cookie") assert (set_cookie is not None) == expect_header assert rv.data == b"3" is_permanent = True app.config["SESSION_REFRESH_EACH_REQUEST"] = True run_test(expect_header=True) is_permanent = True app.config["SESSION_REFRESH_EACH_REQUEST"] = False run_test(expect_header=False) is_permanent = False app.config["SESSION_REFRESH_EACH_REQUEST"] = True run_test(expect_header=False) is_permanent = False app.config["SESSION_REFRESH_EACH_REQUEST"] = False run_test(expect_header=False) def test_session_vary_cookie(app, client): @app.route("/set") def set_session(): flask.session["test"] = "test" return "" @app.route("/get") def get(): return flask.session.get("test") @app.route("/getitem") def getitem(): return flask.session["test"] @app.route("/setdefault") def setdefault(): return flask.session.setdefault("test", "default") @app.route("/clear") def clear(): flask.session.clear() return "" @app.route("/vary-cookie-header-set") def vary_cookie_header_set(): response = flask.Response() response.vary.add("Cookie") flask.session["test"] = "test" return response @app.route("/vary-header-set") def vary_header_set(): response = flask.Response() response.vary.update(("Accept-Encoding", "Accept-Language")) flask.session["test"] = "test" return response @app.route("/no-vary-header") def no_vary_header(): return "" def expect(path, header_value="Cookie"): rv = client.get(path) if header_value: # The 'Vary' key should exist in the headers only once. assert len(rv.headers.get_all("Vary")) == 1 assert rv.headers["Vary"] == header_value else: assert "Vary" not in rv.headers expect("/set") expect("/get") expect("/getitem") expect("/setdefault") expect("/clear") expect("/vary-cookie-header-set") expect("/vary-header-set", "Accept-Encoding, Accept-Language, Cookie") expect("/no-vary-header", None) def test_session_refresh_vary(app, client): @app.get("/login") def login(): flask.session["user_id"] = 1 flask.session.permanent = True return "" @app.get("/ignored") def ignored(): return "" rv = client.get("/login") assert rv.headers["Vary"] == "Cookie" rv = client.get("/ignored") assert rv.headers["Vary"] == "Cookie" def test_flashes(app, req_ctx): assert not flask.session.modified flask.flash("Zap") flask.session.modified = False flask.flash("Zip") assert flask.session.modified assert list(flask.get_flashed_messages()) == ["Zap", "Zip"] def test_extended_flashing(app): # Be sure app.testing=True below, else tests can fail silently. # # Specifically, if app.testing is not set to True, the AssertionErrors # in the view functions will cause a 500 response to the test client # instead of propagating exceptions. @app.route("/") def index(): flask.flash("Hello World") flask.flash("Hello World", "error") flask.flash(Markup("Testing"), "warning") return "" @app.route("/test/") def test(): messages = flask.get_flashed_messages() assert list(messages) == [ "Hello World", "Hello World", Markup("Testing"), ] return "" @app.route("/test_with_categories/") def test_with_categories(): messages = flask.get_flashed_messages(with_categories=True) assert len(messages) == 3 assert list(messages) == [ ("message", "Hello World"), ("error", "Hello World"), ("warning", Markup("Testing")), ] return "" @app.route("/test_filter/") def test_filter(): messages = flask.get_flashed_messages( category_filter=["message"], with_categories=True ) assert list(messages) == [("message", "Hello World")] return "" @app.route("/test_filters/") def test_filters(): messages = flask.get_flashed_messages( category_filter=["message", "warning"], with_categories=True ) assert list(messages) == [ ("message", "Hello World"), ("warning", Markup("Testing")), ] return "" @app.route("/test_filters_without_returning_categories/") def test_filters2(): messages = flask.get_flashed_messages(category_filter=["message", "warning"]) assert len(messages) == 2 assert messages[0] == "Hello World" assert messages[1] == Markup("Testing") return "" # Create new test client on each test to clean flashed messages. client = app.test_client() client.get("/") client.get("/test_with_categories/") client = app.test_client() client.get("/") client.get("/test_filter/") client = app.test_client() client.get("/") client.get("/test_filters/") client = app.test_client() client.get("/") client.get("/test_filters_without_returning_categories/") def test_request_processing(app, client): evts = [] @app.before_request def before_request(): evts.append("before") @app.after_request def after_request(response): response.data += b"|after" evts.append("after") return response @app.route("/") def index(): assert "before" in evts assert "after" not in evts return "request" assert "after" not in evts rv = client.get("/").data assert "after" in evts assert rv == b"request|after" def test_request_preprocessing_early_return(app, client): evts = [] @app.before_request def before_request1(): evts.append(1) @app.before_request def before_request2(): evts.append(2) return "hello" @app.before_request def before_request3(): evts.append(3) return "bye" @app.route("/") def index(): evts.append("index") return "damnit" rv = client.get("/").data.strip() assert rv == b"hello" assert evts == [1, 2] def test_after_request_processing(app, client): @app.route("/") def index(): @flask.after_this_request def foo(response): response.headers["X-Foo"] = "a header" return response return "Test" resp = client.get("/") assert resp.status_code == 200 assert resp.headers["X-Foo"] == "a header" def test_teardown_request_handler(app, client): called = [] @app.teardown_request def teardown_request(exc): called.append(True) return "Ignored" @app.route("/") def root(): return "Response" rv = client.get("/") assert rv.status_code == 200 assert b"Response" in rv.data assert len(called) == 1 def test_teardown_request_handler_debug_mode(app, client): called = [] @app.teardown_request def teardown_request(exc): called.append(True) return "Ignored" @app.route("/") def root(): return "Response" rv = client.get("/") assert rv.status_code == 200 assert b"Response" in rv.data assert len(called) == 1 def test_teardown_request_handler_error(app, client): called = [] app.testing = False @app.teardown_request def teardown_request1(exc): assert type(exc) is ZeroDivisionError called.append(True) # This raises a new error and blows away sys.exc_info(), so we can # test that all teardown_requests get passed the same original # exception. try: raise TypeError() except Exception: pass @app.teardown_request def teardown_request2(exc): assert type(exc) is ZeroDivisionError called.append(True) # This raises a new error and blows away sys.exc_info(), so we can # test that all teardown_requests get passed the same original # exception. try: raise TypeError() except Exception: pass @app.route("/") def fails(): raise ZeroDivisionError rv = client.get("/") assert rv.status_code == 500 assert b"Internal Server Error" in rv.data assert len(called) == 2 def test_before_after_request_order(app, client): called = [] @app.before_request def before1(): called.append(1) @app.before_request def before2(): called.append(2) @app.after_request def after1(response): called.append(4) return response @app.after_request def after2(response): called.append(3) return response @app.teardown_request def finish1(exc): called.append(6) @app.teardown_request def finish2(exc): called.append(5) @app.route("/") def index(): return "42" rv = client.get("/") assert rv.data == b"42" assert called == [1, 2, 3, 4, 5, 6] def test_error_handling(app, client): app.testing = False @app.errorhandler(404) def not_found(e): return "not found", 404 @app.errorhandler(500) def internal_server_error(e): return "internal server error", 500 @app.errorhandler(Forbidden) def forbidden(e): return "forbidden", 403 @app.route("/") def index(): flask.abort(404) @app.route("/error") def error(): raise ZeroDivisionError @app.route("/forbidden") def error2(): flask.abort(403) rv = client.get("/") assert rv.status_code == 404 assert rv.data == b"not found" rv = client.get("/error") assert rv.status_code == 500 assert b"internal server error" == rv.data rv = client.get("/forbidden") assert rv.status_code == 403 assert b"forbidden" == rv.data def test_error_handling_processing(app, client): app.testing = False @app.errorhandler(500) def internal_server_error(e): return "internal server error", 500 @app.route("/") def broken_func(): raise ZeroDivisionError @app.after_request def after_request(resp): resp.mimetype = "text/x-special" return resp resp = client.get("/") assert resp.mimetype == "text/x-special" assert resp.data == b"internal server error" def test_baseexception_error_handling(app, client): app.testing = False @app.route("/") def broken_func(): raise KeyboardInterrupt() with pytest.raises(KeyboardInterrupt): client.get("/") def test_before_request_and_routing_errors(app, client): @app.before_request def attach_something(): flask.g.something = "value" @app.errorhandler(404) def return_something(error): return flask.g.something, 404 rv = client.get("/") assert rv.status_code == 404 assert rv.data == b"value" def test_user_error_handling(app, client): class MyException(Exception): pass @app.errorhandler(MyException) def handle_my_exception(e): assert isinstance(e, MyException) return "42" @app.route("/") def index(): raise MyException() assert client.get("/").data == b"42" def test_http_error_subclass_handling(app, client): class ForbiddenSubclass(Forbidden): pass @app.errorhandler(ForbiddenSubclass) def handle_forbidden_subclass(e): assert isinstance(e, ForbiddenSubclass) return "banana" @app.errorhandler(403) def handle_403(e): assert not isinstance(e, ForbiddenSubclass) assert isinstance(e, Forbidden) return "apple" @app.route("/1") def index1(): raise ForbiddenSubclass() @app.route("/2") def index2(): flask.abort(403) @app.route("/3") def index3(): raise Forbidden() assert client.get("/1").data == b"banana" assert client.get("/2").data == b"apple" assert client.get("/3").data == b"apple" def test_errorhandler_precedence(app, client): class E1(Exception): pass class E2(Exception): pass class E3(E1, E2): pass @app.errorhandler(E2) def handle_e2(e): return "E2" @app.errorhandler(Exception) def handle_exception(e): return "Exception" @app.route("/E1") def raise_e1(): raise E1 @app.route("/E3") def raise_e3(): raise E3 rv = client.get("/E1") assert rv.data == b"Exception" rv = client.get("/E3") assert rv.data == b"E2" @pytest.mark.parametrize( ("debug", "trap", "expect_key", "expect_abort"), [(False, None, True, True), (True, None, False, True), (False, True, False, False)], ) def test_trap_bad_request_key_error(app, client, debug, trap, expect_key, expect_abort): app.config["DEBUG"] = debug app.config["TRAP_BAD_REQUEST_ERRORS"] = trap @app.route("/key") def fail(): flask.request.form["missing_key"] @app.route("/abort") def allow_abort(): flask.abort(400) if expect_key: rv = client.get("/key") assert rv.status_code == 400 assert b"missing_key" not in rv.data else: with pytest.raises(KeyError) as exc_info: client.get("/key") assert exc_info.errisinstance(BadRequest) assert "missing_key" in exc_info.value.get_description() if expect_abort: rv = client.get("/abort") assert rv.status_code == 400 else: with pytest.raises(BadRequest): client.get("/abort") def test_trapping_of_all_http_exceptions(app, client): app.config["TRAP_HTTP_EXCEPTIONS"] = True @app.route("/fail") def fail(): flask.abort(404) with pytest.raises(NotFound): client.get("/fail") def test_error_handler_after_processor_error(app, client): app.testing = False @app.before_request def before_request(): if _trigger == "before": raise ZeroDivisionError @app.after_request def after_request(response): if _trigger == "after": raise ZeroDivisionError return response @app.route("/") def index(): return "Foo" @app.errorhandler(500) def internal_server_error(e): return "Hello Server Error", 500 for _trigger in "before", "after": rv = client.get("/") assert rv.status_code == 500 assert rv.data == b"Hello Server Error" def test_enctype_debug_helper(app, client): from flask.debughelpers import DebugFilesKeyError app.debug = True @app.route("/fail", methods=["POST"]) def index(): return flask.request.files["foo"].filename with pytest.raises(DebugFilesKeyError) as e: client.post("/fail", data={"foo": "index.txt"}) assert "no file contents were transmitted" in str(e.value) assert "This was submitted: 'index.txt'" in str(e.value) def test_response_types(app, client): @app.route("/text") def from_text(): return "Hällo Wörld" @app.route("/bytes") def from_bytes(): return "Hällo Wörld".encode() @app.route("/full_tuple") def from_full_tuple(): return ( "Meh", 400, {"X-Foo": "Testing", "Content-Type": "text/plain; charset=utf-8"}, ) @app.route("/text_headers") def from_text_headers(): return "Hello", {"X-Foo": "Test", "Content-Type": "text/plain; charset=utf-8"} @app.route("/text_status") def from_text_status(): return "Hi, status!", 400 @app.route("/response_headers") def from_response_headers(): return ( flask.Response( "Hello world", 404, {"Content-Type": "text/html", "X-Foo": "Baz"} ), {"Content-Type": "text/plain", "X-Foo": "Bar", "X-Bar": "Foo"}, ) @app.route("/response_status") def from_response_status(): return app.response_class("Hello world", 400), 500 @app.route("/wsgi") def from_wsgi(): return NotFound() @app.route("/dict") def from_dict(): return {"foo": "bar"}, 201 @app.route("/list") def from_list(): return ["foo", "bar"], 201 assert client.get("/text").data == "Hällo Wörld".encode() assert client.get("/bytes").data == "Hällo Wörld".encode() rv = client.get("/full_tuple") assert rv.data == b"Meh" assert rv.headers["X-Foo"] == "Testing" assert rv.status_code == 400 assert rv.mimetype == "text/plain" rv = client.get("/text_headers") assert rv.data == b"Hello" assert rv.headers["X-Foo"] == "Test" assert rv.status_code == 200 assert rv.mimetype == "text/plain" rv = client.get("/text_status") assert rv.data == b"Hi, status!" assert rv.status_code == 400 assert rv.mimetype == "text/html" rv = client.get("/response_headers") assert rv.data == b"Hello world" assert rv.content_type == "text/plain" assert rv.headers.getlist("X-Foo") == ["Bar"] assert rv.headers["X-Bar"] == "Foo" assert rv.status_code == 404 rv = client.get("/response_status") assert rv.data == b"Hello world" assert rv.status_code == 500 rv = client.get("/wsgi") assert b"Not Found" in rv.data assert rv.status_code == 404 rv = client.get("/dict") assert rv.json == {"foo": "bar"} assert rv.status_code == 201 rv = client.get("/list") assert rv.json == ["foo", "bar"] assert rv.status_code == 201 def test_response_type_errors(): app = flask.Flask(__name__) app.testing = True @app.route("/none") def from_none(): pass @app.route("/small_tuple") def from_small_tuple(): return ("Hello",) @app.route("/large_tuple") def from_large_tuple(): return "Hello", 234, {"X-Foo": "Bar"}, "???" @app.route("/bad_type") def from_bad_type(): return True @app.route("/bad_wsgi") def from_bad_wsgi(): return lambda: None c = app.test_client() with pytest.raises(TypeError) as e: c.get("/none") assert "returned None" in str(e.value) assert "from_none" in str(e.value) with pytest.raises(TypeError) as e: c.get("/small_tuple") assert "tuple must have the form" in str(e.value) with pytest.raises(TypeError): c.get("/large_tuple") with pytest.raises(TypeError) as e: c.get("/bad_type") assert "it was a bool" in str(e.value) with pytest.raises(TypeError): c.get("/bad_wsgi") def test_make_response(app, req_ctx): rv = flask.make_response() assert rv.status_code == 200 assert rv.data == b"" assert rv.mimetype == "text/html" rv = flask.make_response("Awesome") assert rv.status_code == 200 assert rv.data == b"Awesome" assert rv.mimetype == "text/html" rv = flask.make_response("W00t", 404) assert rv.status_code == 404 assert rv.data == b"W00t" assert rv.mimetype == "text/html" rv = flask.make_response(c for c in "Hello") assert rv.status_code == 200 assert rv.data == b"Hello" assert rv.mimetype == "text/html" def test_make_response_with_response_instance(app, req_ctx): rv = flask.make_response(flask.jsonify({"msg": "W00t"}), 400) assert rv.status_code == 400 assert rv.data == b'{"msg":"W00t"}\n' assert rv.mimetype == "application/json" rv = flask.make_response(flask.Response(""), 400) assert rv.status_code == 400 assert rv.data == b"" assert rv.mimetype == "text/html" rv = flask.make_response( flask.Response("", headers={"Content-Type": "text/html"}), 400, [("X-Foo", "bar")], ) assert rv.status_code == 400 assert rv.headers["Content-Type"] == "text/html" assert rv.headers["X-Foo"] == "bar" @pytest.mark.parametrize("compact", [True, False]) def test_jsonify_no_prettyprint(app, compact): app.json.compact = compact rv = app.json.response({"msg": {"submsg": "W00t"}, "msg2": "foobar"}) data = rv.data.strip() assert (b" " not in data) is compact assert (b"\n" not in data) is compact def test_jsonify_mimetype(app, req_ctx): app.json.mimetype = "application/vnd.api+json" msg = {"msg": {"submsg": "W00t"}} rv = flask.make_response(flask.jsonify(msg), 200) assert rv.mimetype == "application/vnd.api+json" def test_json_dump_dataclass(app, req_ctx): from dataclasses import make_dataclass Data = make_dataclass("Data", [("name", str)]) value = app.json.dumps(Data("Flask")) value = app.json.loads(value) assert value == {"name": "Flask"} def test_jsonify_args_and_kwargs_check(app, req_ctx): with pytest.raises(TypeError) as e: flask.jsonify("fake args", kwargs="fake") assert "args or kwargs" in str(e.value) def test_url_generation(app, req_ctx): @app.route("/hello/", methods=["POST"]) def hello(): pass assert flask.url_for("hello", name="test x") == "/hello/test%20x" assert ( flask.url_for("hello", name="test x", _external=True) == "http://localhost/hello/test%20x" ) def test_build_error_handler(app): # Test base case, a URL which results in a BuildError. with app.test_request_context(): pytest.raises(BuildError, flask.url_for, "spam") # Verify the error is re-raised if not the current exception. try: with app.test_request_context(): flask.url_for("spam") except BuildError as err: error = err try: raise RuntimeError("Test case where BuildError is not current.") except RuntimeError: pytest.raises(BuildError, app.handle_url_build_error, error, "spam", {}) # Test a custom handler. def handler(error, endpoint, values): # Just a test. return "/test_handler/" app.url_build_error_handlers.append(handler) with app.test_request_context(): assert flask.url_for("spam") == "/test_handler/" def test_build_error_handler_reraise(app): # Test a custom handler which reraises the BuildError def handler_raises_build_error(error, endpoint, values): raise error app.url_build_error_handlers.append(handler_raises_build_error) with app.test_request_context(): pytest.raises(BuildError, flask.url_for, "not.existing") def test_url_for_passes_special_values_to_build_error_handler(app): @app.url_build_error_handlers.append def handler(error, endpoint, values): assert values == { "_external": False, "_anchor": None, "_method": None, "_scheme": None, } return "handled" with app.test_request_context(): flask.url_for("/") def test_static_files(app, client): rv = client.get("/static/index.html") assert rv.status_code == 200 assert rv.data.strip() == b"

Hello World!

" with app.test_request_context(): assert flask.url_for("static", filename="index.html") == "/static/index.html" rv.close() def test_static_url_path(): app = flask.Flask(__name__, static_url_path="/foo") app.testing = True rv = app.test_client().get("/foo/index.html") assert rv.status_code == 200 rv.close() with app.test_request_context(): assert flask.url_for("static", filename="index.html") == "/foo/index.html" def test_static_url_path_with_ending_slash(): app = flask.Flask(__name__, static_url_path="/foo/") app.testing = True rv = app.test_client().get("/foo/index.html") assert rv.status_code == 200 rv.close() with app.test_request_context(): assert flask.url_for("static", filename="index.html") == "/foo/index.html" def test_static_url_empty_path(app): app = flask.Flask(__name__, static_folder="", static_url_path="") rv = app.test_client().open("/static/index.html", method="GET") assert rv.status_code == 200 rv.close() def test_static_url_empty_path_default(app): app = flask.Flask(__name__, static_folder="") rv = app.test_client().open("/static/index.html", method="GET") assert rv.status_code == 200 rv.close() def test_static_folder_with_pathlib_path(app): from pathlib import Path app = flask.Flask(__name__, static_folder=Path("static")) rv = app.test_client().open("/static/index.html", method="GET") assert rv.status_code == 200 rv.close() def test_static_folder_with_ending_slash(): app = flask.Flask(__name__, static_folder="static/") @app.route("/") def catch_all(path): return path rv = app.test_client().get("/catch/all") assert rv.data == b"catch/all" def test_static_route_with_host_matching(): app = flask.Flask(__name__, host_matching=True, static_host="example.com") c = app.test_client() rv = c.get("http://example.com/static/index.html") assert rv.status_code == 200 rv.close() with app.test_request_context(): rv = flask.url_for("static", filename="index.html", _external=True) assert rv == "http://example.com/static/index.html" # Providing static_host without host_matching=True should error. with pytest.raises(AssertionError): flask.Flask(__name__, static_host="example.com") # Providing host_matching=True with static_folder # but without static_host should error. with pytest.raises(AssertionError): flask.Flask(__name__, host_matching=True) # Providing host_matching=True without static_host # but with static_folder=None should not error. flask.Flask(__name__, host_matching=True, static_folder=None) def test_request_locals(): assert repr(flask.g) == "" assert not flask.g @pytest.mark.parametrize( ("subdomain_matching", "host_matching", "expect_base", "expect_abc", "expect_xyz"), [ (False, False, "default", "default", "default"), (True, False, "default", "abc", ""), (False, True, "default", "abc", "default"), ], ) def test_server_name_matching( subdomain_matching: bool, host_matching: bool, expect_base: str, expect_abc: str, expect_xyz: str, ) -> None: app = flask.Flask( __name__, subdomain_matching=subdomain_matching, host_matching=host_matching, static_host="example.test" if host_matching else None, ) app.config["SERVER_NAME"] = "example.test" @app.route("/", defaults={"name": "default"}, host="") @app.route("/", subdomain="", host=".example.test") def index(name: str) -> str: return name client = app.test_client() r = client.get(base_url="http://example.test") assert r.text == expect_base r = client.get(base_url="http://abc.example.test") assert r.text == expect_abc with pytest.warns() if subdomain_matching else nullcontext(): r = client.get(base_url="http://xyz.other.test") assert r.text == expect_xyz def test_server_name_subdomain(): app = flask.Flask(__name__, subdomain_matching=True) client = app.test_client() @app.route("/") def index(): return "default" @app.route("/", subdomain="foo") def subdomain(): return "subdomain" app.config["SERVER_NAME"] = "dev.local:5000" rv = client.get("/") assert rv.data == b"default" rv = client.get("/", "http://dev.local:5000") assert rv.data == b"default" rv = client.get("/", "https://dev.local:5000") assert rv.data == b"default" app.config["SERVER_NAME"] = "dev.local:443" rv = client.get("/", "https://dev.local") # Werkzeug 1.0 fixes matching https scheme with 443 port if rv.status_code != 404: assert rv.data == b"default" app.config["SERVER_NAME"] = "dev.local" rv = client.get("/", "https://dev.local") assert rv.data == b"default" # suppress Werkzeug 0.15 warning about name mismatch with warnings.catch_warnings(): warnings.filterwarnings( "ignore", "Current server name", UserWarning, "flask.app" ) rv = client.get("/", "http://foo.localhost") assert rv.status_code == 404 rv = client.get("/", "http://foo.dev.local") assert rv.data == b"subdomain" @pytest.mark.parametrize("key", ["TESTING", "PROPAGATE_EXCEPTIONS", "DEBUG", None]) def test_exception_propagation(app, client, key): app.testing = False @app.route("/") def index(): raise ZeroDivisionError if key is not None: app.config[key] = True with pytest.raises(ZeroDivisionError): client.get("/") else: assert client.get("/").status_code == 500 @pytest.mark.parametrize("debug", [True, False]) @pytest.mark.parametrize("use_debugger", [True, False]) @pytest.mark.parametrize("use_reloader", [True, False]) @pytest.mark.parametrize("propagate_exceptions", [None, True, False]) def test_werkzeug_passthrough_errors( monkeypatch, debug, use_debugger, use_reloader, propagate_exceptions, app ): rv = {} # Mocks werkzeug.serving.run_simple method def run_simple_mock(*args, **kwargs): rv["passthrough_errors"] = kwargs.get("passthrough_errors") monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) app.config["PROPAGATE_EXCEPTIONS"] = propagate_exceptions app.run(debug=debug, use_debugger=use_debugger, use_reloader=use_reloader) def test_url_processors(app, client): @app.url_defaults def add_language_code(endpoint, values): if flask.g.lang_code is not None and app.url_map.is_endpoint_expecting( endpoint, "lang_code" ): values.setdefault("lang_code", flask.g.lang_code) @app.url_value_preprocessor def pull_lang_code(endpoint, values): flask.g.lang_code = values.pop("lang_code", None) @app.route("//") def index(): return flask.url_for("about") @app.route("//about") def about(): return flask.url_for("something_else") @app.route("/foo") def something_else(): return flask.url_for("about", lang_code="en") assert client.get("/de/").data == b"/de/about" assert client.get("/de/about").data == b"/foo" assert client.get("/foo").data == b"/en/about" def test_inject_blueprint_url_defaults(app): bp = flask.Blueprint("foo", __name__, template_folder="template") @bp.url_defaults def bp_defaults(endpoint, values): values["page"] = "login" @bp.route("/") def view(page): pass app.register_blueprint(bp) values = dict() app.inject_url_defaults("foo.view", values) expected = dict(page="login") assert values == expected with app.test_request_context("/somepage"): url = flask.url_for("foo.view") expected = "/login" assert url == expected def test_nonascii_pathinfo(app, client): @app.route("/киртеÑÑ‚") def index(): return "Hello World!" rv = client.get("/киртеÑÑ‚") assert rv.data == b"Hello World!" def test_no_setup_after_first_request(app, client): app.debug = True @app.route("/") def index(): return "Awesome" assert client.get("/").data == b"Awesome" with pytest.raises(AssertionError) as exc_info: app.add_url_rule("/foo", endpoint="late") assert "setup method 'add_url_rule'" in str(exc_info.value) def test_routing_redirect_debugging(monkeypatch, app, client): app.config["DEBUG"] = True @app.route("/user/", methods=["GET", "POST"]) def user(): return flask.request.form["status"] # default redirect code preserves form data rv = client.post("/user", data={"status": "success"}, follow_redirects=True) assert rv.data == b"success" # 301 and 302 raise error monkeypatch.setattr(RequestRedirect, "code", 301) with client, pytest.raises(AssertionError) as exc_info: client.post("/user", data={"status": "error"}, follow_redirects=True) assert "canonical URL 'http://localhost/user/'" in str(exc_info.value) def test_route_decorator_custom_endpoint(app, client): app.debug = True @app.route("/foo/") def foo(): return flask.request.endpoint @app.route("/bar/", endpoint="bar") def for_bar(): return flask.request.endpoint @app.route("/bar/123", endpoint="123") def for_bar_foo(): return flask.request.endpoint with app.test_request_context(): assert flask.url_for("foo") == "/foo/" assert flask.url_for("bar") == "/bar/" assert flask.url_for("123") == "/bar/123" assert client.get("/foo/").data == b"foo" assert client.get("/bar/").data == b"bar" assert client.get("/bar/123").data == b"123" def test_get_method_on_g(app_ctx): assert flask.g.get("x") is None assert flask.g.get("x", 11) == 11 flask.g.x = 42 assert flask.g.get("x") == 42 assert flask.g.x == 42 def test_g_iteration_protocol(app_ctx): flask.g.foo = 23 flask.g.bar = 42 assert "foo" in flask.g assert "foos" not in flask.g assert sorted(flask.g) == ["bar", "foo"] def test_subdomain_basic_support(): app = flask.Flask(__name__, subdomain_matching=True) app.config["SERVER_NAME"] = "localhost.localdomain" client = app.test_client() @app.route("/") def normal_index(): return "normal index" @app.route("/", subdomain="test") def test_index(): return "test index" rv = client.get("/", "http://localhost.localdomain/") assert rv.data == b"normal index" rv = client.get("/", "http://test.localhost.localdomain/") assert rv.data == b"test index" def test_subdomain_matching(): app = flask.Flask(__name__, subdomain_matching=True) client = app.test_client() app.config["SERVER_NAME"] = "localhost.localdomain" @app.route("/", subdomain="") def index(user): return f"index for {user}" rv = client.get("/", "http://mitsuhiko.localhost.localdomain/") assert rv.data == b"index for mitsuhiko" def test_subdomain_matching_with_ports(): app = flask.Flask(__name__, subdomain_matching=True) app.config["SERVER_NAME"] = "localhost.localdomain:3000" client = app.test_client() @app.route("/", subdomain="") def index(user): return f"index for {user}" rv = client.get("/", "http://mitsuhiko.localhost.localdomain:3000/") assert rv.data == b"index for mitsuhiko" @pytest.mark.parametrize("matching", (False, True)) def test_subdomain_matching_other_name(matching): app = flask.Flask(__name__, subdomain_matching=matching) app.config["SERVER_NAME"] = "localhost.localdomain:3000" client = app.test_client() @app.route("/") def index(): return "", 204 # suppress Werkzeug 0.15 warning about name mismatch with warnings.catch_warnings(): warnings.filterwarnings( "ignore", "Current server name", UserWarning, "flask.app" ) # ip address can't match name rv = client.get("/", "http://127.0.0.1:3000/") assert rv.status_code == 404 if matching else 204 # allow all subdomains if matching is disabled rv = client.get("/", "http://www.localhost.localdomain:3000/") assert rv.status_code == 404 if matching else 204 def test_multi_route_rules(app, client): @app.route("/") @app.route("//") def index(test="a"): return test rv = client.open("/") assert rv.data == b"a" rv = client.open("/b/") assert rv.data == b"b" def test_multi_route_class_views(app, client): class View: def __init__(self, app): app.add_url_rule("/", "index", self.index) app.add_url_rule("//", "index", self.index) def index(self, test="a"): return test _ = View(app) rv = client.open("/") assert rv.data == b"a" rv = client.open("/b/") assert rv.data == b"b" def test_run_defaults(monkeypatch, app): rv = {} # Mocks werkzeug.serving.run_simple method def run_simple_mock(*args, **kwargs): rv["result"] = "running..." monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) app.run() assert rv["result"] == "running..." def test_run_server_port(monkeypatch, app): rv = {} # Mocks werkzeug.serving.run_simple method def run_simple_mock(hostname, port, application, *args, **kwargs): rv["result"] = f"running on {hostname}:{port} ..." monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) hostname, port = "localhost", 8000 app.run(hostname, port, debug=True) assert rv["result"] == f"running on {hostname}:{port} ..." @pytest.mark.parametrize( "host,port,server_name,expect_host,expect_port", ( (None, None, "pocoo.org:8080", "pocoo.org", 8080), ("localhost", None, "pocoo.org:8080", "localhost", 8080), (None, 80, "pocoo.org:8080", "pocoo.org", 80), ("localhost", 80, "pocoo.org:8080", "localhost", 80), ("localhost", 0, "localhost:8080", "localhost", 0), (None, None, "localhost:8080", "localhost", 8080), (None, None, "localhost:0", "localhost", 0), ), ) def test_run_from_config( monkeypatch, host, port, server_name, expect_host, expect_port, app ): def run_simple_mock(hostname, port, *args, **kwargs): assert hostname == expect_host assert port == expect_port monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) app.config["SERVER_NAME"] = server_name app.run(host, port) def test_max_cookie_size(app, client, recwarn): app.config["MAX_COOKIE_SIZE"] = 100 # outside app context, default to Werkzeug static value, # which is also the default config response = flask.Response() default = flask.Flask.default_config["MAX_COOKIE_SIZE"] assert response.max_cookie_size == default # inside app context, use app config with app.app_context(): assert flask.Response().max_cookie_size == 100 @app.route("/") def index(): r = flask.Response("", status=204) r.set_cookie("foo", "bar" * 100) return r client.get("/") assert len(recwarn) == 1 w = recwarn.pop() assert "cookie is too large" in str(w.message) app.config["MAX_COOKIE_SIZE"] = 0 client.get("/") assert len(recwarn) == 0 @require_cpython_gc def test_app_freed_on_zero_refcount(): # A Flask instance should not create a reference cycle that prevents CPython # from freeing it when all external references to it are released (see #3761). gc.disable() try: app = flask.Flask(__name__) assert app.view_functions["static"] weak = weakref.ref(app) assert weak() is not None del app assert weak() is None finally: gc.enable() flask-3.1.1/tests/test_blueprints.py000066400000000000000000000701521501065614000175610ustar00rootroot00000000000000import pytest from jinja2 import TemplateNotFound from werkzeug.http import parse_cache_control_header import flask def test_blueprint_specific_error_handling(app, client): frontend = flask.Blueprint("frontend", __name__) backend = flask.Blueprint("backend", __name__) sideend = flask.Blueprint("sideend", __name__) @frontend.errorhandler(403) def frontend_forbidden(e): return "frontend says no", 403 @frontend.route("/frontend-no") def frontend_no(): flask.abort(403) @backend.errorhandler(403) def backend_forbidden(e): return "backend says no", 403 @backend.route("/backend-no") def backend_no(): flask.abort(403) @sideend.route("/what-is-a-sideend") def sideend_no(): flask.abort(403) app.register_blueprint(frontend) app.register_blueprint(backend) app.register_blueprint(sideend) @app.errorhandler(403) def app_forbidden(e): return "application itself says no", 403 assert client.get("/frontend-no").data == b"frontend says no" assert client.get("/backend-no").data == b"backend says no" assert client.get("/what-is-a-sideend").data == b"application itself says no" def test_blueprint_specific_user_error_handling(app, client): class MyDecoratorException(Exception): pass class MyFunctionException(Exception): pass blue = flask.Blueprint("blue", __name__) @blue.errorhandler(MyDecoratorException) def my_decorator_exception_handler(e): assert isinstance(e, MyDecoratorException) return "boom" def my_function_exception_handler(e): assert isinstance(e, MyFunctionException) return "bam" blue.register_error_handler(MyFunctionException, my_function_exception_handler) @blue.route("/decorator") def blue_deco_test(): raise MyDecoratorException() @blue.route("/function") def blue_func_test(): raise MyFunctionException() app.register_blueprint(blue) assert client.get("/decorator").data == b"boom" assert client.get("/function").data == b"bam" def test_blueprint_app_error_handling(app, client): errors = flask.Blueprint("errors", __name__) @errors.app_errorhandler(403) def forbidden_handler(e): return "you shall not pass", 403 @app.route("/forbidden") def app_forbidden(): flask.abort(403) forbidden_bp = flask.Blueprint("forbidden_bp", __name__) @forbidden_bp.route("/nope") def bp_forbidden(): flask.abort(403) app.register_blueprint(errors) app.register_blueprint(forbidden_bp) assert client.get("/forbidden").data == b"you shall not pass" assert client.get("/nope").data == b"you shall not pass" @pytest.mark.parametrize( ("prefix", "rule", "url"), ( ("", "/", "/"), ("/", "", "/"), ("/", "/", "/"), ("/foo", "", "/foo"), ("/foo/", "", "/foo/"), ("", "/bar", "/bar"), ("/foo/", "/bar", "/foo/bar"), ("/foo/", "bar", "/foo/bar"), ("/foo", "/bar", "/foo/bar"), ("/foo/", "//bar", "/foo/bar"), ("/foo//", "/bar", "/foo/bar"), ), ) def test_blueprint_prefix_slash(app, client, prefix, rule, url): bp = flask.Blueprint("test", __name__, url_prefix=prefix) @bp.route(rule) def index(): return "", 204 app.register_blueprint(bp) assert client.get(url).status_code == 204 def test_blueprint_url_defaults(app, client): bp = flask.Blueprint("test", __name__) @bp.route("/foo", defaults={"baz": 42}) def foo(bar, baz): return f"{bar}/{baz:d}" @bp.route("/bar") def bar(bar): return str(bar) app.register_blueprint(bp, url_prefix="/1", url_defaults={"bar": 23}) app.register_blueprint(bp, name="test2", url_prefix="/2", url_defaults={"bar": 19}) assert client.get("/1/foo").data == b"23/42" assert client.get("/2/foo").data == b"19/42" assert client.get("/1/bar").data == b"23" assert client.get("/2/bar").data == b"19" def test_blueprint_url_processors(app, client): bp = flask.Blueprint("frontend", __name__, url_prefix="/") @bp.url_defaults def add_language_code(endpoint, values): values.setdefault("lang_code", flask.g.lang_code) @bp.url_value_preprocessor def pull_lang_code(endpoint, values): flask.g.lang_code = values.pop("lang_code") @bp.route("/") def index(): return flask.url_for(".about") @bp.route("/about") def about(): return flask.url_for(".index") app.register_blueprint(bp) assert client.get("/de/").data == b"/de/about" assert client.get("/de/about").data == b"/de/" def test_templates_and_static(test_apps): from blueprintapp import app client = app.test_client() rv = client.get("/") assert rv.data == b"Hello from the Frontend" rv = client.get("/admin/") assert rv.data == b"Hello from the Admin" rv = client.get("/admin/index2") assert rv.data == b"Hello from the Admin" rv = client.get("/admin/static/test.txt") assert rv.data.strip() == b"Admin File" rv.close() rv = client.get("/admin/static/css/test.css") assert rv.data.strip() == b"/* nested file */" rv.close() # try/finally, in case other tests use this app for Blueprint tests. max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"] try: expected_max_age = 3600 if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == expected_max_age: expected_max_age = 7200 app.config["SEND_FILE_MAX_AGE_DEFAULT"] = expected_max_age rv = client.get("/admin/static/css/test.css") cc = parse_cache_control_header(rv.headers["Cache-Control"]) assert cc.max_age == expected_max_age rv.close() finally: app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default with app.test_request_context(): assert ( flask.url_for("admin.static", filename="test.txt") == "/admin/static/test.txt" ) with app.test_request_context(): with pytest.raises(TemplateNotFound) as e: flask.render_template("missing.html") assert e.value.name == "missing.html" with flask.Flask(__name__).test_request_context(): assert flask.render_template("nested/nested.txt") == "I'm nested" def test_default_static_max_age(app): class MyBlueprint(flask.Blueprint): def get_send_file_max_age(self, filename): return 100 blueprint = MyBlueprint("blueprint", __name__, static_folder="static") app.register_blueprint(blueprint) # try/finally, in case other tests use this app for Blueprint tests. max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"] try: with app.test_request_context(): unexpected_max_age = 3600 if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == unexpected_max_age: unexpected_max_age = 7200 app.config["SEND_FILE_MAX_AGE_DEFAULT"] = unexpected_max_age rv = blueprint.send_static_file("index.html") cc = parse_cache_control_header(rv.headers["Cache-Control"]) assert cc.max_age == 100 rv.close() finally: app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default def test_templates_list(test_apps): from blueprintapp import app templates = sorted(app.jinja_env.list_templates()) assert templates == ["admin/index.html", "frontend/index.html"] def test_dotted_name_not_allowed(app, client): with pytest.raises(ValueError): flask.Blueprint("app.ui", __name__) def test_empty_name_not_allowed(app, client): with pytest.raises(ValueError): flask.Blueprint("", __name__) def test_dotted_names_from_app(app, client): test = flask.Blueprint("test", __name__) @app.route("/") def app_index(): return flask.url_for("test.index") @test.route("/test/") def index(): return flask.url_for("app_index") app.register_blueprint(test) rv = client.get("/") assert rv.data == b"/test/" def test_empty_url_defaults(app, client): bp = flask.Blueprint("bp", __name__) @bp.route("/", defaults={"page": 1}) @bp.route("/page/") def something(page): return str(page) app.register_blueprint(bp) assert client.get("/").data == b"1" assert client.get("/page/2").data == b"2" def test_route_decorator_custom_endpoint(app, client): bp = flask.Blueprint("bp", __name__) @bp.route("/foo") def foo(): return flask.request.endpoint @bp.route("/bar", endpoint="bar") def foo_bar(): return flask.request.endpoint @bp.route("/bar/123", endpoint="123") def foo_bar_foo(): return flask.request.endpoint @bp.route("/bar/foo") def bar_foo(): return flask.request.endpoint app.register_blueprint(bp, url_prefix="/py") @app.route("/") def index(): return flask.request.endpoint assert client.get("/").data == b"index" assert client.get("/py/foo").data == b"bp.foo" assert client.get("/py/bar").data == b"bp.bar" assert client.get("/py/bar/123").data == b"bp.123" assert client.get("/py/bar/foo").data == b"bp.bar_foo" def test_route_decorator_custom_endpoint_with_dots(app, client): bp = flask.Blueprint("bp", __name__) with pytest.raises(ValueError): bp.route("/", endpoint="a.b")(lambda: "") with pytest.raises(ValueError): bp.add_url_rule("/", endpoint="a.b") def view(): return "" view.__name__ = "a.b" with pytest.raises(ValueError): bp.add_url_rule("/", view_func=view) def test_endpoint_decorator(app, client): from werkzeug.routing import Rule app.url_map.add(Rule("/foo", endpoint="bar")) bp = flask.Blueprint("bp", __name__) @bp.endpoint("bar") def foobar(): return flask.request.endpoint app.register_blueprint(bp, url_prefix="/bp_prefix") assert client.get("/foo").data == b"bar" assert client.get("/bp_prefix/bar").status_code == 404 def test_template_filter(app): bp = flask.Blueprint("bp", __name__) @bp.app_template_filter() def my_reverse(s): return s[::-1] app.register_blueprint(bp, url_prefix="/py") assert "my_reverse" in app.jinja_env.filters.keys() assert app.jinja_env.filters["my_reverse"] == my_reverse assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" def test_add_template_filter(app): bp = flask.Blueprint("bp", __name__) def my_reverse(s): return s[::-1] bp.add_app_template_filter(my_reverse) app.register_blueprint(bp, url_prefix="/py") assert "my_reverse" in app.jinja_env.filters.keys() assert app.jinja_env.filters["my_reverse"] == my_reverse assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" def test_template_filter_with_name(app): bp = flask.Blueprint("bp", __name__) @bp.app_template_filter("strrev") def my_reverse(s): return s[::-1] app.register_blueprint(bp, url_prefix="/py") assert "strrev" in app.jinja_env.filters.keys() assert app.jinja_env.filters["strrev"] == my_reverse assert app.jinja_env.filters["strrev"]("abcd") == "dcba" def test_add_template_filter_with_name(app): bp = flask.Blueprint("bp", __name__) def my_reverse(s): return s[::-1] bp.add_app_template_filter(my_reverse, "strrev") app.register_blueprint(bp, url_prefix="/py") assert "strrev" in app.jinja_env.filters.keys() assert app.jinja_env.filters["strrev"] == my_reverse assert app.jinja_env.filters["strrev"]("abcd") == "dcba" def test_template_filter_with_template(app, client): bp = flask.Blueprint("bp", __name__) @bp.app_template_filter() def super_reverse(s): return s[::-1] app.register_blueprint(bp, url_prefix="/py") @app.route("/") def index(): return flask.render_template("template_filter.html", value="abcd") rv = client.get("/") assert rv.data == b"dcba" def test_template_filter_after_route_with_template(app, client): @app.route("/") def index(): return flask.render_template("template_filter.html", value="abcd") bp = flask.Blueprint("bp", __name__) @bp.app_template_filter() def super_reverse(s): return s[::-1] app.register_blueprint(bp, url_prefix="/py") rv = client.get("/") assert rv.data == b"dcba" def test_add_template_filter_with_template(app, client): bp = flask.Blueprint("bp", __name__) def super_reverse(s): return s[::-1] bp.add_app_template_filter(super_reverse) app.register_blueprint(bp, url_prefix="/py") @app.route("/") def index(): return flask.render_template("template_filter.html", value="abcd") rv = client.get("/") assert rv.data == b"dcba" def test_template_filter_with_name_and_template(app, client): bp = flask.Blueprint("bp", __name__) @bp.app_template_filter("super_reverse") def my_reverse(s): return s[::-1] app.register_blueprint(bp, url_prefix="/py") @app.route("/") def index(): return flask.render_template("template_filter.html", value="abcd") rv = client.get("/") assert rv.data == b"dcba" def test_add_template_filter_with_name_and_template(app, client): bp = flask.Blueprint("bp", __name__) def my_reverse(s): return s[::-1] bp.add_app_template_filter(my_reverse, "super_reverse") app.register_blueprint(bp, url_prefix="/py") @app.route("/") def index(): return flask.render_template("template_filter.html", value="abcd") rv = client.get("/") assert rv.data == b"dcba" def test_template_test(app): bp = flask.Blueprint("bp", __name__) @bp.app_template_test() def is_boolean(value): return isinstance(value, bool) app.register_blueprint(bp, url_prefix="/py") assert "is_boolean" in app.jinja_env.tests.keys() assert app.jinja_env.tests["is_boolean"] == is_boolean assert app.jinja_env.tests["is_boolean"](False) def test_add_template_test(app): bp = flask.Blueprint("bp", __name__) def is_boolean(value): return isinstance(value, bool) bp.add_app_template_test(is_boolean) app.register_blueprint(bp, url_prefix="/py") assert "is_boolean" in app.jinja_env.tests.keys() assert app.jinja_env.tests["is_boolean"] == is_boolean assert app.jinja_env.tests["is_boolean"](False) def test_template_test_with_name(app): bp = flask.Blueprint("bp", __name__) @bp.app_template_test("boolean") def is_boolean(value): return isinstance(value, bool) app.register_blueprint(bp, url_prefix="/py") assert "boolean" in app.jinja_env.tests.keys() assert app.jinja_env.tests["boolean"] == is_boolean assert app.jinja_env.tests["boolean"](False) def test_add_template_test_with_name(app): bp = flask.Blueprint("bp", __name__) def is_boolean(value): return isinstance(value, bool) bp.add_app_template_test(is_boolean, "boolean") app.register_blueprint(bp, url_prefix="/py") assert "boolean" in app.jinja_env.tests.keys() assert app.jinja_env.tests["boolean"] == is_boolean assert app.jinja_env.tests["boolean"](False) def test_template_test_with_template(app, client): bp = flask.Blueprint("bp", __name__) @bp.app_template_test() def boolean(value): return isinstance(value, bool) app.register_blueprint(bp, url_prefix="/py") @app.route("/") def index(): return flask.render_template("template_test.html", value=False) rv = client.get("/") assert b"Success!" in rv.data def test_template_test_after_route_with_template(app, client): @app.route("/") def index(): return flask.render_template("template_test.html", value=False) bp = flask.Blueprint("bp", __name__) @bp.app_template_test() def boolean(value): return isinstance(value, bool) app.register_blueprint(bp, url_prefix="/py") rv = client.get("/") assert b"Success!" in rv.data def test_add_template_test_with_template(app, client): bp = flask.Blueprint("bp", __name__) def boolean(value): return isinstance(value, bool) bp.add_app_template_test(boolean) app.register_blueprint(bp, url_prefix="/py") @app.route("/") def index(): return flask.render_template("template_test.html", value=False) rv = client.get("/") assert b"Success!" in rv.data def test_template_test_with_name_and_template(app, client): bp = flask.Blueprint("bp", __name__) @bp.app_template_test("boolean") def is_boolean(value): return isinstance(value, bool) app.register_blueprint(bp, url_prefix="/py") @app.route("/") def index(): return flask.render_template("template_test.html", value=False) rv = client.get("/") assert b"Success!" in rv.data def test_add_template_test_with_name_and_template(app, client): bp = flask.Blueprint("bp", __name__) def is_boolean(value): return isinstance(value, bool) bp.add_app_template_test(is_boolean, "boolean") app.register_blueprint(bp, url_prefix="/py") @app.route("/") def index(): return flask.render_template("template_test.html", value=False) rv = client.get("/") assert b"Success!" in rv.data def test_context_processing(app, client): answer_bp = flask.Blueprint("answer_bp", __name__) def template_string(): return flask.render_template_string( "{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}" "{% if answer %}{{ answer }} is the answer.{% endif %}" ) # App global context processor @answer_bp.app_context_processor def not_answer_context_processor(): return {"notanswer": 43} # Blueprint local context processor @answer_bp.context_processor def answer_context_processor(): return {"answer": 42} # Setup endpoints for testing @answer_bp.route("/bp") def bp_page(): return template_string() @app.route("/") def app_page(): return template_string() # Register the blueprint app.register_blueprint(answer_bp) app_page_bytes = client.get("/").data answer_page_bytes = client.get("/bp").data assert b"43" in app_page_bytes assert b"42" not in app_page_bytes assert b"42" in answer_page_bytes assert b"43" in answer_page_bytes def test_template_global(app): bp = flask.Blueprint("bp", __name__) @bp.app_template_global() def get_answer(): return 42 # Make sure the function is not in the jinja_env already assert "get_answer" not in app.jinja_env.globals.keys() app.register_blueprint(bp) # Tests assert "get_answer" in app.jinja_env.globals.keys() assert app.jinja_env.globals["get_answer"] is get_answer assert app.jinja_env.globals["get_answer"]() == 42 with app.app_context(): rv = flask.render_template_string("{{ get_answer() }}") assert rv == "42" def test_request_processing(app, client): bp = flask.Blueprint("bp", __name__) evts = [] @bp.before_request def before_bp(): evts.append("before") @bp.after_request def after_bp(response): response.data += b"|after" evts.append("after") return response @bp.teardown_request def teardown_bp(exc): evts.append("teardown") # Setup routes for testing @bp.route("/bp") def bp_endpoint(): return "request" app.register_blueprint(bp) assert evts == [] rv = client.get("/bp") assert rv.data == b"request|after" assert evts == ["before", "after", "teardown"] def test_app_request_processing(app, client): bp = flask.Blueprint("bp", __name__) evts = [] @bp.before_app_request def before_app(): evts.append("before") @bp.after_app_request def after_app(response): response.data += b"|after" evts.append("after") return response @bp.teardown_app_request def teardown_app(exc): evts.append("teardown") app.register_blueprint(bp) # Setup routes for testing @app.route("/") def bp_endpoint(): return "request" # before first request assert evts == [] # first request resp = client.get("/").data assert resp == b"request|after" assert evts == ["before", "after", "teardown"] # second request resp = client.get("/").data assert resp == b"request|after" assert evts == ["before", "after", "teardown"] * 2 def test_app_url_processors(app, client): bp = flask.Blueprint("bp", __name__) # Register app-wide url defaults and preprocessor on blueprint @bp.app_url_defaults def add_language_code(endpoint, values): values.setdefault("lang_code", flask.g.lang_code) @bp.app_url_value_preprocessor def pull_lang_code(endpoint, values): flask.g.lang_code = values.pop("lang_code") # Register route rules at the app level @app.route("//") def index(): return flask.url_for("about") @app.route("//about") def about(): return flask.url_for("index") app.register_blueprint(bp) assert client.get("/de/").data == b"/de/about" assert client.get("/de/about").data == b"/de/" def test_nested_blueprint(app, client): parent = flask.Blueprint("parent", __name__) child = flask.Blueprint("child", __name__) grandchild = flask.Blueprint("grandchild", __name__) @parent.errorhandler(403) def forbidden(e): return "Parent no", 403 @parent.route("/") def parent_index(): return "Parent yes" @parent.route("/no") def parent_no(): flask.abort(403) @child.route("/") def child_index(): return "Child yes" @child.route("/no") def child_no(): flask.abort(403) @grandchild.errorhandler(403) def grandchild_forbidden(e): return "Grandchild no", 403 @grandchild.route("/") def grandchild_index(): return "Grandchild yes" @grandchild.route("/no") def grandchild_no(): flask.abort(403) child.register_blueprint(grandchild, url_prefix="/grandchild") parent.register_blueprint(child, url_prefix="/child") app.register_blueprint(parent, url_prefix="/parent") assert client.get("/parent/").data == b"Parent yes" assert client.get("/parent/child/").data == b"Child yes" assert client.get("/parent/child/grandchild/").data == b"Grandchild yes" assert client.get("/parent/no").data == b"Parent no" assert client.get("/parent/child/no").data == b"Parent no" assert client.get("/parent/child/grandchild/no").data == b"Grandchild no" def test_nested_callback_order(app, client): parent = flask.Blueprint("parent", __name__) child = flask.Blueprint("child", __name__) @app.before_request def app_before1(): flask.g.setdefault("seen", []).append("app_1") @app.teardown_request def app_teardown1(e=None): assert flask.g.seen.pop() == "app_1" @app.before_request def app_before2(): flask.g.setdefault("seen", []).append("app_2") @app.teardown_request def app_teardown2(e=None): assert flask.g.seen.pop() == "app_2" @app.context_processor def app_ctx(): return dict(key="app") @parent.before_request def parent_before1(): flask.g.setdefault("seen", []).append("parent_1") @parent.teardown_request def parent_teardown1(e=None): assert flask.g.seen.pop() == "parent_1" @parent.before_request def parent_before2(): flask.g.setdefault("seen", []).append("parent_2") @parent.teardown_request def parent_teardown2(e=None): assert flask.g.seen.pop() == "parent_2" @parent.context_processor def parent_ctx(): return dict(key="parent") @child.before_request def child_before1(): flask.g.setdefault("seen", []).append("child_1") @child.teardown_request def child_teardown1(e=None): assert flask.g.seen.pop() == "child_1" @child.before_request def child_before2(): flask.g.setdefault("seen", []).append("child_2") @child.teardown_request def child_teardown2(e=None): assert flask.g.seen.pop() == "child_2" @child.context_processor def child_ctx(): return dict(key="child") @child.route("/a") def a(): return ", ".join(flask.g.seen) @child.route("/b") def b(): return flask.render_template_string("{{ key }}") parent.register_blueprint(child) app.register_blueprint(parent) assert ( client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2" ) assert client.get("/b").data == b"child" @pytest.mark.parametrize( "parent_init, child_init, parent_registration, child_registration", [ ("/parent", "/child", None, None), ("/parent", None, None, "/child"), (None, None, "/parent", "/child"), ("/other", "/something", "/parent", "/child"), ], ) def test_nesting_url_prefixes( parent_init, child_init, parent_registration, child_registration, app, client, ) -> None: parent = flask.Blueprint("parent", __name__, url_prefix=parent_init) child = flask.Blueprint("child", __name__, url_prefix=child_init) @child.route("/") def index(): return "index" parent.register_blueprint(child, url_prefix=child_registration) app.register_blueprint(parent, url_prefix=parent_registration) response = client.get("/parent/child/") assert response.status_code == 200 def test_nesting_subdomains(app, client) -> None: app.subdomain_matching = True app.config["SERVER_NAME"] = "example.test" client.allow_subdomain_redirects = True parent = flask.Blueprint("parent", __name__) child = flask.Blueprint("child", __name__) @child.route("/child/") def index(): return "child" parent.register_blueprint(child) app.register_blueprint(parent, subdomain="api") response = client.get("/child/", base_url="http://api.example.test") assert response.status_code == 200 def test_child_and_parent_subdomain(app, client) -> None: app.subdomain_matching = True app.config["SERVER_NAME"] = "example.test" client.allow_subdomain_redirects = True parent = flask.Blueprint("parent", __name__) child = flask.Blueprint("child", __name__, subdomain="api") @child.route("/") def index(): return "child" parent.register_blueprint(child) app.register_blueprint(parent, subdomain="parent") response = client.get("/", base_url="http://api.parent.example.test") assert response.status_code == 200 response = client.get("/", base_url="http://parent.example.test") assert response.status_code == 404 def test_unique_blueprint_names(app, client) -> None: bp = flask.Blueprint("bp", __name__) bp2 = flask.Blueprint("bp", __name__) app.register_blueprint(bp) with pytest.raises(ValueError): app.register_blueprint(bp) # same bp, same name, error app.register_blueprint(bp, name="again") # same bp, different name, ok with pytest.raises(ValueError): app.register_blueprint(bp2) # different bp, same name, error app.register_blueprint(bp2, name="alt") # different bp, different name, ok def test_self_registration(app, client) -> None: bp = flask.Blueprint("bp", __name__) with pytest.raises(ValueError): bp.register_blueprint(bp) def test_blueprint_renaming(app, client) -> None: bp = flask.Blueprint("bp", __name__) bp2 = flask.Blueprint("bp2", __name__) @bp.get("/") def index(): return flask.request.endpoint @bp.get("/error") def error(): flask.abort(403) @bp.errorhandler(403) def forbidden(_: Exception): return "Error", 403 @bp2.get("/") def index2(): return flask.request.endpoint bp.register_blueprint(bp2, url_prefix="/a", name="sub") app.register_blueprint(bp, url_prefix="/a") app.register_blueprint(bp, url_prefix="/b", name="alt") assert client.get("/a/").data == b"bp.index" assert client.get("/b/").data == b"alt.index" assert client.get("/a/a/").data == b"bp.sub.index2" assert client.get("/b/a/").data == b"alt.sub.index2" assert client.get("/a/error").data == b"Error" assert client.get("/b/error").data == b"Error" flask-3.1.1/tests/test_cli.py000066400000000000000000000476201501065614000161450ustar00rootroot00000000000000# This file was part of Flask-CLI and was modified under the terms of # its Revised BSD License. Copyright © 2015 CERN. import importlib.metadata import os import platform import ssl import sys import types from functools import partial from pathlib import Path import click import pytest from _pytest.monkeypatch import notset from click.testing import CliRunner from flask import Blueprint from flask import current_app from flask import Flask from flask.cli import AppGroup from flask.cli import find_best_app from flask.cli import FlaskGroup from flask.cli import get_version from flask.cli import load_dotenv from flask.cli import locate_app from flask.cli import NoAppException from flask.cli import prepare_import from flask.cli import run_command from flask.cli import ScriptInfo from flask.cli import with_appcontext cwd = Path.cwd() test_path = (Path(__file__) / ".." / "test_apps").resolve() @pytest.fixture def runner(): return CliRunner() def test_cli_name(test_apps): """Make sure the CLI object's name is the app's name and not the app itself""" from cliapp.app import testapp assert testapp.cli.name == testapp.name def test_find_best_app(test_apps): class Module: app = Flask("appname") assert find_best_app(Module) == Module.app class Module: application = Flask("appname") assert find_best_app(Module) == Module.application class Module: myapp = Flask("appname") assert find_best_app(Module) == Module.myapp class Module: @staticmethod def create_app(): return Flask("appname") app = find_best_app(Module) assert isinstance(app, Flask) assert app.name == "appname" class Module: @staticmethod def create_app(**kwargs): return Flask("appname") app = find_best_app(Module) assert isinstance(app, Flask) assert app.name == "appname" class Module: @staticmethod def make_app(): return Flask("appname") app = find_best_app(Module) assert isinstance(app, Flask) assert app.name == "appname" class Module: myapp = Flask("appname1") @staticmethod def create_app(): return Flask("appname2") assert find_best_app(Module) == Module.myapp class Module: myapp = Flask("appname1") @staticmethod def create_app(): return Flask("appname2") assert find_best_app(Module) == Module.myapp class Module: pass pytest.raises(NoAppException, find_best_app, Module) class Module: myapp1 = Flask("appname1") myapp2 = Flask("appname2") pytest.raises(NoAppException, find_best_app, Module) class Module: @staticmethod def create_app(foo, bar): return Flask("appname2") pytest.raises(NoAppException, find_best_app, Module) class Module: @staticmethod def create_app(): raise TypeError("bad bad factory!") pytest.raises(TypeError, find_best_app, Module) @pytest.mark.parametrize( "value,path,result", ( ("test", cwd, "test"), ("test.py", cwd, "test"), ("a/test", cwd / "a", "test"), ("test/__init__.py", cwd, "test"), ("test/__init__", cwd, "test"), # nested package ( test_path / "cliapp" / "inner1" / "__init__", test_path, "cliapp.inner1", ), ( test_path / "cliapp" / "inner1" / "inner2", test_path, "cliapp.inner1.inner2", ), # dotted name ("test.a.b", cwd, "test.a.b"), (test_path / "cliapp.app", test_path, "cliapp.app"), # not a Python file, will be caught during import (test_path / "cliapp" / "message.txt", test_path, "cliapp.message.txt"), ), ) def test_prepare_import(request, value, path, result): """Expect the correct path to be set and the correct import and app names to be returned. :func:`prepare_exec_for_file` has a side effect where the parent directory of the given import is added to :data:`sys.path`. This is reset after the test runs. """ original_path = sys.path[:] def reset_path(): sys.path[:] = original_path request.addfinalizer(reset_path) assert prepare_import(value) == result assert sys.path[0] == str(path) @pytest.mark.parametrize( "iname,aname,result", ( ("cliapp.app", None, "testapp"), ("cliapp.app", "testapp", "testapp"), ("cliapp.factory", None, "app"), ("cliapp.factory", "create_app", "app"), ("cliapp.factory", "create_app()", "app"), ("cliapp.factory", 'create_app2("foo", "bar")', "app2_foo_bar"), # trailing comma space ("cliapp.factory", 'create_app2("foo", "bar", )', "app2_foo_bar"), # strip whitespace ("cliapp.factory", " create_app () ", "app"), ), ) def test_locate_app(test_apps, iname, aname, result): assert locate_app(iname, aname).name == result @pytest.mark.parametrize( "iname,aname", ( ("notanapp.py", None), ("cliapp/app", None), ("cliapp.app", "notanapp"), # not enough arguments ("cliapp.factory", 'create_app2("foo")'), # invalid identifier ("cliapp.factory", "create_app("), # no app returned ("cliapp.factory", "no_app"), # nested import error ("cliapp.importerrorapp", None), # not a Python file ("cliapp.message.txt", None), ), ) def test_locate_app_raises(test_apps, iname, aname): with pytest.raises(NoAppException): locate_app(iname, aname) def test_locate_app_suppress_raise(test_apps): app = locate_app("notanapp.py", None, raise_if_not_found=False) assert app is None # only direct import error is suppressed with pytest.raises(NoAppException): locate_app("cliapp.importerrorapp", None, raise_if_not_found=False) def test_get_version(test_apps, capsys): class MockCtx: resilient_parsing = False color = None def exit(self): return ctx = MockCtx() get_version(ctx, None, "test") out, err = capsys.readouterr() assert f"Python {platform.python_version()}" in out assert f"Flask {importlib.metadata.version('flask')}" in out assert f"Werkzeug {importlib.metadata.version('werkzeug')}" in out def test_scriptinfo(test_apps, monkeypatch): obj = ScriptInfo(app_import_path="cliapp.app:testapp") app = obj.load_app() assert app.name == "testapp" assert obj.load_app() is app # import app with module's absolute path cli_app_path = str(test_path / "cliapp" / "app.py") obj = ScriptInfo(app_import_path=cli_app_path) app = obj.load_app() assert app.name == "testapp" assert obj.load_app() is app obj = ScriptInfo(app_import_path=f"{cli_app_path}:testapp") app = obj.load_app() assert app.name == "testapp" assert obj.load_app() is app def create_app(): return Flask("createapp") obj = ScriptInfo(create_app=create_app) app = obj.load_app() assert app.name == "createapp" assert obj.load_app() is app obj = ScriptInfo() pytest.raises(NoAppException, obj.load_app) # import app from wsgi.py in current directory monkeypatch.chdir(test_path / "helloworld") obj = ScriptInfo() app = obj.load_app() assert app.name == "hello" # import app from app.py in current directory monkeypatch.chdir(test_path / "cliapp") obj = ScriptInfo() app = obj.load_app() assert app.name == "testapp" def test_app_cli_has_app_context(app, runner): def _param_cb(ctx, param, value): # current_app should be available in parameter callbacks return bool(current_app) @app.cli.command() @click.argument("value", callback=_param_cb) def check(value): app = click.get_current_context().obj.load_app() # the loaded app should be the same as current_app same_app = current_app._get_current_object() is app return same_app, value cli = FlaskGroup(create_app=lambda: app) result = runner.invoke(cli, ["check", "x"], standalone_mode=False) assert result.return_value == (True, True) def test_with_appcontext(runner): @click.command() @with_appcontext def testcmd(): click.echo(current_app.name) obj = ScriptInfo(create_app=lambda: Flask("testapp")) result = runner.invoke(testcmd, obj=obj) assert result.exit_code == 0 assert result.output == "testapp\n" def test_appgroup_app_context(runner): @click.group(cls=AppGroup) def cli(): pass @cli.command() def test(): click.echo(current_app.name) @cli.group() def subgroup(): pass @subgroup.command() def test2(): click.echo(current_app.name) obj = ScriptInfo(create_app=lambda: Flask("testappgroup")) result = runner.invoke(cli, ["test"], obj=obj) assert result.exit_code == 0 assert result.output == "testappgroup\n" result = runner.invoke(cli, ["subgroup", "test2"], obj=obj) assert result.exit_code == 0 assert result.output == "testappgroup\n" def test_flaskgroup_app_context(runner): def create_app(): return Flask("flaskgroup") @click.group(cls=FlaskGroup, create_app=create_app) def cli(**params): pass @cli.command() def test(): click.echo(current_app.name) result = runner.invoke(cli, ["test"]) assert result.exit_code == 0 assert result.output == "flaskgroup\n" @pytest.mark.parametrize("set_debug_flag", (True, False)) def test_flaskgroup_debug(runner, set_debug_flag): def create_app(): app = Flask("flaskgroup") app.debug = True return app @click.group(cls=FlaskGroup, create_app=create_app, set_debug_flag=set_debug_flag) def cli(**params): pass @cli.command() def test(): click.echo(str(current_app.debug)) result = runner.invoke(cli, ["test"]) assert result.exit_code == 0 assert result.output == f"{not set_debug_flag}\n" def test_flaskgroup_nested(app, runner): cli = click.Group("cli") flask_group = FlaskGroup(name="flask", create_app=lambda: app) cli.add_command(flask_group) @flask_group.command() def show(): click.echo(current_app.name) result = runner.invoke(cli, ["flask", "show"]) assert result.output == "flask_test\n" def test_no_command_echo_loading_error(): from flask.cli import cli try: runner = CliRunner(mix_stderr=False) except (DeprecationWarning, TypeError): # Click >= 8.2 runner = CliRunner() result = runner.invoke(cli, ["missing"]) assert result.exit_code == 2 assert "FLASK_APP" in result.stderr assert "Usage:" in result.stderr def test_help_echo_loading_error(): from flask.cli import cli try: runner = CliRunner(mix_stderr=False) except (DeprecationWarning, TypeError): # Click >= 8.2 runner = CliRunner() result = runner.invoke(cli, ["--help"]) assert result.exit_code == 0 assert "FLASK_APP" in result.stderr assert "Usage:" in result.stdout def test_help_echo_exception(): def create_app(): raise Exception("oh no") cli = FlaskGroup(create_app=create_app) try: runner = CliRunner(mix_stderr=False) except (DeprecationWarning, TypeError): # Click >= 8.2 runner = CliRunner() result = runner.invoke(cli, ["--help"]) assert result.exit_code == 0 assert "Exception: oh no" in result.stderr assert "Usage:" in result.stdout class TestRoutes: @pytest.fixture def app(self): app = Flask(__name__) app.add_url_rule( "/get_post//", methods=["GET", "POST"], endpoint="yyy_get_post", ) app.add_url_rule("/zzz_post", methods=["POST"], endpoint="aaa_post") return app @pytest.fixture def invoke(self, app, runner): cli = FlaskGroup(create_app=lambda: app) return partial(runner.invoke, cli) def expect_order(self, order, output): # skip the header and match the start of each row for expect, line in zip(order, output.splitlines()[2:]): # do this instead of startswith for nicer pytest output assert line[: len(expect)] == expect def test_simple(self, invoke): result = invoke(["routes"]) assert result.exit_code == 0 self.expect_order(["aaa_post", "static", "yyy_get_post"], result.output) def test_sort(self, app, invoke): default_output = invoke(["routes"]).output endpoint_output = invoke(["routes", "-s", "endpoint"]).output assert default_output == endpoint_output self.expect_order( ["static", "yyy_get_post", "aaa_post"], invoke(["routes", "-s", "methods"]).output, ) self.expect_order( ["yyy_get_post", "static", "aaa_post"], invoke(["routes", "-s", "rule"]).output, ) match_order = [r.endpoint for r in app.url_map.iter_rules()] self.expect_order(match_order, invoke(["routes", "-s", "match"]).output) def test_all_methods(self, invoke): output = invoke(["routes"]).output assert "GET, HEAD, OPTIONS, POST" not in output output = invoke(["routes", "--all-methods"]).output assert "GET, HEAD, OPTIONS, POST" in output def test_no_routes(self, runner): app = Flask(__name__, static_folder=None) cli = FlaskGroup(create_app=lambda: app) result = runner.invoke(cli, ["routes"]) assert result.exit_code == 0 assert "No routes were registered." in result.output def test_subdomain(self, runner): app = Flask(__name__, static_folder=None) app.add_url_rule("/a", subdomain="a", endpoint="a") app.add_url_rule("/b", subdomain="b", endpoint="b") cli = FlaskGroup(create_app=lambda: app) result = runner.invoke(cli, ["routes"]) assert result.exit_code == 0 assert "Subdomain" in result.output def test_host(self, runner): app = Flask(__name__, static_folder=None, host_matching=True) app.add_url_rule("/a", host="a", endpoint="a") app.add_url_rule("/b", host="b", endpoint="b") cli = FlaskGroup(create_app=lambda: app) result = runner.invoke(cli, ["routes"]) assert result.exit_code == 0 assert "Host" in result.output def dotenv_not_available(): try: import dotenv # noqa: F401 except ImportError: return True return False need_dotenv = pytest.mark.skipif( dotenv_not_available(), reason="dotenv is not installed" ) @need_dotenv def test_load_dotenv(monkeypatch): # can't use monkeypatch.delitem since the keys don't exist yet for item in ("FOO", "BAR", "SPAM", "HAM"): monkeypatch._setitem.append((os.environ, item, notset)) monkeypatch.setenv("EGGS", "3") monkeypatch.chdir(test_path) assert load_dotenv() assert Path.cwd() == test_path # .flaskenv doesn't overwrite .env assert os.environ["FOO"] == "env" # set only in .flaskenv assert os.environ["BAR"] == "bar" # set only in .env assert os.environ["SPAM"] == "1" # set manually, files don't overwrite assert os.environ["EGGS"] == "3" # test env file encoding assert os.environ["HAM"] == "ç«è…¿" # Non existent file should not load assert not load_dotenv("non-existent-file", load_defaults=False) @need_dotenv def test_dotenv_path(monkeypatch): for item in ("FOO", "BAR", "EGGS"): monkeypatch._setitem.append((os.environ, item, notset)) load_dotenv(test_path / ".flaskenv") assert Path.cwd() == cwd assert "FOO" in os.environ def test_dotenv_optional(monkeypatch): monkeypatch.setitem(sys.modules, "dotenv", None) monkeypatch.chdir(test_path) load_dotenv() assert "FOO" not in os.environ @need_dotenv def test_disable_dotenv_from_env(monkeypatch, runner): monkeypatch.chdir(test_path) monkeypatch.setitem(os.environ, "FLASK_SKIP_DOTENV", "1") runner.invoke(FlaskGroup()) assert "FOO" not in os.environ def test_run_cert_path(): # no key with pytest.raises(click.BadParameter): run_command.make_context("run", ["--cert", __file__]) # no cert with pytest.raises(click.BadParameter): run_command.make_context("run", ["--key", __file__]) # cert specified first ctx = run_command.make_context("run", ["--cert", __file__, "--key", __file__]) assert ctx.params["cert"] == (__file__, __file__) # key specified first ctx = run_command.make_context("run", ["--key", __file__, "--cert", __file__]) assert ctx.params["cert"] == (__file__, __file__) def test_run_cert_adhoc(monkeypatch): monkeypatch.setitem(sys.modules, "cryptography", None) # cryptography not installed with pytest.raises(click.BadParameter): run_command.make_context("run", ["--cert", "adhoc"]) # cryptography installed monkeypatch.setitem(sys.modules, "cryptography", types.ModuleType("cryptography")) ctx = run_command.make_context("run", ["--cert", "adhoc"]) assert ctx.params["cert"] == "adhoc" # no key with adhoc with pytest.raises(click.BadParameter): run_command.make_context("run", ["--cert", "adhoc", "--key", __file__]) def test_run_cert_import(monkeypatch): monkeypatch.setitem(sys.modules, "not_here", None) # ImportError with pytest.raises(click.BadParameter): run_command.make_context("run", ["--cert", "not_here"]) with pytest.raises(click.BadParameter): run_command.make_context("run", ["--cert", "flask"]) # SSLContext ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) monkeypatch.setitem(sys.modules, "ssl_context", ssl_context) ctx = run_command.make_context("run", ["--cert", "ssl_context"]) assert ctx.params["cert"] is ssl_context # no --key with SSLContext with pytest.raises(click.BadParameter): run_command.make_context("run", ["--cert", "ssl_context", "--key", __file__]) def test_run_cert_no_ssl(monkeypatch): monkeypatch.setitem(sys.modules, "ssl", None) with pytest.raises(click.BadParameter): run_command.make_context("run", ["--cert", "not_here"]) def test_cli_blueprints(app): """Test blueprint commands register correctly to the application""" custom = Blueprint("custom", __name__, cli_group="customized") nested = Blueprint("nested", __name__) merged = Blueprint("merged", __name__, cli_group=None) late = Blueprint("late", __name__) @custom.cli.command("custom") def custom_command(): click.echo("custom_result") @nested.cli.command("nested") def nested_command(): click.echo("nested_result") @merged.cli.command("merged") def merged_command(): click.echo("merged_result") @late.cli.command("late") def late_command(): click.echo("late_result") app.register_blueprint(custom) app.register_blueprint(nested) app.register_blueprint(merged) app.register_blueprint(late, cli_group="late_registration") app_runner = app.test_cli_runner() result = app_runner.invoke(args=["customized", "custom"]) assert "custom_result" in result.output result = app_runner.invoke(args=["nested", "nested"]) assert "nested_result" in result.output result = app_runner.invoke(args=["merged"]) assert "merged_result" in result.output result = app_runner.invoke(args=["late_registration", "late"]) assert "late_result" in result.output def test_cli_empty(app): """If a Blueprint's CLI group is empty, do not register it.""" bp = Blueprint("blue", __name__, cli_group="blue") app.register_blueprint(bp) result = app.test_cli_runner().invoke(args=["blue", "--help"]) assert result.exit_code == 2, f"Unexpected success:\n\n{result.output}" def test_run_exclude_patterns(): ctx = run_command.make_context("run", ["--exclude-patterns", __file__]) assert ctx.params["exclude_patterns"] == [__file__] flask-3.1.1/tests/test_config.py000066400000000000000000000171041501065614000166350ustar00rootroot00000000000000import json import os import pytest import flask # config keys used for the TestConfig TEST_KEY = "foo" SECRET_KEY = "config" def common_object_test(app): assert app.secret_key == "config" assert app.config["TEST_KEY"] == "foo" assert "TestConfig" not in app.config def test_config_from_pyfile(): app = flask.Flask(__name__) app.config.from_pyfile(f"{__file__.rsplit('.', 1)[0]}.py") common_object_test(app) def test_config_from_object(): app = flask.Flask(__name__) app.config.from_object(__name__) common_object_test(app) def test_config_from_file_json(): app = flask.Flask(__name__) current_dir = os.path.dirname(os.path.abspath(__file__)) app.config.from_file(os.path.join(current_dir, "static", "config.json"), json.load) common_object_test(app) def test_config_from_file_toml(): tomllib = pytest.importorskip("tomllib", reason="tomllib added in 3.11") app = flask.Flask(__name__) current_dir = os.path.dirname(os.path.abspath(__file__)) app.config.from_file( os.path.join(current_dir, "static", "config.toml"), tomllib.load, text=False ) common_object_test(app) def test_from_prefixed_env(monkeypatch): monkeypatch.setenv("FLASK_STRING", "value") monkeypatch.setenv("FLASK_BOOL", "true") monkeypatch.setenv("FLASK_INT", "1") monkeypatch.setenv("FLASK_FLOAT", "1.2") monkeypatch.setenv("FLASK_LIST", "[1, 2]") monkeypatch.setenv("FLASK_DICT", '{"k": "v"}') monkeypatch.setenv("NOT_FLASK_OTHER", "other") app = flask.Flask(__name__) app.config.from_prefixed_env() assert app.config["STRING"] == "value" assert app.config["BOOL"] is True assert app.config["INT"] == 1 assert app.config["FLOAT"] == 1.2 assert app.config["LIST"] == [1, 2] assert app.config["DICT"] == {"k": "v"} assert "OTHER" not in app.config def test_from_prefixed_env_custom_prefix(monkeypatch): monkeypatch.setenv("FLASK_A", "a") monkeypatch.setenv("NOT_FLASK_A", "b") app = flask.Flask(__name__) app.config.from_prefixed_env("NOT_FLASK") assert app.config["A"] == "b" def test_from_prefixed_env_nested(monkeypatch): monkeypatch.setenv("FLASK_EXIST__ok", "other") monkeypatch.setenv("FLASK_EXIST__inner__ik", "2") monkeypatch.setenv("FLASK_EXIST__new__more", '{"k": false}') monkeypatch.setenv("FLASK_NEW__K", "v") app = flask.Flask(__name__) app.config["EXIST"] = {"ok": "value", "flag": True, "inner": {"ik": 1}} app.config.from_prefixed_env() if os.name != "nt": assert app.config["EXIST"] == { "ok": "other", "flag": True, "inner": {"ik": 2}, "new": {"more": {"k": False}}, } else: # Windows env var keys are always uppercase. assert app.config["EXIST"] == { "ok": "value", "OK": "other", "flag": True, "inner": {"ik": 1}, "INNER": {"IK": 2}, "NEW": {"MORE": {"k": False}}, } assert app.config["NEW"] == {"K": "v"} def test_config_from_mapping(): app = flask.Flask(__name__) app.config.from_mapping({"SECRET_KEY": "config", "TEST_KEY": "foo"}) common_object_test(app) app = flask.Flask(__name__) app.config.from_mapping([("SECRET_KEY", "config"), ("TEST_KEY", "foo")]) common_object_test(app) app = flask.Flask(__name__) app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo") common_object_test(app) app = flask.Flask(__name__) app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo", skip_key="skip") common_object_test(app) app = flask.Flask(__name__) with pytest.raises(TypeError): app.config.from_mapping({}, {}) def test_config_from_class(): class Base: TEST_KEY = "foo" class Test(Base): SECRET_KEY = "config" app = flask.Flask(__name__) app.config.from_object(Test) common_object_test(app) def test_config_from_envvar(monkeypatch): monkeypatch.setattr("os.environ", {}) app = flask.Flask(__name__) with pytest.raises(RuntimeError) as e: app.config.from_envvar("FOO_SETTINGS") assert "'FOO_SETTINGS' is not set" in str(e.value) assert not app.config.from_envvar("FOO_SETTINGS", silent=True) monkeypatch.setattr( "os.environ", {"FOO_SETTINGS": f"{__file__.rsplit('.', 1)[0]}.py"} ) assert app.config.from_envvar("FOO_SETTINGS") common_object_test(app) def test_config_from_envvar_missing(monkeypatch): monkeypatch.setattr("os.environ", {"FOO_SETTINGS": "missing.cfg"}) app = flask.Flask(__name__) with pytest.raises(IOError) as e: app.config.from_envvar("FOO_SETTINGS") msg = str(e.value) assert msg.startswith( "[Errno 2] Unable to load configuration file (No such file or directory):" ) assert msg.endswith("missing.cfg'") assert not app.config.from_envvar("FOO_SETTINGS", silent=True) def test_config_missing(): app = flask.Flask(__name__) with pytest.raises(IOError) as e: app.config.from_pyfile("missing.cfg") msg = str(e.value) assert msg.startswith( "[Errno 2] Unable to load configuration file (No such file or directory):" ) assert msg.endswith("missing.cfg'") assert not app.config.from_pyfile("missing.cfg", silent=True) def test_config_missing_file(): app = flask.Flask(__name__) with pytest.raises(IOError) as e: app.config.from_file("missing.json", load=json.load) msg = str(e.value) assert msg.startswith( "[Errno 2] Unable to load configuration file (No such file or directory):" ) assert msg.endswith("missing.json'") assert not app.config.from_file("missing.json", load=json.load, silent=True) def test_custom_config_class(): class Config(flask.Config): pass class Flask(flask.Flask): config_class = Config app = Flask(__name__) assert isinstance(app.config, Config) app.config.from_object(__name__) common_object_test(app) def test_session_lifetime(): app = flask.Flask(__name__) app.config["PERMANENT_SESSION_LIFETIME"] = 42 assert app.permanent_session_lifetime.seconds == 42 def test_get_namespace(): app = flask.Flask(__name__) app.config["FOO_OPTION_1"] = "foo option 1" app.config["FOO_OPTION_2"] = "foo option 2" app.config["BAR_STUFF_1"] = "bar stuff 1" app.config["BAR_STUFF_2"] = "bar stuff 2" foo_options = app.config.get_namespace("FOO_") assert 2 == len(foo_options) assert "foo option 1" == foo_options["option_1"] assert "foo option 2" == foo_options["option_2"] bar_options = app.config.get_namespace("BAR_", lowercase=False) assert 2 == len(bar_options) assert "bar stuff 1" == bar_options["STUFF_1"] assert "bar stuff 2" == bar_options["STUFF_2"] foo_options = app.config.get_namespace("FOO_", trim_namespace=False) assert 2 == len(foo_options) assert "foo option 1" == foo_options["foo_option_1"] assert "foo option 2" == foo_options["foo_option_2"] bar_options = app.config.get_namespace( "BAR_", lowercase=False, trim_namespace=False ) assert 2 == len(bar_options) assert "bar stuff 1" == bar_options["BAR_STUFF_1"] assert "bar stuff 2" == bar_options["BAR_STUFF_2"] @pytest.mark.parametrize("encoding", ["utf-8", "iso-8859-15", "latin-1"]) def test_from_pyfile_weird_encoding(tmp_path, encoding): f = tmp_path / "my_config.py" f.write_text(f'# -*- coding: {encoding} -*-\nTEST_VALUE = "föö"\n', encoding) app = flask.Flask(__name__) app.config.from_pyfile(os.fspath(f)) value = app.config["TEST_VALUE"] assert value == "föö" flask-3.1.1/tests/test_converters.py000066400000000000000000000021051501065614000175550ustar00rootroot00000000000000from werkzeug.routing import BaseConverter from flask import request from flask import session from flask import url_for def test_custom_converters(app, client): class ListConverter(BaseConverter): def to_python(self, value): return value.split(",") def to_url(self, value): base_to_url = super().to_url return ",".join(base_to_url(x) for x in value) app.url_map.converters["list"] = ListConverter @app.route("/") def index(args): return "|".join(args) assert client.get("/1,2,3").data == b"1|2|3" with app.test_request_context(): assert url_for("index", args=[4, 5, 6]) == "/4,5,6" def test_context_available(app, client): class ContextConverter(BaseConverter): def to_python(self, value): assert request is not None assert session is not None return value app.url_map.converters["ctx"] = ContextConverter @app.get("/") def index(name): return name assert client.get("/admin").data == b"admin" flask-3.1.1/tests/test_helpers.py000066400000000000000000000245341501065614000170370ustar00rootroot00000000000000import io import os import pytest import werkzeug.exceptions import flask from flask.helpers import get_debug_flag class FakePath: """Fake object to represent a ``PathLike object``. This represents a ``pathlib.Path`` object in python 3. See: https://www.python.org/dev/peps/pep-0519/ """ def __init__(self, path): self.path = path def __fspath__(self): return self.path class PyBytesIO: def __init__(self, *args, **kwargs): self._io = io.BytesIO(*args, **kwargs) def __getattr__(self, name): return getattr(self._io, name) class TestSendfile: def test_send_file(self, app, req_ctx): rv = flask.send_file("static/index.html") assert rv.direct_passthrough assert rv.mimetype == "text/html" with app.open_resource("static/index.html") as f: rv.direct_passthrough = False assert rv.data == f.read() rv.close() def test_static_file(self, app, req_ctx): # Default max_age is None. # Test with static file handler. rv = app.send_static_file("index.html") assert rv.cache_control.max_age is None rv.close() # Test with direct use of send_file. rv = flask.send_file("static/index.html") assert rv.cache_control.max_age is None rv.close() app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 3600 # Test with static file handler. rv = app.send_static_file("index.html") assert rv.cache_control.max_age == 3600 rv.close() # Test with direct use of send_file. rv = flask.send_file("static/index.html") assert rv.cache_control.max_age == 3600 rv.close() # Test with pathlib.Path. rv = app.send_static_file(FakePath("index.html")) assert rv.cache_control.max_age == 3600 rv.close() class StaticFileApp(flask.Flask): def get_send_file_max_age(self, filename): return 10 app = StaticFileApp(__name__) with app.test_request_context(): # Test with static file handler. rv = app.send_static_file("index.html") assert rv.cache_control.max_age == 10 rv.close() # Test with direct use of send_file. rv = flask.send_file("static/index.html") assert rv.cache_control.max_age == 10 rv.close() def test_send_from_directory(self, app, req_ctx): app.root_path = os.path.join( os.path.dirname(__file__), "test_apps", "subdomaintestmodule" ) rv = flask.send_from_directory("static", "hello.txt") rv.direct_passthrough = False assert rv.data.strip() == b"Hello Subdomain" rv.close() class TestUrlFor: def test_url_for_with_anchor(self, app, req_ctx): @app.route("/") def index(): return "42" assert flask.url_for("index", _anchor="x y") == "/#x%20y" def test_url_for_with_scheme(self, app, req_ctx): @app.route("/") def index(): return "42" assert ( flask.url_for("index", _external=True, _scheme="https") == "https://localhost/" ) def test_url_for_with_scheme_not_external(self, app, req_ctx): app.add_url_rule("/", endpoint="index") # Implicit external with scheme. url = flask.url_for("index", _scheme="https") assert url == "https://localhost/" # Error when external=False with scheme with pytest.raises(ValueError): flask.url_for("index", _scheme="https", _external=False) def test_url_for_with_alternating_schemes(self, app, req_ctx): @app.route("/") def index(): return "42" assert flask.url_for("index", _external=True) == "http://localhost/" assert ( flask.url_for("index", _external=True, _scheme="https") == "https://localhost/" ) assert flask.url_for("index", _external=True) == "http://localhost/" def test_url_with_method(self, app, req_ctx): from flask.views import MethodView class MyView(MethodView): def get(self, id=None): if id is None: return "List" return f"Get {id:d}" def post(self): return "Create" myview = MyView.as_view("myview") app.add_url_rule("/myview/", methods=["GET"], view_func=myview) app.add_url_rule("/myview/", methods=["GET"], view_func=myview) app.add_url_rule("/myview/create", methods=["POST"], view_func=myview) assert flask.url_for("myview", _method="GET") == "/myview/" assert flask.url_for("myview", id=42, _method="GET") == "/myview/42" assert flask.url_for("myview", _method="POST") == "/myview/create" def test_url_for_with_self(self, app, req_ctx): @app.route("/") def index(self): return "42" assert flask.url_for("index", self="2") == "/2" def test_redirect_no_app(): response = flask.redirect("https://localhost", 307) assert response.location == "https://localhost" assert response.status_code == 307 def test_redirect_with_app(app): def redirect(location, code=302): raise ValueError app.redirect = redirect with app.app_context(), pytest.raises(ValueError): flask.redirect("other") def test_abort_no_app(): with pytest.raises(werkzeug.exceptions.Unauthorized): flask.abort(401) with pytest.raises(LookupError): flask.abort(900) def test_app_aborter_class(): class MyAborter(werkzeug.exceptions.Aborter): pass class MyFlask(flask.Flask): aborter_class = MyAborter app = MyFlask(__name__) assert isinstance(app.aborter, MyAborter) def test_abort_with_app(app): class My900Error(werkzeug.exceptions.HTTPException): code = 900 app.aborter.mapping[900] = My900Error with app.app_context(), pytest.raises(My900Error): flask.abort(900) class TestNoImports: """Test Flasks are created without import. Avoiding ``__import__`` helps create Flask instances where there are errors at import time. Those runtime errors will be apparent to the user soon enough, but tools which build Flask instances meta-programmatically benefit from a Flask which does not ``__import__``. Instead of importing to retrieve file paths or metadata on a module or package, use the pkgutil and imp modules in the Python standard library. """ def test_name_with_import_error(self, modules_tmp_path): (modules_tmp_path / "importerror.py").write_text("raise NotImplementedError()") try: flask.Flask("importerror") except NotImplementedError: AssertionError("Flask(import_name) is importing import_name.") class TestStreaming: def test_streaming_with_context(self, app, client): @app.route("/") def index(): def generate(): yield "Hello " yield flask.request.args["name"] yield "!" return flask.Response(flask.stream_with_context(generate())) rv = client.get("/?name=World") assert rv.data == b"Hello World!" def test_streaming_with_context_as_decorator(self, app, client): @app.route("/") def index(): @flask.stream_with_context def generate(hello): yield hello yield flask.request.args["name"] yield "!" return flask.Response(generate("Hello ")) rv = client.get("/?name=World") assert rv.data == b"Hello World!" def test_streaming_with_context_and_custom_close(self, app, client): called = [] class Wrapper: def __init__(self, gen): self._gen = gen def __iter__(self): return self def close(self): called.append(42) def __next__(self): return next(self._gen) next = __next__ @app.route("/") def index(): def generate(): yield "Hello " yield flask.request.args["name"] yield "!" return flask.Response(flask.stream_with_context(Wrapper(generate()))) rv = client.get("/?name=World") assert rv.data == b"Hello World!" assert called == [42] def test_stream_keeps_session(self, app, client): @app.route("/") def index(): flask.session["test"] = "flask" @flask.stream_with_context def gen(): yield flask.session["test"] return flask.Response(gen()) rv = client.get("/") assert rv.data == b"flask" class TestHelpers: @pytest.mark.parametrize( ("debug", "expect"), [ ("", False), ("0", False), ("False", False), ("No", False), ("True", True), ], ) def test_get_debug_flag(self, monkeypatch, debug, expect): monkeypatch.setenv("FLASK_DEBUG", debug) assert get_debug_flag() == expect def test_make_response(self): app = flask.Flask(__name__) with app.test_request_context(): rv = flask.helpers.make_response() assert rv.status_code == 200 assert rv.mimetype == "text/html" rv = flask.helpers.make_response("Hello") assert rv.status_code == 200 assert rv.data == b"Hello" assert rv.mimetype == "text/html" @pytest.mark.parametrize("mode", ("r", "rb", "rt")) def test_open_resource(mode): app = flask.Flask(__name__) with app.open_resource("static/index.html", mode) as f: assert "

Hello World!

" in str(f.read()) @pytest.mark.parametrize("mode", ("w", "x", "a", "r+")) def test_open_resource_exceptions(mode): app = flask.Flask(__name__) with pytest.raises(ValueError): app.open_resource("static/index.html", mode) @pytest.mark.parametrize("encoding", ("utf-8", "utf-16-le")) def test_open_resource_with_encoding(tmp_path, encoding): app = flask.Flask(__name__, root_path=os.fspath(tmp_path)) (tmp_path / "test").write_text("test", encoding=encoding) with app.open_resource("test", mode="rt", encoding=encoding) as f: assert f.read() == "test" flask-3.1.1/tests/test_instance_config.py000066400000000000000000000063651501065614000205300ustar00rootroot00000000000000import os import pytest import flask def test_explicit_instance_paths(modules_tmp_path): with pytest.raises(ValueError, match=".*must be absolute"): flask.Flask(__name__, instance_path="instance") app = flask.Flask(__name__, instance_path=os.fspath(modules_tmp_path)) assert app.instance_path == os.fspath(modules_tmp_path) def test_uninstalled_module_paths(modules_tmp_path, purge_module): (modules_tmp_path / "config_module_app.py").write_text( "import os\n" "import flask\n" "here = os.path.abspath(os.path.dirname(__file__))\n" "app = flask.Flask(__name__)\n" ) purge_module("config_module_app") from config_module_app import app assert app.instance_path == os.fspath(modules_tmp_path / "instance") def test_uninstalled_package_paths(modules_tmp_path, purge_module): app = modules_tmp_path / "config_package_app" app.mkdir() (app / "__init__.py").write_text( "import os\n" "import flask\n" "here = os.path.abspath(os.path.dirname(__file__))\n" "app = flask.Flask(__name__)\n" ) purge_module("config_package_app") from config_package_app import app assert app.instance_path == os.fspath(modules_tmp_path / "instance") def test_uninstalled_namespace_paths(tmp_path, monkeypatch, purge_module): def create_namespace(package): project = tmp_path / f"project-{package}" monkeypatch.syspath_prepend(os.fspath(project)) ns = project / "namespace" / package ns.mkdir(parents=True) (ns / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") return project _ = create_namespace("package1") project2 = create_namespace("package2") purge_module("namespace.package2") purge_module("namespace") from namespace.package2 import app assert app.instance_path == os.fspath(project2 / "instance") def test_installed_module_paths( modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages ): (site_packages / "site_app.py").write_text( "import flask\napp = flask.Flask(__name__)\n" ) purge_module("site_app") from site_app import app assert app.instance_path == os.fspath( modules_tmp_path / "var" / "site_app-instance" ) def test_installed_package_paths( modules_tmp_path, modules_tmp_path_prefix, purge_module, monkeypatch ): installed_path = modules_tmp_path / "path" installed_path.mkdir() monkeypatch.syspath_prepend(installed_path) app = installed_path / "installed_package" app.mkdir() (app / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") purge_module("installed_package") from installed_package import app assert app.instance_path == os.fspath( modules_tmp_path / "var" / "installed_package-instance" ) def test_prefix_package_paths( modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages ): app = site_packages / "site_package" app.mkdir() (app / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") purge_module("site_package") import site_package assert site_package.app.instance_path == os.fspath( modules_tmp_path / "var" / "site_package-instance" ) flask-3.1.1/tests/test_json.py000066400000000000000000000210451501065614000163400ustar00rootroot00000000000000import datetime import decimal import io import uuid import pytest from werkzeug.http import http_date import flask from flask import json from flask.json.provider import DefaultJSONProvider @pytest.mark.parametrize("debug", (True, False)) def test_bad_request_debug_message(app, client, debug): app.config["DEBUG"] = debug app.config["TRAP_BAD_REQUEST_ERRORS"] = False @app.route("/json", methods=["POST"]) def post_json(): flask.request.get_json() return None rv = client.post("/json", data=None, content_type="application/json") assert rv.status_code == 400 contains = b"Failed to decode JSON object" in rv.data assert contains == debug def test_json_bad_requests(app, client): @app.route("/json", methods=["POST"]) def return_json(): return flask.jsonify(foo=str(flask.request.get_json())) rv = client.post("/json", data="malformed", content_type="application/json") assert rv.status_code == 400 def test_json_custom_mimetypes(app, client): @app.route("/json", methods=["POST"]) def return_json(): return flask.request.get_json() rv = client.post("/json", data='"foo"', content_type="application/x+json") assert rv.data == b"foo" @pytest.mark.parametrize( "test_value,expected", [(True, '"\\u2603"'), (False, '"\u2603"')] ) def test_json_as_unicode(test_value, expected, app, app_ctx): app.json.ensure_ascii = test_value rv = app.json.dumps("\N{SNOWMAN}") assert rv == expected def test_json_dump_to_file(app, app_ctx): test_data = {"name": "Flask"} out = io.StringIO() flask.json.dump(test_data, out) out.seek(0) rv = flask.json.load(out) assert rv == test_data @pytest.mark.parametrize( "test_value", [0, -1, 1, 23, 3.14, "s", "longer string", True, False, None] ) def test_jsonify_basic_types(test_value, app, client): url = "/jsonify_basic_types" app.add_url_rule(url, url, lambda x=test_value: flask.jsonify(x)) rv = client.get(url) assert rv.mimetype == "application/json" assert flask.json.loads(rv.data) == test_value def test_jsonify_dicts(app, client): d = { "a": 0, "b": 23, "c": 3.14, "d": "t", "e": "Hi", "f": True, "g": False, "h": ["test list", 10, False], "i": {"test": "dict"}, } @app.route("/kw") def return_kwargs(): return flask.jsonify(**d) @app.route("/dict") def return_dict(): return flask.jsonify(d) for url in "/kw", "/dict": rv = client.get(url) assert rv.mimetype == "application/json" assert flask.json.loads(rv.data) == d def test_jsonify_arrays(app, client): """Test jsonify of lists and args unpacking.""" a_list = [ 0, 42, 3.14, "t", "hello", True, False, ["test list", 2, False], {"test": "dict"}, ] @app.route("/args_unpack") def return_args_unpack(): return flask.jsonify(*a_list) @app.route("/array") def return_array(): return flask.jsonify(a_list) for url in "/args_unpack", "/array": rv = client.get(url) assert rv.mimetype == "application/json" assert flask.json.loads(rv.data) == a_list @pytest.mark.parametrize( "value", [datetime.datetime(1973, 3, 11, 6, 30, 45), datetime.date(1975, 1, 5)] ) def test_jsonify_datetime(app, client, value): @app.route("/") def index(): return flask.jsonify(value=value) r = client.get() assert r.json["value"] == http_date(value) class FixedOffset(datetime.tzinfo): """Fixed offset in hours east from UTC. This is a slight adaptation of the ``FixedOffset`` example found in https://docs.python.org/2.7/library/datetime.html. """ def __init__(self, hours, name): self.__offset = datetime.timedelta(hours=hours) self.__name = name def utcoffset(self, dt): return self.__offset def tzname(self, dt): return self.__name def dst(self, dt): return datetime.timedelta() @pytest.mark.parametrize("tz", (("UTC", 0), ("PST", -8), ("KST", 9))) def test_jsonify_aware_datetimes(tz): """Test if aware datetime.datetime objects are converted into GMT.""" tzinfo = FixedOffset(hours=tz[1], name=tz[0]) dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo) gmt = FixedOffset(hours=0, name="GMT") expected = dt.astimezone(gmt).strftime('"%a, %d %b %Y %H:%M:%S %Z"') assert flask.json.dumps(dt) == expected def test_jsonify_uuid_types(app, client): """Test jsonify with uuid.UUID types""" test_uuid = uuid.UUID(bytes=b"\xde\xad\xbe\xef" * 4) url = "/uuid_test" app.add_url_rule(url, url, lambda: flask.jsonify(x=test_uuid)) rv = client.get(url) rv_x = flask.json.loads(rv.data)["x"] assert rv_x == str(test_uuid) rv_uuid = uuid.UUID(rv_x) assert rv_uuid == test_uuid def test_json_decimal(): rv = flask.json.dumps(decimal.Decimal("0.003")) assert rv == '"0.003"' def test_json_attr(app, client): @app.route("/add", methods=["POST"]) def add(): json = flask.request.get_json() return str(json["a"] + json["b"]) rv = client.post( "/add", data=flask.json.dumps({"a": 1, "b": 2}), content_type="application/json", ) assert rv.data == b"3" def test_tojson_filter(app, req_ctx): # The tojson filter is tested in Jinja, this confirms that it's # using Flask's dumps. rv = flask.render_template_string( "const data = {{ data|tojson }};", data={"name": "", "time": datetime.datetime(2021, 2, 1, 7, 15)}, ) assert rv == ( 'const data = {"name": "\\u003c/script\\u003e",' ' "time": "Mon, 01 Feb 2021 07:15:00 GMT"};' ) def test_json_customization(app, client): class X: # noqa: B903, for Python2 compatibility def __init__(self, val): self.val = val def default(o): if isinstance(o, X): return f"<{o.val}>" return DefaultJSONProvider.default(o) class CustomProvider(DefaultJSONProvider): def object_hook(self, obj): if len(obj) == 1 and "_foo" in obj: return X(obj["_foo"]) return obj def loads(self, s, **kwargs): kwargs.setdefault("object_hook", self.object_hook) return super().loads(s, **kwargs) app.json = CustomProvider(app) app.json.default = default @app.route("/", methods=["POST"]) def index(): return flask.json.dumps(flask.request.get_json()["x"]) rv = client.post( "/", data=flask.json.dumps({"x": {"_foo": 42}}), content_type="application/json", ) assert rv.data == b'"<42>"' def _has_encoding(name): try: import codecs codecs.lookup(name) return True except LookupError: return False def test_json_key_sorting(app, client): app.debug = True assert app.json.sort_keys d = dict.fromkeys(range(20), "foo") @app.route("/") def index(): return flask.jsonify(values=d) rv = client.get("/") lines = [x.strip() for x in rv.data.strip().decode("utf-8").splitlines()] sorted_by_str = [ "{", '"values": {', '"0": "foo",', '"1": "foo",', '"10": "foo",', '"11": "foo",', '"12": "foo",', '"13": "foo",', '"14": "foo",', '"15": "foo",', '"16": "foo",', '"17": "foo",', '"18": "foo",', '"19": "foo",', '"2": "foo",', '"3": "foo",', '"4": "foo",', '"5": "foo",', '"6": "foo",', '"7": "foo",', '"8": "foo",', '"9": "foo"', "}", "}", ] sorted_by_int = [ "{", '"values": {', '"0": "foo",', '"1": "foo",', '"2": "foo",', '"3": "foo",', '"4": "foo",', '"5": "foo",', '"6": "foo",', '"7": "foo",', '"8": "foo",', '"9": "foo",', '"10": "foo",', '"11": "foo",', '"12": "foo",', '"13": "foo",', '"14": "foo",', '"15": "foo",', '"16": "foo",', '"17": "foo",', '"18": "foo",', '"19": "foo"', "}", "}", ] try: assert lines == sorted_by_int except AssertionError: assert lines == sorted_by_str def test_html_method(): class ObjectWithHTML: def __html__(self): return "

test

" result = json.dumps(ObjectWithHTML()) assert result == '"

test

"' flask-3.1.1/tests/test_json_tag.py000066400000000000000000000037161501065614000172000ustar00rootroot00000000000000from datetime import datetime from datetime import timezone from uuid import uuid4 import pytest from markupsafe import Markup from flask.json.tag import JSONTag from flask.json.tag import TaggedJSONSerializer @pytest.mark.parametrize( "data", ( {" t": (1, 2, 3)}, {" t__": b"a"}, {" di": " di"}, {"x": (1, 2, 3), "y": 4}, (1, 2, 3), [(1, 2, 3)], b"\xff", Markup(""), uuid4(), datetime.now(tz=timezone.utc).replace(microsecond=0), ), ) def test_dump_load_unchanged(data): s = TaggedJSONSerializer() assert s.loads(s.dumps(data)) == data def test_duplicate_tag(): class TagDict(JSONTag): key = " d" s = TaggedJSONSerializer() pytest.raises(KeyError, s.register, TagDict) s.register(TagDict, force=True, index=0) assert isinstance(s.tags[" d"], TagDict) assert isinstance(s.order[0], TagDict) def test_custom_tag(): class Foo: # noqa: B903, for Python2 compatibility def __init__(self, data): self.data = data class TagFoo(JSONTag): __slots__ = () key = " f" def check(self, value): return isinstance(value, Foo) def to_json(self, value): return self.serializer.tag(value.data) def to_python(self, value): return Foo(value) s = TaggedJSONSerializer() s.register(TagFoo) assert s.loads(s.dumps(Foo("bar"))).data == "bar" def test_tag_interface(): t = JSONTag(None) pytest.raises(NotImplementedError, t.check, None) pytest.raises(NotImplementedError, t.to_json, None) pytest.raises(NotImplementedError, t.to_python, None) def test_tag_order(): class Tag1(JSONTag): key = " 1" class Tag2(JSONTag): key = " 2" s = TaggedJSONSerializer() s.register(Tag1, index=-1) assert isinstance(s.order[-2], Tag1) s.register(Tag2, index=None) assert isinstance(s.order[-1], Tag2) flask-3.1.1/tests/test_logging.py000066400000000000000000000047411501065614000170210ustar00rootroot00000000000000import logging import sys from io import StringIO import pytest from flask.logging import default_handler from flask.logging import has_level_handler from flask.logging import wsgi_errors_stream @pytest.fixture(autouse=True) def reset_logging(pytestconfig): root_handlers = logging.root.handlers[:] logging.root.handlers = [] root_level = logging.root.level logger = logging.getLogger("flask_test") logger.handlers = [] logger.setLevel(logging.NOTSET) logging_plugin = pytestconfig.pluginmanager.unregister(name="logging-plugin") yield logging.root.handlers[:] = root_handlers logging.root.setLevel(root_level) logger.handlers = [] logger.setLevel(logging.NOTSET) if logging_plugin: pytestconfig.pluginmanager.register(logging_plugin, "logging-plugin") def test_logger(app): assert app.logger.name == "flask_test" assert app.logger.level == logging.NOTSET assert app.logger.handlers == [default_handler] def test_logger_debug(app): app.debug = True assert app.logger.level == logging.DEBUG assert app.logger.handlers == [default_handler] def test_existing_handler(app): logging.root.addHandler(logging.StreamHandler()) assert app.logger.level == logging.NOTSET assert not app.logger.handlers def test_wsgi_errors_stream(app, client): @app.route("/") def index(): app.logger.error("test") return "" stream = StringIO() client.get("/", errors_stream=stream) assert "ERROR in test_logging: test" in stream.getvalue() assert wsgi_errors_stream._get_current_object() is sys.stderr with app.test_request_context(errors_stream=stream): assert wsgi_errors_stream._get_current_object() is stream def test_has_level_handler(): logger = logging.getLogger("flask.app") assert not has_level_handler(logger) handler = logging.StreamHandler() logging.root.addHandler(handler) assert has_level_handler(logger) logger.propagate = False assert not has_level_handler(logger) logger.propagate = True handler.setLevel(logging.ERROR) assert not has_level_handler(logger) def test_log_view_exception(app, client): @app.route("/") def index(): raise Exception("test") app.testing = False stream = StringIO() rv = client.get("/", errors_stream=stream) assert rv.status_code == 500 assert rv.data err = stream.getvalue() assert "Exception on / [GET]" in err assert "Exception: test" in err flask-3.1.1/tests/test_regression.py000066400000000000000000000013101501065614000175400ustar00rootroot00000000000000import flask def test_aborting(app): class Foo(Exception): whatever = 42 @app.errorhandler(Foo) def handle_foo(e): return str(e.whatever) @app.route("/") def index(): raise flask.abort(flask.redirect(flask.url_for("test"))) @app.route("/test") def test(): raise Foo() with app.test_client() as c: rv = c.get("/") location_parts = rv.headers["Location"].rpartition("/") if location_parts[0]: # For older Werkzeug that used absolute redirects. assert location_parts[0] == "http://localhost" assert location_parts[2] == "test" rv = c.get("/test") assert rv.data == b"42" flask-3.1.1/tests/test_reqctx.py000066400000000000000000000207631501065614000167030ustar00rootroot00000000000000import warnings import pytest import flask from flask.globals import request_ctx from flask.sessions import SecureCookieSessionInterface from flask.sessions import SessionInterface try: from greenlet import greenlet except ImportError: greenlet = None def test_teardown_on_pop(app): buffer = [] @app.teardown_request def end_of_request(exception): buffer.append(exception) ctx = app.test_request_context() ctx.push() assert buffer == [] ctx.pop() assert buffer == [None] def test_teardown_with_previous_exception(app): buffer = [] @app.teardown_request def end_of_request(exception): buffer.append(exception) try: raise Exception("dummy") except Exception: pass with app.test_request_context(): assert buffer == [] assert buffer == [None] def test_teardown_with_handled_exception(app): buffer = [] @app.teardown_request def end_of_request(exception): buffer.append(exception) with app.test_request_context(): assert buffer == [] try: raise Exception("dummy") except Exception: pass assert buffer == [None] def test_proper_test_request_context(app): app.config.update(SERVER_NAME="localhost.localdomain:5000") @app.route("/") def index(): return None @app.route("/", subdomain="foo") def sub(): return None with app.test_request_context("/"): assert ( flask.url_for("index", _external=True) == "http://localhost.localdomain:5000/" ) with app.test_request_context("/"): assert ( flask.url_for("sub", _external=True) == "http://foo.localhost.localdomain:5000/" ) # suppress Werkzeug 0.15 warning about name mismatch with warnings.catch_warnings(): warnings.filterwarnings( "ignore", "Current server name", UserWarning, "flask.app" ) with app.test_request_context( "/", environ_overrides={"HTTP_HOST": "localhost"} ): pass app.config.update(SERVER_NAME="localhost") with app.test_request_context("/", environ_overrides={"SERVER_NAME": "localhost"}): pass app.config.update(SERVER_NAME="localhost:80") with app.test_request_context( "/", environ_overrides={"SERVER_NAME": "localhost:80"} ): pass def test_context_binding(app): @app.route("/") def index(): return f"Hello {flask.request.args['name']}!" @app.route("/meh") def meh(): return flask.request.url with app.test_request_context("/?name=World"): assert index() == "Hello World!" with app.test_request_context("/meh"): assert meh() == "http://localhost/meh" assert not flask.request def test_context_test(app): assert not flask.request assert not flask.has_request_context() ctx = app.test_request_context() ctx.push() try: assert flask.request assert flask.has_request_context() finally: ctx.pop() def test_manual_context_binding(app): @app.route("/") def index(): return f"Hello {flask.request.args['name']}!" ctx = app.test_request_context("/?name=World") ctx.push() assert index() == "Hello World!" ctx.pop() with pytest.raises(RuntimeError): index() @pytest.mark.skipif(greenlet is None, reason="greenlet not installed") class TestGreenletContextCopying: def test_greenlet_context_copying(self, app, client): greenlets = [] @app.route("/") def index(): flask.session["fizz"] = "buzz" reqctx = request_ctx.copy() def g(): assert not flask.request assert not flask.current_app with reqctx: assert flask.request assert flask.current_app == app assert flask.request.path == "/" assert flask.request.args["foo"] == "bar" assert flask.session.get("fizz") == "buzz" assert not flask.request return 42 greenlets.append(greenlet(g)) return "Hello World!" rv = client.get("/?foo=bar") assert rv.data == b"Hello World!" result = greenlets[0].run() assert result == 42 def test_greenlet_context_copying_api(self, app, client): greenlets = [] @app.route("/") def index(): flask.session["fizz"] = "buzz" @flask.copy_current_request_context def g(): assert flask.request assert flask.current_app == app assert flask.request.path == "/" assert flask.request.args["foo"] == "bar" assert flask.session.get("fizz") == "buzz" return 42 greenlets.append(greenlet(g)) return "Hello World!" rv = client.get("/?foo=bar") assert rv.data == b"Hello World!" result = greenlets[0].run() assert result == 42 def test_session_error_pops_context(): class SessionError(Exception): pass class FailingSessionInterface(SessionInterface): def open_session(self, app, request): raise SessionError() class CustomFlask(flask.Flask): session_interface = FailingSessionInterface() app = CustomFlask(__name__) @app.route("/") def index(): # shouldn't get here AssertionError() response = app.test_client().get("/") assert response.status_code == 500 assert not flask.request assert not flask.current_app def test_session_dynamic_cookie_name(): # This session interface will use a cookie with a different name if the # requested url ends with the string "dynamic_cookie" class PathAwareSessionInterface(SecureCookieSessionInterface): def get_cookie_name(self, app): if flask.request.url.endswith("dynamic_cookie"): return "dynamic_cookie_name" else: return super().get_cookie_name(app) class CustomFlask(flask.Flask): session_interface = PathAwareSessionInterface() app = CustomFlask(__name__) app.secret_key = "secret_key" @app.route("/set", methods=["POST"]) def set(): flask.session["value"] = flask.request.form["value"] return "value set" @app.route("/get") def get(): v = flask.session.get("value", "None") return v @app.route("/set_dynamic_cookie", methods=["POST"]) def set_dynamic_cookie(): flask.session["value"] = flask.request.form["value"] return "value set" @app.route("/get_dynamic_cookie") def get_dynamic_cookie(): v = flask.session.get("value", "None") return v test_client = app.test_client() # first set the cookie in both /set urls but each with a different value assert test_client.post("/set", data={"value": "42"}).data == b"value set" assert ( test_client.post("/set_dynamic_cookie", data={"value": "616"}).data == b"value set" ) # now check that the relevant values come back - meaning that different # cookies are being used for the urls that end with "dynamic cookie" assert test_client.get("/get").data == b"42" assert test_client.get("/get_dynamic_cookie").data == b"616" def test_bad_environ_raises_bad_request(): app = flask.Flask(__name__) from flask.testing import EnvironBuilder builder = EnvironBuilder(app) environ = builder.get_environ() # use a non-printable character in the Host - this is key to this test environ["HTTP_HOST"] = "\x8a" with app.request_context(environ): response = app.full_dispatch_request() assert response.status_code == 400 def test_environ_for_valid_idna_completes(): app = flask.Flask(__name__) @app.route("/") def index(): return "Hello World!" from flask.testing import EnvironBuilder builder = EnvironBuilder(app) environ = builder.get_environ() # these characters are all IDNA-compatible environ["HTTP_HOST"] = "ąśźäüжŠßÑ.com" with app.request_context(environ): response = app.full_dispatch_request() assert response.status_code == 200 def test_normal_environ_completes(): app = flask.Flask(__name__) @app.route("/") def index(): return "Hello World!" response = app.test_client().get("/", headers={"host": "xn--on-0ia.com"}) assert response.status_code == 200 flask-3.1.1/tests/test_request.py000066400000000000000000000035561501065614000170660ustar00rootroot00000000000000from __future__ import annotations from flask import Flask from flask import Request from flask import request from flask.testing import FlaskClient def test_max_content_length(app: Flask, client: FlaskClient) -> None: app.config["MAX_CONTENT_LENGTH"] = 50 @app.post("/") def index(): request.form["myfile"] AssertionError() @app.errorhandler(413) def catcher(error): return "42" rv = client.post("/", data={"myfile": "foo" * 50}) assert rv.data == b"42" def test_limit_config(app: Flask): app.config["MAX_CONTENT_LENGTH"] = 100 app.config["MAX_FORM_MEMORY_SIZE"] = 50 app.config["MAX_FORM_PARTS"] = 3 r = Request({}) # no app context, use Werkzeug defaults assert r.max_content_length is None assert r.max_form_memory_size == 500_000 assert r.max_form_parts == 1_000 # in app context, use config with app.app_context(): assert r.max_content_length == 100 assert r.max_form_memory_size == 50 assert r.max_form_parts == 3 # regardless of app context, use override r.max_content_length = 90 r.max_form_memory_size = 30 r.max_form_parts = 4 assert r.max_content_length == 90 assert r.max_form_memory_size == 30 assert r.max_form_parts == 4 with app.app_context(): assert r.max_content_length == 90 assert r.max_form_memory_size == 30 assert r.max_form_parts == 4 def test_trusted_hosts_config(app: Flask) -> None: app.config["TRUSTED_HOSTS"] = ["example.test", ".other.test"] @app.get("/") def index() -> str: return "" client = app.test_client() r = client.get(base_url="http://example.test") assert r.status_code == 200 r = client.get(base_url="http://a.other.test") assert r.status_code == 200 r = client.get(base_url="http://bad.test") assert r.status_code == 400 flask-3.1.1/tests/test_session_interface.py000066400000000000000000000014301501065614000210660ustar00rootroot00000000000000import flask from flask.globals import request_ctx from flask.sessions import SessionInterface def test_open_session_with_endpoint(): """If request.endpoint (or other URL matching behavior) is needed while loading the session, RequestContext.match_request() can be called manually. """ class MySessionInterface(SessionInterface): def save_session(self, app, session, response): pass def open_session(self, app, request): request_ctx.match_request() assert request.endpoint is not None app = flask.Flask(__name__) app.session_interface = MySessionInterface() @app.get("/") def index(): return "Hello, World!" response = app.test_client().get("/") assert response.status_code == 200 flask-3.1.1/tests/test_signals.py000066400000000000000000000114001501065614000170210ustar00rootroot00000000000000import flask def test_template_rendered(app, client): @app.route("/") def index(): return flask.render_template("simple_template.html", whiskey=42) recorded = [] def record(sender, template, context): recorded.append((template, context)) flask.template_rendered.connect(record, app) try: client.get("/") assert len(recorded) == 1 template, context = recorded[0] assert template.name == "simple_template.html" assert context["whiskey"] == 42 finally: flask.template_rendered.disconnect(record, app) def test_before_render_template(): app = flask.Flask(__name__) @app.route("/") def index(): return flask.render_template("simple_template.html", whiskey=42) recorded = [] def record(sender, template, context): context["whiskey"] = 43 recorded.append((template, context)) flask.before_render_template.connect(record, app) try: rv = app.test_client().get("/") assert len(recorded) == 1 template, context = recorded[0] assert template.name == "simple_template.html" assert context["whiskey"] == 43 assert rv.data == b"

43

" finally: flask.before_render_template.disconnect(record, app) def test_request_signals(): app = flask.Flask(__name__) calls = [] def before_request_signal(sender): calls.append("before-signal") def after_request_signal(sender, response): assert response.data == b"stuff" calls.append("after-signal") @app.before_request def before_request_handler(): calls.append("before-handler") @app.after_request def after_request_handler(response): calls.append("after-handler") response.data = "stuff" return response @app.route("/") def index(): calls.append("handler") return "ignored anyway" flask.request_started.connect(before_request_signal, app) flask.request_finished.connect(after_request_signal, app) try: rv = app.test_client().get("/") assert rv.data == b"stuff" assert calls == [ "before-signal", "before-handler", "handler", "after-handler", "after-signal", ] finally: flask.request_started.disconnect(before_request_signal, app) flask.request_finished.disconnect(after_request_signal, app) def test_request_exception_signal(): app = flask.Flask(__name__) recorded = [] @app.route("/") def index(): raise ZeroDivisionError def record(sender, exception): recorded.append(exception) flask.got_request_exception.connect(record, app) try: assert app.test_client().get("/").status_code == 500 assert len(recorded) == 1 assert isinstance(recorded[0], ZeroDivisionError) finally: flask.got_request_exception.disconnect(record, app) def test_appcontext_signals(app, client): recorded = [] def record_push(sender, **kwargs): recorded.append("push") def record_pop(sender, **kwargs): recorded.append("pop") @app.route("/") def index(): return "Hello" flask.appcontext_pushed.connect(record_push, app) flask.appcontext_popped.connect(record_pop, app) try: rv = client.get("/") assert rv.data == b"Hello" assert recorded == ["push", "pop"] finally: flask.appcontext_pushed.disconnect(record_push, app) flask.appcontext_popped.disconnect(record_pop, app) def test_flash_signal(app): @app.route("/") def index(): flask.flash("This is a flash message", category="notice") return flask.redirect("/other") recorded = [] def record(sender, message, category): recorded.append((message, category)) flask.message_flashed.connect(record, app) try: client = app.test_client() with client.session_transaction(): client.get("/") assert len(recorded) == 1 message, category = recorded[0] assert message == "This is a flash message" assert category == "notice" finally: flask.message_flashed.disconnect(record, app) def test_appcontext_tearing_down_signal(app, client): app.testing = False recorded = [] def record_teardown(sender, exc): recorded.append(exc) @app.route("/") def index(): raise ZeroDivisionError flask.appcontext_tearing_down.connect(record_teardown, app) try: rv = client.get("/") assert rv.status_code == 500 assert len(recorded) == 1 assert isinstance(recorded[0], ZeroDivisionError) finally: flask.appcontext_tearing_down.disconnect(record_teardown, app) flask-3.1.1/tests/test_subclassing.py000066400000000000000000000007331501065614000177050ustar00rootroot00000000000000from io import StringIO import flask def test_suppressed_exception_logging(): class SuppressedFlask(flask.Flask): def log_exception(self, exc_info): pass out = StringIO() app = SuppressedFlask(__name__) @app.route("/") def index(): raise Exception("test") rv = app.test_client().get("/", errors_stream=out) assert rv.status_code == 500 assert b"Internal Server Error" in rv.data assert not out.getvalue() flask-3.1.1/tests/test_templating.py000066400000000000000000000300431501065614000175310ustar00rootroot00000000000000import logging import pytest import werkzeug.serving from jinja2 import TemplateNotFound from markupsafe import Markup import flask def test_context_processing(app, client): @app.context_processor def context_processor(): return {"injected_value": 42} @app.route("/") def index(): return flask.render_template("context_template.html", value=23) rv = client.get("/") assert rv.data == b"

23|42" def test_original_win(app, client): @app.route("/") def index(): return flask.render_template_string("{{ config }}", config=42) rv = client.get("/") assert rv.data == b"42" def test_simple_stream(app, client): @app.route("/") def index(): return flask.stream_template_string("{{ config }}", config=42) rv = client.get("/") assert rv.data == b"42" def test_request_less_rendering(app, app_ctx): app.config["WORLD_NAME"] = "Special World" @app.context_processor def context_processor(): return dict(foo=42) rv = flask.render_template_string("Hello {{ config.WORLD_NAME }} {{ foo }}") assert rv == "Hello Special World 42" def test_standard_context(app, client): @app.route("/") def index(): flask.g.foo = 23 flask.session["test"] = "aha" return flask.render_template_string( """ {{ request.args.foo }} {{ g.foo }} {{ config.DEBUG }} {{ session.test }} """ ) rv = client.get("/?foo=42") assert rv.data.split() == [b"42", b"23", b"False", b"aha"] def test_escaping(app, client): text = "

Hello World!" @app.route("/") def index(): return flask.render_template( "escaping_template.html", text=text, html=Markup(text) ) lines = client.get("/").data.splitlines() assert lines == [ b"<p>Hello World!", b"

Hello World!", b"

Hello World!", b"

Hello World!", b"<p>Hello World!", b"

Hello World!", ] def test_no_escaping(app, client): text = "

Hello World!" @app.route("/") def index(): return flask.render_template( "non_escaping_template.txt", text=text, html=Markup(text) ) lines = client.get("/").data.splitlines() assert lines == [ b"

Hello World!", b"

Hello World!", b"

Hello World!", b"

Hello World!", b"<p>Hello World!", b"

Hello World!", b"

Hello World!", b"

Hello World!", ] def test_escaping_without_template_filename(app, client, req_ctx): assert flask.render_template_string("{{ foo }}", foo="") == "<test>" assert flask.render_template("mail.txt", foo="") == " Mail" def test_macros(app, req_ctx): macro = flask.get_template_attribute("_macro.html", "hello") assert macro("World") == "Hello World!" def test_template_filter(app): @app.template_filter() def my_reverse(s): return s[::-1] assert "my_reverse" in app.jinja_env.filters.keys() assert app.jinja_env.filters["my_reverse"] == my_reverse assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" def test_add_template_filter(app): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse) assert "my_reverse" in app.jinja_env.filters.keys() assert app.jinja_env.filters["my_reverse"] == my_reverse assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" def test_template_filter_with_name(app): @app.template_filter("strrev") def my_reverse(s): return s[::-1] assert "strrev" in app.jinja_env.filters.keys() assert app.jinja_env.filters["strrev"] == my_reverse assert app.jinja_env.filters["strrev"]("abcd") == "dcba" def test_add_template_filter_with_name(app): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse, "strrev") assert "strrev" in app.jinja_env.filters.keys() assert app.jinja_env.filters["strrev"] == my_reverse assert app.jinja_env.filters["strrev"]("abcd") == "dcba" def test_template_filter_with_template(app, client): @app.template_filter() def super_reverse(s): return s[::-1] @app.route("/") def index(): return flask.render_template("template_filter.html", value="abcd") rv = client.get("/") assert rv.data == b"dcba" def test_add_template_filter_with_template(app, client): def super_reverse(s): return s[::-1] app.add_template_filter(super_reverse) @app.route("/") def index(): return flask.render_template("template_filter.html", value="abcd") rv = client.get("/") assert rv.data == b"dcba" def test_template_filter_with_name_and_template(app, client): @app.template_filter("super_reverse") def my_reverse(s): return s[::-1] @app.route("/") def index(): return flask.render_template("template_filter.html", value="abcd") rv = client.get("/") assert rv.data == b"dcba" def test_add_template_filter_with_name_and_template(app, client): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse, "super_reverse") @app.route("/") def index(): return flask.render_template("template_filter.html", value="abcd") rv = client.get("/") assert rv.data == b"dcba" def test_template_test(app): @app.template_test() def boolean(value): return isinstance(value, bool) assert "boolean" in app.jinja_env.tests.keys() assert app.jinja_env.tests["boolean"] == boolean assert app.jinja_env.tests["boolean"](False) def test_add_template_test(app): def boolean(value): return isinstance(value, bool) app.add_template_test(boolean) assert "boolean" in app.jinja_env.tests.keys() assert app.jinja_env.tests["boolean"] == boolean assert app.jinja_env.tests["boolean"](False) def test_template_test_with_name(app): @app.template_test("boolean") def is_boolean(value): return isinstance(value, bool) assert "boolean" in app.jinja_env.tests.keys() assert app.jinja_env.tests["boolean"] == is_boolean assert app.jinja_env.tests["boolean"](False) def test_add_template_test_with_name(app): def is_boolean(value): return isinstance(value, bool) app.add_template_test(is_boolean, "boolean") assert "boolean" in app.jinja_env.tests.keys() assert app.jinja_env.tests["boolean"] == is_boolean assert app.jinja_env.tests["boolean"](False) def test_template_test_with_template(app, client): @app.template_test() def boolean(value): return isinstance(value, bool) @app.route("/") def index(): return flask.render_template("template_test.html", value=False) rv = client.get("/") assert b"Success!" in rv.data def test_add_template_test_with_template(app, client): def boolean(value): return isinstance(value, bool) app.add_template_test(boolean) @app.route("/") def index(): return flask.render_template("template_test.html", value=False) rv = client.get("/") assert b"Success!" in rv.data def test_template_test_with_name_and_template(app, client): @app.template_test("boolean") def is_boolean(value): return isinstance(value, bool) @app.route("/") def index(): return flask.render_template("template_test.html", value=False) rv = client.get("/") assert b"Success!" in rv.data def test_add_template_test_with_name_and_template(app, client): def is_boolean(value): return isinstance(value, bool) app.add_template_test(is_boolean, "boolean") @app.route("/") def index(): return flask.render_template("template_test.html", value=False) rv = client.get("/") assert b"Success!" in rv.data def test_add_template_global(app, app_ctx): @app.template_global() def get_stuff(): return 42 assert "get_stuff" in app.jinja_env.globals.keys() assert app.jinja_env.globals["get_stuff"] == get_stuff assert app.jinja_env.globals["get_stuff"](), 42 rv = flask.render_template_string("{{ get_stuff() }}") assert rv == "42" def test_custom_template_loader(client): class MyFlask(flask.Flask): def create_global_jinja_loader(self): from jinja2 import DictLoader return DictLoader({"index.html": "Hello Custom World!"}) app = MyFlask(__name__) @app.route("/") def index(): return flask.render_template("index.html") c = app.test_client() rv = c.get("/") assert rv.data == b"Hello Custom World!" def test_iterable_loader(app, client): @app.context_processor def context_processor(): return {"whiskey": "Jameson"} @app.route("/") def index(): return flask.render_template( [ "no_template.xml", # should skip this one "simple_template.html", # should render this "context_template.html", ], value=23, ) rv = client.get("/") assert rv.data == b"

Jameson

" def test_templates_auto_reload(app): # debug is False, config option is None assert app.debug is False assert app.config["TEMPLATES_AUTO_RELOAD"] is None assert app.jinja_env.auto_reload is False # debug is False, config option is False app = flask.Flask(__name__) app.config["TEMPLATES_AUTO_RELOAD"] = False assert app.debug is False assert app.jinja_env.auto_reload is False # debug is False, config option is True app = flask.Flask(__name__) app.config["TEMPLATES_AUTO_RELOAD"] = True assert app.debug is False assert app.jinja_env.auto_reload is True # debug is True, config option is None app = flask.Flask(__name__) app.config["DEBUG"] = True assert app.config["TEMPLATES_AUTO_RELOAD"] is None assert app.jinja_env.auto_reload is True # debug is True, config option is False app = flask.Flask(__name__) app.config["DEBUG"] = True app.config["TEMPLATES_AUTO_RELOAD"] = False assert app.jinja_env.auto_reload is False # debug is True, config option is True app = flask.Flask(__name__) app.config["DEBUG"] = True app.config["TEMPLATES_AUTO_RELOAD"] = True assert app.jinja_env.auto_reload is True def test_templates_auto_reload_debug_run(app, monkeypatch): def run_simple_mock(*args, **kwargs): pass monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) app.run() assert not app.jinja_env.auto_reload app.run(debug=True) assert app.jinja_env.auto_reload def test_template_loader_debugging(test_apps, monkeypatch): from blueprintapp import app called = [] class _TestHandler(logging.Handler): def handle(self, record): called.append(True) text = str(record.msg) assert "1: trying loader of application 'blueprintapp'" in text assert ( "2: trying loader of blueprint 'admin' (blueprintapp.apps.admin)" ) in text assert ( "trying loader of blueprint 'frontend' (blueprintapp.apps.frontend)" ) in text assert "Error: the template could not be found" in text assert ( "looked up from an endpoint that belongs to the blueprint 'frontend'" ) in text assert "See https://flask.palletsprojects.com/blueprints/#templates" in text with app.test_client() as c: monkeypatch.setitem(app.config, "EXPLAIN_TEMPLATE_LOADING", True) monkeypatch.setattr( logging.getLogger("blueprintapp"), "handlers", [_TestHandler()] ) with pytest.raises(TemplateNotFound) as excinfo: c.get("/missing") assert "missing_template.html" in str(excinfo.value) assert len(called) == 1 def test_custom_jinja_env(): class CustomEnvironment(flask.templating.Environment): pass class CustomFlask(flask.Flask): jinja_environment = CustomEnvironment app = CustomFlask(__name__) assert isinstance(app.jinja_env, CustomEnvironment) flask-3.1.1/tests/test_testing.py000066400000000000000000000245761501065614000170600ustar00rootroot00000000000000import importlib.metadata import click import pytest import flask from flask import appcontext_popped from flask.cli import ScriptInfo from flask.globals import _cv_request from flask.json import jsonify from flask.testing import EnvironBuilder from flask.testing import FlaskCliRunner def test_environ_defaults_from_config(app, client): app.config["SERVER_NAME"] = "example.com:1234" app.config["APPLICATION_ROOT"] = "/foo" @app.route("/") def index(): return flask.request.url ctx = app.test_request_context() assert ctx.request.url == "http://example.com:1234/foo/" rv = client.get("/") assert rv.data == b"http://example.com:1234/foo/" def test_environ_defaults(app, client, app_ctx, req_ctx): @app.route("/") def index(): return flask.request.url ctx = app.test_request_context() assert ctx.request.url == "http://localhost/" with client: rv = client.get("/") assert rv.data == b"http://localhost/" def test_environ_base_default(app, client): @app.route("/") def index(): flask.g.remote_addr = flask.request.remote_addr flask.g.user_agent = flask.request.user_agent.string return "" with client: client.get("/") assert flask.g.remote_addr == "127.0.0.1" assert flask.g.user_agent == ( f"Werkzeug/{importlib.metadata.version('werkzeug')}" ) def test_environ_base_modified(app, client): @app.route("/") def index(): flask.g.remote_addr = flask.request.remote_addr flask.g.user_agent = flask.request.user_agent.string return "" client.environ_base["REMOTE_ADDR"] = "192.168.0.22" client.environ_base["HTTP_USER_AGENT"] = "Foo" with client: client.get("/") assert flask.g.remote_addr == "192.168.0.22" assert flask.g.user_agent == "Foo" def test_client_open_environ(app, client, request): @app.route("/index") def index(): return flask.request.remote_addr builder = EnvironBuilder(app, path="/index", method="GET") request.addfinalizer(builder.close) rv = client.open(builder) assert rv.data == b"127.0.0.1" environ = builder.get_environ() client.environ_base["REMOTE_ADDR"] = "127.0.0.2" rv = client.open(environ) assert rv.data == b"127.0.0.2" def test_specify_url_scheme(app, client): @app.route("/") def index(): return flask.request.url ctx = app.test_request_context(url_scheme="https") assert ctx.request.url == "https://localhost/" rv = client.get("/", url_scheme="https") assert rv.data == b"https://localhost/" def test_path_is_url(app): eb = EnvironBuilder(app, "https://example.com/") assert eb.url_scheme == "https" assert eb.host == "example.com" assert eb.script_root == "" assert eb.path == "/" def test_environbuilder_json_dumps(app): """EnvironBuilder.json_dumps() takes settings from the app.""" app.json.ensure_ascii = False eb = EnvironBuilder(app, json="\u20ac") assert eb.input_stream.read().decode("utf8") == '"\u20ac"' def test_blueprint_with_subdomain(): app = flask.Flask(__name__, subdomain_matching=True) app.config["SERVER_NAME"] = "example.com:1234" app.config["APPLICATION_ROOT"] = "/foo" client = app.test_client() bp = flask.Blueprint("company", __name__, subdomain="xxx") @bp.route("/") def index(): return flask.request.url app.register_blueprint(bp) ctx = app.test_request_context("/", subdomain="xxx") assert ctx.request.url == "http://xxx.example.com:1234/foo/" with ctx: assert ctx.request.blueprint == bp.name rv = client.get("/", subdomain="xxx") assert rv.data == b"http://xxx.example.com:1234/foo/" def test_redirect_keep_session(app, client, app_ctx): @app.route("/", methods=["GET", "POST"]) def index(): if flask.request.method == "POST": return flask.redirect("/getsession") flask.session["data"] = "foo" return "index" @app.route("/getsession") def get_session(): return flask.session.get("data", "") with client: rv = client.get("/getsession") assert rv.data == b"" rv = client.get("/") assert rv.data == b"index" assert flask.session.get("data") == "foo" rv = client.post("/", data={}, follow_redirects=True) assert rv.data == b"foo" assert flask.session.get("data") == "foo" rv = client.get("/getsession") assert rv.data == b"foo" def test_session_transactions(app, client): @app.route("/") def index(): return str(flask.session["foo"]) with client: with client.session_transaction() as sess: assert len(sess) == 0 sess["foo"] = [42] assert len(sess) == 1 rv = client.get("/") assert rv.data == b"[42]" with client.session_transaction() as sess: assert len(sess) == 1 assert sess["foo"] == [42] def test_session_transactions_no_null_sessions(): app = flask.Flask(__name__) with app.test_client() as c: with pytest.raises(RuntimeError) as e: with c.session_transaction(): pass assert "Session backend did not open a session" in str(e.value) def test_session_transactions_keep_context(app, client, req_ctx): client.get("/") req = flask.request._get_current_object() assert req is not None with client.session_transaction(): assert req is flask.request._get_current_object() def test_session_transaction_needs_cookies(app): c = app.test_client(use_cookies=False) with pytest.raises(TypeError, match="Cookies are disabled."): with c.session_transaction(): pass def test_test_client_context_binding(app, client): app.testing = False @app.route("/") def index(): flask.g.value = 42 return "Hello World!" @app.route("/other") def other(): raise ZeroDivisionError with client: resp = client.get("/") assert flask.g.value == 42 assert resp.data == b"Hello World!" assert resp.status_code == 200 with client: resp = client.get("/other") assert not hasattr(flask.g, "value") assert b"Internal Server Error" in resp.data assert resp.status_code == 500 flask.g.value = 23 with pytest.raises(RuntimeError): flask.g.value # noqa: B018 def test_reuse_client(client): c = client with c: assert client.get("/").status_code == 404 with c: assert client.get("/").status_code == 404 def test_full_url_request(app, client): @app.route("/action", methods=["POST"]) def action(): return "x" with client: rv = client.post("http://domain.com/action?vodka=42", data={"gin": 43}) assert rv.status_code == 200 assert "gin" in flask.request.form assert "vodka" in flask.request.args def test_json_request_and_response(app, client): @app.route("/echo", methods=["POST"]) def echo(): return jsonify(flask.request.get_json()) with client: json_data = {"drink": {"gin": 1, "tonic": True}, "price": 10} rv = client.post("/echo", json=json_data) # Request should be in JSON assert flask.request.is_json assert flask.request.get_json() == json_data # Response should be in JSON assert rv.status_code == 200 assert rv.is_json assert rv.get_json() == json_data def test_client_json_no_app_context(app, client): @app.route("/hello", methods=["POST"]) def hello(): return f"Hello, {flask.request.json['name']}!" class Namespace: count = 0 def add(self, app): self.count += 1 ns = Namespace() with appcontext_popped.connected_to(ns.add, app): rv = client.post("/hello", json={"name": "Flask"}) assert rv.get_data(as_text=True) == "Hello, Flask!" assert ns.count == 1 def test_subdomain(): app = flask.Flask(__name__, subdomain_matching=True) app.config["SERVER_NAME"] = "example.com" client = app.test_client() @app.route("/", subdomain="") def view(company_id): return company_id with app.test_request_context(): url = flask.url_for("view", company_id="xxx") with client: response = client.get(url) assert 200 == response.status_code assert b"xxx" == response.data def test_nosubdomain(app, client): app.config["SERVER_NAME"] = "example.com" @app.route("/") def view(company_id): return company_id with app.test_request_context(): url = flask.url_for("view", company_id="xxx") with client: response = client.get(url) assert 200 == response.status_code assert b"xxx" == response.data def test_cli_runner_class(app): runner = app.test_cli_runner() assert isinstance(runner, FlaskCliRunner) class SubRunner(FlaskCliRunner): pass app.test_cli_runner_class = SubRunner runner = app.test_cli_runner() assert isinstance(runner, SubRunner) def test_cli_invoke(app): @app.cli.command("hello") def hello_command(): click.echo("Hello, World!") runner = app.test_cli_runner() # invoke with command name result = runner.invoke(args=["hello"]) assert "Hello" in result.output # invoke with command object result = runner.invoke(hello_command) assert "Hello" in result.output def test_cli_custom_obj(app): class NS: called = False def create_app(): NS.called = True return app @app.cli.command("hello") def hello_command(): click.echo("Hello, World!") script_info = ScriptInfo(create_app=create_app) runner = app.test_cli_runner() runner.invoke(hello_command, obj=script_info) assert NS.called def test_client_pop_all_preserved(app, req_ctx, client): @app.route("/") def index(): # stream_with_context pushes a third context, preserved by response return flask.stream_with_context("hello") # req_ctx fixture pushed an initial context with client: # request pushes a second request context, preserved by client rv = client.get("/") # close the response, releasing the context held by stream_with_context rv.close() # only req_ctx fixture should still be pushed assert _cv_request.get(None) is req_ctx flask-3.1.1/tests/test_user_error_handler.py000066400000000000000000000203641501065614000212560ustar00rootroot00000000000000import pytest from werkzeug.exceptions import Forbidden from werkzeug.exceptions import HTTPException from werkzeug.exceptions import InternalServerError from werkzeug.exceptions import NotFound import flask def test_error_handler_no_match(app, client): class CustomException(Exception): pass @app.errorhandler(CustomException) def custom_exception_handler(e): assert isinstance(e, CustomException) return "custom" with pytest.raises(TypeError) as exc_info: app.register_error_handler(CustomException(), None) assert "CustomException() is an instance, not a class." in str(exc_info.value) with pytest.raises(ValueError) as exc_info: app.register_error_handler(list, None) assert "'list' is not a subclass of Exception." in str(exc_info.value) @app.errorhandler(500) def handle_500(e): assert isinstance(e, InternalServerError) if e.original_exception is not None: return f"wrapped {type(e.original_exception).__name__}" return "direct" with pytest.raises(ValueError) as exc_info: app.register_error_handler(999, None) assert "Use a subclass of HTTPException" in str(exc_info.value) @app.route("/custom") def custom_test(): raise CustomException() @app.route("/keyerror") def key_error(): raise KeyError() @app.route("/abort") def do_abort(): flask.abort(500) app.testing = False assert client.get("/custom").data == b"custom" assert client.get("/keyerror").data == b"wrapped KeyError" assert client.get("/abort").data == b"direct" def test_error_handler_subclass(app): class ParentException(Exception): pass class ChildExceptionUnregistered(ParentException): pass class ChildExceptionRegistered(ParentException): pass @app.errorhandler(ParentException) def parent_exception_handler(e): assert isinstance(e, ParentException) return "parent" @app.errorhandler(ChildExceptionRegistered) def child_exception_handler(e): assert isinstance(e, ChildExceptionRegistered) return "child-registered" @app.route("/parent") def parent_test(): raise ParentException() @app.route("/child-unregistered") def unregistered_test(): raise ChildExceptionUnregistered() @app.route("/child-registered") def registered_test(): raise ChildExceptionRegistered() c = app.test_client() assert c.get("/parent").data == b"parent" assert c.get("/child-unregistered").data == b"parent" assert c.get("/child-registered").data == b"child-registered" def test_error_handler_http_subclass(app): class ForbiddenSubclassRegistered(Forbidden): pass class ForbiddenSubclassUnregistered(Forbidden): pass @app.errorhandler(403) def code_exception_handler(e): assert isinstance(e, Forbidden) return "forbidden" @app.errorhandler(ForbiddenSubclassRegistered) def subclass_exception_handler(e): assert isinstance(e, ForbiddenSubclassRegistered) return "forbidden-registered" @app.route("/forbidden") def forbidden_test(): raise Forbidden() @app.route("/forbidden-registered") def registered_test(): raise ForbiddenSubclassRegistered() @app.route("/forbidden-unregistered") def unregistered_test(): raise ForbiddenSubclassUnregistered() c = app.test_client() assert c.get("/forbidden").data == b"forbidden" assert c.get("/forbidden-unregistered").data == b"forbidden" assert c.get("/forbidden-registered").data == b"forbidden-registered" def test_error_handler_blueprint(app): bp = flask.Blueprint("bp", __name__) @bp.errorhandler(500) def bp_exception_handler(e): return "bp-error" @bp.route("/error") def bp_test(): raise InternalServerError() @app.errorhandler(500) def app_exception_handler(e): return "app-error" @app.route("/error") def app_test(): raise InternalServerError() app.register_blueprint(bp, url_prefix="/bp") c = app.test_client() assert c.get("/error").data == b"app-error" assert c.get("/bp/error").data == b"bp-error" def test_default_error_handler(): bp = flask.Blueprint("bp", __name__) @bp.errorhandler(HTTPException) def bp_exception_handler(e): assert isinstance(e, HTTPException) assert isinstance(e, NotFound) return "bp-default" @bp.errorhandler(Forbidden) def bp_forbidden_handler(e): assert isinstance(e, Forbidden) return "bp-forbidden" @bp.route("/undefined") def bp_registered_test(): raise NotFound() @bp.route("/forbidden") def bp_forbidden_test(): raise Forbidden() app = flask.Flask(__name__) @app.errorhandler(HTTPException) def catchall_exception_handler(e): assert isinstance(e, HTTPException) assert isinstance(e, NotFound) return "default" @app.errorhandler(Forbidden) def catchall_forbidden_handler(e): assert isinstance(e, Forbidden) return "forbidden" @app.route("/forbidden") def forbidden(): raise Forbidden() @app.route("/slash/") def slash(): return "slash" app.register_blueprint(bp, url_prefix="/bp") c = app.test_client() assert c.get("/bp/undefined").data == b"bp-default" assert c.get("/bp/forbidden").data == b"bp-forbidden" assert c.get("/undefined").data == b"default" assert c.get("/forbidden").data == b"forbidden" # Don't handle RequestRedirect raised when adding slash. assert c.get("/slash", follow_redirects=True).data == b"slash" class TestGenericHandlers: """Test how very generic handlers are dispatched to.""" class Custom(Exception): pass @pytest.fixture() def app(self, app): @app.route("/custom") def do_custom(): raise self.Custom() @app.route("/error") def do_error(): raise KeyError() @app.route("/abort") def do_abort(): flask.abort(500) @app.route("/raise") def do_raise(): raise InternalServerError() app.config["PROPAGATE_EXCEPTIONS"] = False return app def report_error(self, e): original = getattr(e, "original_exception", None) if original is not None: return f"wrapped {type(original).__name__}" return f"direct {type(e).__name__}" @pytest.mark.parametrize("to_handle", (InternalServerError, 500)) def test_handle_class_or_code(self, app, client, to_handle): """``InternalServerError`` and ``500`` are aliases, they should have the same behavior. Both should only receive ``InternalServerError``, which might wrap another error. """ @app.errorhandler(to_handle) def handle_500(e): assert isinstance(e, InternalServerError) return self.report_error(e) assert client.get("/custom").data == b"wrapped Custom" assert client.get("/error").data == b"wrapped KeyError" assert client.get("/abort").data == b"direct InternalServerError" assert client.get("/raise").data == b"direct InternalServerError" def test_handle_generic_http(self, app, client): """``HTTPException`` should only receive ``HTTPException`` subclasses. It will receive ``404`` routing exceptions. """ @app.errorhandler(HTTPException) def handle_http(e): assert isinstance(e, HTTPException) return str(e.code) assert client.get("/error").data == b"500" assert client.get("/abort").data == b"500" assert client.get("/not-found").data == b"404" def test_handle_generic(self, app, client): """Generic ``Exception`` will handle all exceptions directly, including ``HTTPExceptions``. """ @app.errorhandler(Exception) def handle_exception(e): return self.report_error(e) assert client.get("/custom").data == b"direct Custom" assert client.get("/error").data == b"direct KeyError" assert client.get("/abort").data == b"direct InternalServerError" assert client.get("/not-found").data == b"direct NotFound" flask-3.1.1/tests/test_views.py000066400000000000000000000151141501065614000165240ustar00rootroot00000000000000import pytest from werkzeug.http import parse_set_header import flask.views def common_test(app): c = app.test_client() assert c.get("/").data == b"GET" assert c.post("/").data == b"POST" assert c.put("/").status_code == 405 meths = parse_set_header(c.open("/", method="OPTIONS").headers["Allow"]) assert sorted(meths) == ["GET", "HEAD", "OPTIONS", "POST"] def test_basic_view(app): class Index(flask.views.View): methods = ["GET", "POST"] def dispatch_request(self): return flask.request.method app.add_url_rule("/", view_func=Index.as_view("index")) common_test(app) def test_method_based_view(app): class Index(flask.views.MethodView): def get(self): return "GET" def post(self): return "POST" app.add_url_rule("/", view_func=Index.as_view("index")) common_test(app) def test_view_patching(app): class Index(flask.views.MethodView): def get(self): raise ZeroDivisionError def post(self): raise ZeroDivisionError class Other(Index): def get(self): return "GET" def post(self): return "POST" view = Index.as_view("index") view.view_class = Other app.add_url_rule("/", view_func=view) common_test(app) def test_view_inheritance(app, client): class Index(flask.views.MethodView): def get(self): return "GET" def post(self): return "POST" class BetterIndex(Index): def delete(self): return "DELETE" app.add_url_rule("/", view_func=BetterIndex.as_view("index")) meths = parse_set_header(client.open("/", method="OPTIONS").headers["Allow"]) assert sorted(meths) == ["DELETE", "GET", "HEAD", "OPTIONS", "POST"] def test_view_decorators(app, client): def add_x_parachute(f): def new_function(*args, **kwargs): resp = flask.make_response(f(*args, **kwargs)) resp.headers["X-Parachute"] = "awesome" return resp return new_function class Index(flask.views.View): decorators = [add_x_parachute] def dispatch_request(self): return "Awesome" app.add_url_rule("/", view_func=Index.as_view("index")) rv = client.get("/") assert rv.headers["X-Parachute"] == "awesome" assert rv.data == b"Awesome" def test_view_provide_automatic_options_attr(): app = flask.Flask(__name__) class Index1(flask.views.View): provide_automatic_options = False def dispatch_request(self): return "Hello World!" app.add_url_rule("/", view_func=Index1.as_view("index")) c = app.test_client() rv = c.open("/", method="OPTIONS") assert rv.status_code == 405 app = flask.Flask(__name__) class Index2(flask.views.View): methods = ["OPTIONS"] provide_automatic_options = True def dispatch_request(self): return "Hello World!" app.add_url_rule("/", view_func=Index2.as_view("index")) c = app.test_client() rv = c.open("/", method="OPTIONS") assert sorted(rv.allow) == ["OPTIONS"] app = flask.Flask(__name__) class Index3(flask.views.View): def dispatch_request(self): return "Hello World!" app.add_url_rule("/", view_func=Index3.as_view("index")) c = app.test_client() rv = c.open("/", method="OPTIONS") assert "OPTIONS" in rv.allow def test_implicit_head(app, client): class Index(flask.views.MethodView): def get(self): return flask.Response("Blub", headers={"X-Method": flask.request.method}) app.add_url_rule("/", view_func=Index.as_view("index")) rv = client.get("/") assert rv.data == b"Blub" assert rv.headers["X-Method"] == "GET" rv = client.head("/") assert rv.data == b"" assert rv.headers["X-Method"] == "HEAD" def test_explicit_head(app, client): class Index(flask.views.MethodView): def get(self): return "GET" def head(self): return flask.Response("", headers={"X-Method": "HEAD"}) app.add_url_rule("/", view_func=Index.as_view("index")) rv = client.get("/") assert rv.data == b"GET" rv = client.head("/") assert rv.data == b"" assert rv.headers["X-Method"] == "HEAD" def test_endpoint_override(app): app.debug = True class Index(flask.views.View): methods = ["GET", "POST"] def dispatch_request(self): return flask.request.method app.add_url_rule("/", view_func=Index.as_view("index")) with pytest.raises(AssertionError): app.add_url_rule("/", view_func=Index.as_view("index")) # But these tests should still pass. We just log a warning. common_test(app) def test_methods_var_inheritance(app, client): class BaseView(flask.views.MethodView): methods = ["GET", "PROPFIND"] class ChildView(BaseView): def get(self): return "GET" def propfind(self): return "PROPFIND" app.add_url_rule("/", view_func=ChildView.as_view("index")) assert client.get("/").data == b"GET" assert client.open("/", method="PROPFIND").data == b"PROPFIND" assert ChildView.methods == {"PROPFIND", "GET"} def test_multiple_inheritance(app, client): class GetView(flask.views.MethodView): def get(self): return "GET" class DeleteView(flask.views.MethodView): def delete(self): return "DELETE" class GetDeleteView(GetView, DeleteView): pass app.add_url_rule("/", view_func=GetDeleteView.as_view("index")) assert client.get("/").data == b"GET" assert client.delete("/").data == b"DELETE" assert sorted(GetDeleteView.methods) == ["DELETE", "GET"] def test_remove_method_from_parent(app, client): class GetView(flask.views.MethodView): def get(self): return "GET" class OtherView(flask.views.MethodView): def post(self): return "POST" class View(GetView, OtherView): methods = ["GET"] app.add_url_rule("/", view_func=View.as_view("index")) assert client.get("/").data == b"GET" assert client.post("/").status_code == 405 assert sorted(View.methods) == ["GET"] def test_init_once(app, client): n = 0 class CountInit(flask.views.View): init_every_request = False def __init__(self): nonlocal n n += 1 def dispatch_request(self): return str(n) app.add_url_rule("/", view_func=CountInit.as_view("index")) assert client.get("/").data == b"1" assert client.get("/").data == b"1" flask-3.1.1/tests/type_check/000077500000000000000000000000001501065614000160725ustar00rootroot00000000000000flask-3.1.1/tests/type_check/typing_app_decorators.py000066400000000000000000000011151501065614000230410ustar00rootroot00000000000000from __future__ import annotations from flask import Flask from flask import Response app = Flask(__name__) @app.after_request def after_sync(response: Response) -> Response: return Response() @app.after_request async def after_async(response: Response) -> Response: return Response() @app.before_request def before_sync() -> None: ... @app.before_request async def before_async() -> None: ... @app.teardown_appcontext def teardown_sync(exc: BaseException | None) -> None: ... @app.teardown_appcontext async def teardown_async(exc: BaseException | None) -> None: ... flask-3.1.1/tests/type_check/typing_error_handler.py000066400000000000000000000012071501065614000226640ustar00rootroot00000000000000from __future__ import annotations from http import HTTPStatus from werkzeug.exceptions import BadRequest from werkzeug.exceptions import NotFound from flask import Flask app = Flask(__name__) @app.errorhandler(400) @app.errorhandler(HTTPStatus.BAD_REQUEST) @app.errorhandler(BadRequest) def handle_400(e: BadRequest) -> str: return "" @app.errorhandler(ValueError) def handle_custom(e: ValueError) -> str: return "" @app.errorhandler(ValueError) def handle_accept_base(e: Exception) -> str: return "" @app.errorhandler(BadRequest) @app.errorhandler(404) def handle_multiple(e: BadRequest | NotFound) -> str: return "" flask-3.1.1/tests/type_check/typing_route.py000066400000000000000000000047441501065614000212050ustar00rootroot00000000000000from __future__ import annotations import typing as t from http import HTTPStatus from flask import Flask from flask import jsonify from flask import stream_template from flask.templating import render_template from flask.views import View from flask.wrappers import Response app = Flask(__name__) @app.route("/str") def hello_str() -> str: return "

Hello, World!

" @app.route("/bytes") def hello_bytes() -> bytes: return b"

Hello, World!

" @app.route("/json") def hello_json() -> Response: return jsonify("Hello, World!") @app.route("/json/dict") def hello_json_dict() -> dict[str, t.Any]: return {"response": "Hello, World!"} @app.route("/json/dict") def hello_json_list() -> list[t.Any]: return [{"message": "Hello"}, {"message": "World"}] class StatusJSON(t.TypedDict): status: str @app.route("/typed-dict") def typed_dict() -> StatusJSON: return {"status": "ok"} @app.route("/generator") def hello_generator() -> t.Generator[str, None, None]: def show() -> t.Generator[str, None, None]: for x in range(100): yield f"data:{x}\n\n" return show() @app.route("/generator-expression") def hello_generator_expression() -> t.Iterator[bytes]: return (f"data:{x}\n\n".encode() for x in range(100)) @app.route("/iterator") def hello_iterator() -> t.Iterator[str]: return iter([f"data:{x}\n\n" for x in range(100)]) @app.route("/status") @app.route("/status/") def tuple_status(code: int = 200) -> tuple[str, int]: return "hello", code @app.route("/status-enum") def tuple_status_enum() -> tuple[str, int]: return "hello", HTTPStatus.OK @app.route("/headers") def tuple_headers() -> tuple[str, dict[str, str]]: return "Hello, World!", {"Content-Type": "text/plain"} @app.route("/template") @app.route("/template/") def return_template(name: str | None = None) -> str: return render_template("index.html", name=name) @app.route("/template") def return_template_stream() -> t.Iterator[str]: return stream_template("index.html", name="Hello") @app.route("/async") async def async_route() -> str: return "Hello" class RenderTemplateView(View): def __init__(self: RenderTemplateView, template_name: str) -> None: self.template_name = template_name def dispatch_request(self: RenderTemplateView) -> str: return render_template(self.template_name) app.add_url_rule( "/about", view_func=RenderTemplateView.as_view("about_page", template_name="about.html"), ) flask-3.1.1/uv.lock000066400000000000000000007447121501065614000141350ustar00rootroot00000000000000version = 1 revision = 2 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", "python_full_version == '3.10.*'", "python_full_version < '3.10'", ] [[package]] name = "alabaster" version = "0.7.16" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] name = "anyio" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] [[package]] name = "asgiref" version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] [[package]] name = "blinker" version = "1.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] [[package]] name = "cachetools" version = "5.5.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, ] [[package]] name = "certifi" version = "2025.4.26" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, ] [[package]] name = "cffi" version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220, upload-time = "2024-09-04T20:45:01.577Z" }, { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605, upload-time = "2024-09-04T20:45:03.837Z" }, { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload-time = "2024-09-04T20:45:17.284Z" }, { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820, upload-time = "2024-09-04T20:45:18.762Z" }, { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290, upload-time = "2024-09-04T20:45:20.226Z" }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] [[package]] name = "chardet" version = "5.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[package]] name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "click" version = "8.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "cryptography" version = "44.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096, upload-time = "2025-05-02T19:36:04.667Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281, upload-time = "2025-05-02T19:34:50.665Z" }, { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305, upload-time = "2025-05-02T19:34:53.042Z" }, { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040, upload-time = "2025-05-02T19:34:54.675Z" }, { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411, upload-time = "2025-05-02T19:34:56.61Z" }, { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263, upload-time = "2025-05-02T19:34:58.591Z" }, { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198, upload-time = "2025-05-02T19:35:00.988Z" }, { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502, upload-time = "2025-05-02T19:35:03.091Z" }, { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173, upload-time = "2025-05-02T19:35:05.018Z" }, { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713, upload-time = "2025-05-02T19:35:07.187Z" }, { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064, upload-time = "2025-05-02T19:35:08.879Z" }, { url = "https://files.pythonhosted.org/packages/10/a8/8c540a421b44fd267a7d58a1fd5f072a552d72204a3f08194f98889de76d/cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d", size = 2773887, upload-time = "2025-05-02T19:35:10.41Z" }, { url = "https://files.pythonhosted.org/packages/b9/0d/c4b1657c39ead18d76bbd122da86bd95bdc4095413460d09544000a17d56/cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d", size = 3209737, upload-time = "2025-05-02T19:35:12.12Z" }, { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501, upload-time = "2025-05-02T19:35:13.775Z" }, { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307, upload-time = "2025-05-02T19:35:15.917Z" }, { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876, upload-time = "2025-05-02T19:35:18.138Z" }, { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127, upload-time = "2025-05-02T19:35:19.864Z" }, { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164, upload-time = "2025-05-02T19:35:21.449Z" }, { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081, upload-time = "2025-05-02T19:35:23.187Z" }, { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716, upload-time = "2025-05-02T19:35:25.426Z" }, { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398, upload-time = "2025-05-02T19:35:27.678Z" }, { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900, upload-time = "2025-05-02T19:35:29.312Z" }, { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067, upload-time = "2025-05-02T19:35:31.547Z" }, { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467, upload-time = "2025-05-02T19:35:33.805Z" }, { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375, upload-time = "2025-05-02T19:35:35.369Z" }, { url = "https://files.pythonhosted.org/packages/7f/10/abcf7418536df1eaba70e2cfc5c8a0ab07aa7aa02a5cbc6a78b9d8b4f121/cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d", size = 3393192, upload-time = "2025-05-02T19:35:37.468Z" }, { url = "https://files.pythonhosted.org/packages/06/59/ecb3ef380f5891978f92a7f9120e2852b1df6f0a849c277b8ea45b865db2/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8", size = 3898419, upload-time = "2025-05-02T19:35:39.065Z" }, { url = "https://files.pythonhosted.org/packages/bb/d0/35e2313dbb38cf793aa242182ad5bc5ef5c8fd4e5dbdc380b936c7d51169/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4", size = 4117892, upload-time = "2025-05-02T19:35:40.839Z" }, { url = "https://files.pythonhosted.org/packages/dc/c8/31fb6e33b56c2c2100d76de3fd820afaa9d4d0b6aea1ccaf9aaf35dc7ce3/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff", size = 3900855, upload-time = "2025-05-02T19:35:42.599Z" }, { url = "https://files.pythonhosted.org/packages/43/2a/08cc2ec19e77f2a3cfa2337b429676406d4bb78ddd130a05c458e7b91d73/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06", size = 4117619, upload-time = "2025-05-02T19:35:44.774Z" }, { url = "https://files.pythonhosted.org/packages/02/68/fc3d3f84022a75f2ac4b1a1c0e5d6a0c2ea259e14cd4aae3e0e68e56483c/cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9", size = 3136570, upload-time = "2025-05-02T19:35:46.94Z" }, { url = "https://files.pythonhosted.org/packages/8d/4b/c11ad0b6c061902de5223892d680e89c06c7c4d606305eb8de56c5427ae6/cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375", size = 3390230, upload-time = "2025-05-02T19:35:49.062Z" }, { url = "https://files.pythonhosted.org/packages/58/11/0a6bf45d53b9b2290ea3cec30e78b78e6ca29dc101e2e296872a0ffe1335/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647", size = 3895216, upload-time = "2025-05-02T19:35:51.351Z" }, { url = "https://files.pythonhosted.org/packages/0a/27/b28cdeb7270e957f0077a2c2bfad1b38f72f1f6d699679f97b816ca33642/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259", size = 4115044, upload-time = "2025-05-02T19:35:53.044Z" }, { url = "https://files.pythonhosted.org/packages/35/b0/ec4082d3793f03cb248881fecefc26015813199b88f33e3e990a43f79835/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff", size = 3898034, upload-time = "2025-05-02T19:35:54.72Z" }, { url = "https://files.pythonhosted.org/packages/0b/7f/adf62e0b8e8d04d50c9a91282a57628c00c54d4ae75e2b02a223bd1f2613/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5", size = 4114449, upload-time = "2025-05-02T19:35:57.139Z" }, { url = "https://files.pythonhosted.org/packages/87/62/d69eb4a8ee231f4bf733a92caf9da13f1c81a44e874b1d4080c25ecbb723/cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c", size = 3134369, upload-time = "2025-05-02T19:35:58.907Z" }, ] [[package]] name = "distlib" version = "0.3.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, ] [[package]] name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] [[package]] name = "flask" version = "3.1.1" source = { editable = "." } dependencies = [ { name = "blinker" }, { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, { name = "werkzeug" }, ] [package.optional-dependencies] async = [ { name = "asgiref" }, ] dotenv = [ { name = "python-dotenv" }, ] [package.dev-dependencies] dev = [ { name = "ruff" }, { name = "tox" }, { name = "tox-uv" }, ] docs = [ { name = "pallets-sphinx-themes" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-tabs" }, { name = "sphinxcontrib-log-cabinet" }, ] docs-auto = [ { name = "sphinx-autobuild" }, ] gha-update = [ { name = "gha-update", marker = "python_full_version >= '3.12'" }, ] pre-commit = [ { name = "pre-commit" }, { name = "pre-commit-uv" }, ] tests = [ { name = "asgiref" }, { name = "greenlet", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "python-dotenv" }, ] typing = [ { name = "asgiref" }, { name = "cryptography" }, { name = "mypy" }, { name = "pyright" }, { name = "pytest" }, { name = "python-dotenv" }, { name = "types-contextvars" }, { name = "types-dataclasses" }, ] [package.metadata] requires-dist = [ { name = "asgiref", marker = "extra == 'async'", specifier = ">=3.2" }, { name = "blinker", specifier = ">=1.9.0" }, { name = "click", specifier = ">=8.1.3" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=3.6.0" }, { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "jinja2", specifier = ">=3.1.2" }, { name = "markupsafe", specifier = ">=2.1.1" }, { name = "python-dotenv", marker = "extra == 'dotenv'" }, { name = "werkzeug", specifier = ">=3.1.0" }, ] provides-extras = ["async", "dotenv"] [package.metadata.requires-dev] dev = [ { name = "ruff" }, { name = "tox" }, { name = "tox-uv" }, ] docs = [ { name = "pallets-sphinx-themes" }, { name = "sphinx" }, { name = "sphinx-tabs" }, { name = "sphinxcontrib-log-cabinet" }, ] docs-auto = [{ name = "sphinx-autobuild" }] gha-update = [{ name = "gha-update", marker = "python_full_version >= '3.12'" }] pre-commit = [ { name = "pre-commit" }, { name = "pre-commit-uv" }, ] tests = [ { name = "asgiref" }, { name = "greenlet", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "python-dotenv" }, ] typing = [ { name = "asgiref" }, { name = "cryptography" }, { name = "mypy" }, { name = "pyright" }, { name = "pytest" }, { name = "python-dotenv" }, { name = "types-contextvars" }, { name = "types-dataclasses" }, ] [[package]] name = "gha-update" version = "0.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "httpx", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/ac/f1b6699a529bd298a777199861a8232590bb612eac92e15bf1033134f123/gha_update-0.1.0.tar.gz", hash = "sha256:8c0f55ed7bdc11fb061d67984814fd642bd3a1872028e34c15c913cd59202d53", size = 3345, upload-time = "2024-08-23T20:58:42.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/06/832338d1b5f82f17e1f4985146d81050cd2709ea54dab3f15b343ad227cc/gha_update-0.1.0-py3-none-any.whl", hash = "sha256:ec110088749eed66b5f55cc7f15f41a6af037446a5ac6a435b0768a57d52e087", size = 4513, upload-time = "2024-08-23T20:58:41.364Z" }, ] [[package]] name = "greenlet" version = "3.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/34/c1/a82edae11d46c0d83481aacaa1e578fea21d94a1ef400afd734d47ad95ad/greenlet-3.2.2.tar.gz", hash = "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485", size = 185797, upload-time = "2025-05-09T19:47:35.066Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/05/66/910217271189cc3f32f670040235f4bf026ded8ca07270667d69c06e7324/greenlet-3.2.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c49e9f7c6f625507ed83a7485366b46cbe325717c60837f7244fc99ba16ba9d6", size = 267395, upload-time = "2025-05-09T14:50:45.357Z" }, { url = "https://files.pythonhosted.org/packages/a8/36/8d812402ca21017c82880f399309afadb78a0aa300a9b45d741e4df5d954/greenlet-3.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3cc1a3ed00ecfea8932477f729a9f616ad7347a5e55d50929efa50a86cb7be7", size = 625742, upload-time = "2025-05-09T15:23:58.293Z" }, { url = "https://files.pythonhosted.org/packages/7b/77/66d7b59dfb7cc1102b2f880bc61cb165ee8998c9ec13c96606ba37e54c77/greenlet-3.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9896249fbef2c615853b890ee854f22c671560226c9221cfd27c995db97e5c", size = 637014, upload-time = "2025-05-09T15:24:47.025Z" }, { url = "https://files.pythonhosted.org/packages/36/a7/ff0d408f8086a0d9a5aac47fa1b33a040a9fca89bd5a3f7b54d1cd6e2793/greenlet-3.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7409796591d879425997a518138889d8d17e63ada7c99edc0d7a1c22007d4907", size = 632874, upload-time = "2025-05-09T15:29:20.014Z" }, { url = "https://files.pythonhosted.org/packages/a1/75/1dc2603bf8184da9ebe69200849c53c3c1dca5b3a3d44d9f5ca06a930550/greenlet-3.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7791dcb496ec53d60c7f1c78eaa156c21f402dda38542a00afc3e20cae0f480f", size = 631652, upload-time = "2025-05-09T14:53:30.961Z" }, { url = "https://files.pythonhosted.org/packages/7b/74/ddc8c3bd4c2c20548e5bf2b1d2e312a717d44e2eca3eadcfc207b5f5ad80/greenlet-3.2.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8009ae46259e31bc73dc183e402f548e980c96f33a6ef58cc2e7865db012e13", size = 580619, upload-time = "2025-05-09T14:53:42.049Z" }, { url = "https://files.pythonhosted.org/packages/7e/f2/40f26d7b3077b1c7ae7318a4de1f8ffc1d8ccbad8f1d8979bf5080250fd6/greenlet-3.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fd9fb7c941280e2c837b603850efc93c999ae58aae2b40765ed682a6907ebbc5", size = 1109809, upload-time = "2025-05-09T15:26:59.063Z" }, { url = "https://files.pythonhosted.org/packages/c5/21/9329e8c276746b0d2318b696606753f5e7b72d478adcf4ad9a975521ea5f/greenlet-3.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:00cd814b8959b95a546e47e8d589610534cfb71f19802ea8a2ad99d95d702057", size = 1133455, upload-time = "2025-05-09T14:53:55.823Z" }, { url = "https://files.pythonhosted.org/packages/bb/1e/0dca9619dbd736d6981f12f946a497ec21a0ea27262f563bca5729662d4d/greenlet-3.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:d0cb7d47199001de7658c213419358aa8937df767936506db0db7ce1a71f4a2f", size = 294991, upload-time = "2025-05-09T15:05:56.847Z" }, { url = "https://files.pythonhosted.org/packages/a3/9f/a47e19261747b562ce88219e5ed8c859d42c6e01e73da6fbfa3f08a7be13/greenlet-3.2.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:dcb9cebbf3f62cb1e5afacae90761ccce0effb3adaa32339a0670fe7805d8068", size = 268635, upload-time = "2025-05-09T14:50:39.007Z" }, { url = "https://files.pythonhosted.org/packages/11/80/a0042b91b66975f82a914d515e81c1944a3023f2ce1ed7a9b22e10b46919/greenlet-3.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf3fc9145141250907730886b031681dfcc0de1c158f3cc51c092223c0f381ce", size = 628786, upload-time = "2025-05-09T15:24:00.692Z" }, { url = "https://files.pythonhosted.org/packages/38/a2/8336bf1e691013f72a6ebab55da04db81a11f68e82bb691f434909fa1327/greenlet-3.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efcdfb9df109e8a3b475c016f60438fcd4be68cd13a365d42b35914cdab4bb2b", size = 640866, upload-time = "2025-05-09T15:24:48.153Z" }, { url = "https://files.pythonhosted.org/packages/f8/7e/f2a3a13e424670a5d08826dab7468fa5e403e0fbe0b5f951ff1bc4425b45/greenlet-3.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd139e4943547ce3a56ef4b8b1b9479f9e40bb47e72cc906f0f66b9d0d5cab3", size = 636752, upload-time = "2025-05-09T15:29:23.182Z" }, { url = "https://files.pythonhosted.org/packages/fd/5d/ce4a03a36d956dcc29b761283f084eb4a3863401c7cb505f113f73af8774/greenlet-3.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71566302219b17ca354eb274dfd29b8da3c268e41b646f330e324e3967546a74", size = 636028, upload-time = "2025-05-09T14:53:32.854Z" }, { url = "https://files.pythonhosted.org/packages/4b/29/b130946b57e3ceb039238413790dd3793c5e7b8e14a54968de1fe449a7cf/greenlet-3.2.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3091bc45e6b0c73f225374fefa1536cd91b1e987377b12ef5b19129b07d93ebe", size = 583869, upload-time = "2025-05-09T14:53:43.614Z" }, { url = "https://files.pythonhosted.org/packages/ac/30/9f538dfe7f87b90ecc75e589d20cbd71635531a617a336c386d775725a8b/greenlet-3.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:44671c29da26539a5f142257eaba5110f71887c24d40df3ac87f1117df589e0e", size = 1112886, upload-time = "2025-05-09T15:27:01.304Z" }, { url = "https://files.pythonhosted.org/packages/be/92/4b7deeb1a1e9c32c1b59fdca1cac3175731c23311ddca2ea28a8b6ada91c/greenlet-3.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c23ea227847c9dbe0b3910f5c0dd95658b607137614eb821e6cbaecd60d81cc6", size = 1138355, upload-time = "2025-05-09T14:53:58.011Z" }, { url = "https://files.pythonhosted.org/packages/c5/eb/7551c751a2ea6498907b2fcbe31d7a54b602ba5e8eb9550a9695ca25d25c/greenlet-3.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:0a16fb934fcabfdfacf21d79e6fed81809d8cd97bc1be9d9c89f0e4567143d7b", size = 295437, upload-time = "2025-05-09T15:00:57.733Z" }, { url = "https://files.pythonhosted.org/packages/2c/a1/88fdc6ce0df6ad361a30ed78d24c86ea32acb2b563f33e39e927b1da9ea0/greenlet-3.2.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:df4d1509efd4977e6a844ac96d8be0b9e5aa5d5c77aa27ca9f4d3f92d3fcf330", size = 270413, upload-time = "2025-05-09T14:51:32.455Z" }, { url = "https://files.pythonhosted.org/packages/a6/2e/6c1caffd65490c68cd9bcec8cb7feb8ac7b27d38ba1fea121fdc1f2331dc/greenlet-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da956d534a6d1b9841f95ad0f18ace637668f680b1339ca4dcfb2c1837880a0b", size = 637242, upload-time = "2025-05-09T15:24:02.63Z" }, { url = "https://files.pythonhosted.org/packages/98/28/088af2cedf8823b6b7ab029a5626302af4ca1037cf8b998bed3a8d3cb9e2/greenlet-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c7b15fb9b88d9ee07e076f5a683027bc3befd5bb5d25954bb633c385d8b737e", size = 651444, upload-time = "2025-05-09T15:24:49.856Z" }, { url = "https://files.pythonhosted.org/packages/4a/9f/0116ab876bb0bc7a81eadc21c3f02cd6100dcd25a1cf2a085a130a63a26a/greenlet-3.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752f0e79785e11180ebd2e726c8a88109ded3e2301d40abced2543aa5d164275", size = 646067, upload-time = "2025-05-09T15:29:24.989Z" }, { url = "https://files.pythonhosted.org/packages/35/17/bb8f9c9580e28a94a9575da847c257953d5eb6e39ca888239183320c1c28/greenlet-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae572c996ae4b5e122331e12bbb971ea49c08cc7c232d1bd43150800a2d6c65", size = 648153, upload-time = "2025-05-09T14:53:34.716Z" }, { url = "https://files.pythonhosted.org/packages/2c/ee/7f31b6f7021b8df6f7203b53b9cc741b939a2591dcc6d899d8042fcf66f2/greenlet-3.2.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02f5972ff02c9cf615357c17ab713737cccfd0eaf69b951084a9fd43f39833d3", size = 603865, upload-time = "2025-05-09T14:53:45.738Z" }, { url = "https://files.pythonhosted.org/packages/b5/2d/759fa59323b521c6f223276a4fc3d3719475dc9ae4c44c2fe7fc750f8de0/greenlet-3.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4fefc7aa68b34b9224490dfda2e70ccf2131368493add64b4ef2d372955c207e", size = 1119575, upload-time = "2025-05-09T15:27:04.248Z" }, { url = "https://files.pythonhosted.org/packages/30/05/356813470060bce0e81c3df63ab8cd1967c1ff6f5189760c1a4734d405ba/greenlet-3.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a31ead8411a027c2c4759113cf2bd473690517494f3d6e4bf67064589afcd3c5", size = 1147460, upload-time = "2025-05-09T14:54:00.315Z" }, { url = "https://files.pythonhosted.org/packages/07/f4/b2a26a309a04fb844c7406a4501331b9400e1dd7dd64d3450472fd47d2e1/greenlet-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec", size = 296239, upload-time = "2025-05-09T14:57:17.633Z" }, { url = "https://files.pythonhosted.org/packages/89/30/97b49779fff8601af20972a62cc4af0c497c1504dfbb3e93be218e093f21/greenlet-3.2.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59", size = 269150, upload-time = "2025-05-09T14:50:30.784Z" }, { url = "https://files.pythonhosted.org/packages/21/30/877245def4220f684bc2e01df1c2e782c164e84b32e07373992f14a2d107/greenlet-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf", size = 637381, upload-time = "2025-05-09T15:24:12.893Z" }, { url = "https://files.pythonhosted.org/packages/8e/16/adf937908e1f913856b5371c1d8bdaef5f58f251d714085abeea73ecc471/greenlet-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325", size = 651427, upload-time = "2025-05-09T15:24:51.074Z" }, { url = "https://files.pythonhosted.org/packages/ad/49/6d79f58fa695b618654adac64e56aff2eeb13344dc28259af8f505662bb1/greenlet-3.2.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5", size = 645795, upload-time = "2025-05-09T15:29:26.673Z" }, { url = "https://files.pythonhosted.org/packages/5a/e6/28ed5cb929c6b2f001e96b1d0698c622976cd8f1e41fe7ebc047fa7c6dd4/greenlet-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825", size = 648398, upload-time = "2025-05-09T14:53:36.61Z" }, { url = "https://files.pythonhosted.org/packages/9d/70/b200194e25ae86bc57077f695b6cc47ee3118becf54130c5514456cf8dac/greenlet-3.2.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d", size = 606795, upload-time = "2025-05-09T14:53:47.039Z" }, { url = "https://files.pythonhosted.org/packages/f8/c8/ba1def67513a941154ed8f9477ae6e5a03f645be6b507d3930f72ed508d3/greenlet-3.2.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf", size = 1117976, upload-time = "2025-05-09T15:27:06.542Z" }, { url = "https://files.pythonhosted.org/packages/c3/30/d0e88c1cfcc1b3331d63c2b54a0a3a4a950ef202fb8b92e772ca714a9221/greenlet-3.2.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708", size = 1145509, upload-time = "2025-05-09T14:54:02.223Z" }, { url = "https://files.pythonhosted.org/packages/90/2e/59d6491834b6e289051b252cf4776d16da51c7c6ca6a87ff97e3a50aa0cd/greenlet-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421", size = 296023, upload-time = "2025-05-09T14:53:24.157Z" }, { url = "https://files.pythonhosted.org/packages/65/66/8a73aace5a5335a1cba56d0da71b7bd93e450f17d372c5b7c5fa547557e9/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418", size = 629911, upload-time = "2025-05-09T15:24:22.376Z" }, { url = "https://files.pythonhosted.org/packages/48/08/c8b8ebac4e0c95dcc68ec99198842e7db53eda4ab3fb0a4e785690883991/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4", size = 635251, upload-time = "2025-05-09T15:24:52.205Z" }, { url = "https://files.pythonhosted.org/packages/37/26/7db30868f73e86b9125264d2959acabea132b444b88185ba5c462cb8e571/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763", size = 632620, upload-time = "2025-05-09T15:29:28.051Z" }, { url = "https://files.pythonhosted.org/packages/10/ec/718a3bd56249e729016b0b69bee4adea0dfccf6ca43d147ef3b21edbca16/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b", size = 628851, upload-time = "2025-05-09T14:53:38.472Z" }, { url = "https://files.pythonhosted.org/packages/9b/9d/d1c79286a76bc62ccdc1387291464af16a4204ea717f24e77b0acd623b99/greenlet-3.2.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207", size = 593718, upload-time = "2025-05-09T14:53:48.313Z" }, { url = "https://files.pythonhosted.org/packages/cd/41/96ba2bf948f67b245784cd294b84e3d17933597dffd3acdb367a210d1949/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8", size = 1105752, upload-time = "2025-05-09T15:27:08.217Z" }, { url = "https://files.pythonhosted.org/packages/68/3b/3b97f9d33c1f2eb081759da62bd6162159db260f602f048bc2f36b4c453e/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51", size = 1125170, upload-time = "2025-05-09T14:54:04.082Z" }, { url = "https://files.pythonhosted.org/packages/31/df/b7d17d66c8d0f578d2885a3d8f565e9e4725eacc9d3fdc946d0031c055c4/greenlet-3.2.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", size = 269899, upload-time = "2025-05-09T14:54:01.581Z" }, { url = "https://files.pythonhosted.org/packages/37/3a/dbf22e1c7c1affc68ad4bc8f06619945c74a92b112ae6a401bed1f1ed63b/greenlet-3.2.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:1e4747712c4365ef6765708f948acc9c10350719ca0545e362c24ab973017370", size = 266190, upload-time = "2025-05-09T14:50:53.356Z" }, { url = "https://files.pythonhosted.org/packages/33/b1/21fabb65b13f504e8428595c54be73b78e7a542a2bd08ed9e1c56c8fcee2/greenlet-3.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782743700ab75716650b5238a4759f840bb2dcf7bff56917e9ffdf9f1f23ec59", size = 623904, upload-time = "2025-05-09T15:24:24.588Z" }, { url = "https://files.pythonhosted.org/packages/ec/9e/3346e463f13b593aafc683df6a85e9495a9b0c16c54c41f7e34353adea40/greenlet-3.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:354f67445f5bed6604e493a06a9a49ad65675d3d03477d38a4db4a427e9aad0e", size = 635672, upload-time = "2025-05-09T15:24:53.737Z" }, { url = "https://files.pythonhosted.org/packages/8e/88/6e8459e4789a276d1a18d656fd95334d21fe0609c6d6f446f88dbfd9483d/greenlet-3.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3aeca9848d08ce5eb653cf16e15bb25beeab36e53eb71cc32569f5f3afb2a3aa", size = 630975, upload-time = "2025-05-09T15:29:29.393Z" }, { url = "https://files.pythonhosted.org/packages/ab/80/81ccf96daf166e8334c37663498dad742d61114cdf801f4872a38e8e31d5/greenlet-3.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cb8553ee954536500d88a1a2f58fcb867e45125e600e80f586ade399b3f8819", size = 630252, upload-time = "2025-05-09T14:53:42.765Z" }, { url = "https://files.pythonhosted.org/packages/c1/61/3489e3fd3b7dc81c73368177313231a1a1b30df660a0c117830aa18e0f29/greenlet-3.2.2-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1592a615b598643dbfd566bac8467f06c8c8ab6e56f069e573832ed1d5d528cc", size = 579122, upload-time = "2025-05-09T14:53:49.702Z" }, { url = "https://files.pythonhosted.org/packages/be/55/57685fe335e88f8c75d204f9967e46e5fba601f861fb80821e5fb7ab959d/greenlet-3.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1f72667cc341c95184f1c68f957cb2d4fc31eef81646e8e59358a10ce6689457", size = 1108299, upload-time = "2025-05-09T15:27:10.193Z" }, { url = "https://files.pythonhosted.org/packages/ef/e2/3f27dd194989e8481ccac3b36932836b596d58f908106b8608f98587d9f7/greenlet-3.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a8fa80665b1a29faf76800173ff5325095f3e66a78e62999929809907aca5659", size = 1132431, upload-time = "2025-05-09T14:54:05.517Z" }, { url = "https://files.pythonhosted.org/packages/ce/7b/803075f7b1df9165032af07d81d783b04c59e64fb28b09fd7a0e5a249adc/greenlet-3.2.2-cp39-cp39-win32.whl", hash = "sha256:6629311595e3fe7304039c67f00d145cd1d38cf723bb5b99cc987b23c1433d61", size = 277740, upload-time = "2025-05-09T15:13:47.009Z" }, { url = "https://files.pythonhosted.org/packages/ff/a3/eb7713abfd0a079d24b775d01c6578afbcc6676d89508ab3cbebd5c836ea/greenlet-3.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:eeb27bece45c0c2a5842ac4c5a1b5c2ceaefe5711078eed4e8043159fa05c834", size = 294863, upload-time = "2025-05-09T15:09:46.366Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi", marker = "python_full_version >= '3.12'" }, { name = "h11", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "python_full_version >= '3.12'" }, { name = "certifi", marker = "python_full_version >= '3.12'" }, { name = "httpcore", marker = "python_full_version >= '3.12'" }, { name = "idna", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "identify" version = "2.6.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201, upload-time = "2025-04-19T15:10:38.32Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101, upload-time = "2025-04-19T15:10:36.701Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "imagesize" version = "1.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] [[package]] name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "itsdangerous" version = "2.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] [[package]] name = "mypy" version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433, upload-time = "2025-02-05T03:49:29.145Z" }, { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472, upload-time = "2025-02-05T03:49:16.986Z" }, { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424, upload-time = "2025-02-05T03:49:46.908Z" }, { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450, upload-time = "2025-02-05T03:50:05.89Z" }, { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765, upload-time = "2025-02-05T03:49:33.56Z" }, { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701, upload-time = "2025-02-05T03:49:38.981Z" }, { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload-time = "2025-02-05T03:50:17.287Z" }, { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload-time = "2025-02-05T03:49:51.21Z" }, { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload-time = "2025-02-05T03:50:20.885Z" }, { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload-time = "2025-02-05T03:49:42.408Z" }, { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload-time = "2025-02-05T03:49:07.707Z" }, { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload-time = "2025-02-05T03:49:54.581Z" }, { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981, upload-time = "2025-02-05T03:50:28.25Z" }, { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175, upload-time = "2025-02-05T03:50:13.411Z" }, { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675, upload-time = "2025-02-05T03:50:31.421Z" }, { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020, upload-time = "2025-02-05T03:48:48.705Z" }, { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582, upload-time = "2025-02-05T03:49:03.628Z" }, { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614, upload-time = "2025-02-05T03:50:00.313Z" }, { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload-time = "2025-02-05T03:48:55.789Z" }, { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload-time = "2025-02-05T03:48:44.581Z" }, { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload-time = "2025-02-05T03:49:25.514Z" }, { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" }, { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" }, { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" }, { url = "https://files.pythonhosted.org/packages/5a/fa/79cf41a55b682794abe71372151dbbf856e3008f6767057229e6649d294a/mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078", size = 10737129, upload-time = "2025-02-05T03:50:24.509Z" }, { url = "https://files.pythonhosted.org/packages/d3/33/dd8feb2597d648de29e3da0a8bf4e1afbda472964d2a4a0052203a6f3594/mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba", size = 9856335, upload-time = "2025-02-05T03:49:36.398Z" }, { url = "https://files.pythonhosted.org/packages/e4/b5/74508959c1b06b96674b364ffeb7ae5802646b32929b7701fc6b18447592/mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5", size = 11611935, upload-time = "2025-02-05T03:49:14.154Z" }, { url = "https://files.pythonhosted.org/packages/6c/53/da61b9d9973efcd6507183fdad96606996191657fe79701b2c818714d573/mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b", size = 12365827, upload-time = "2025-02-05T03:48:59.458Z" }, { url = "https://files.pythonhosted.org/packages/c1/72/965bd9ee89540c79a25778cc080c7e6ef40aa1eeac4d52cec7eae6eb5228/mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2", size = 12541924, upload-time = "2025-02-05T03:50:03.12Z" }, { url = "https://files.pythonhosted.org/packages/46/d0/f41645c2eb263e6c77ada7d76f894c580c9ddb20d77f0c24d34273a4dab2/mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980", size = 9271176, upload-time = "2025-02-05T03:50:10.86Z" }, { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pallets-sphinx-themes" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-notfound-page" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/08/c57dd89e45dbc976930200a2cb7826ed76f3c9791454a9fcd1cde3f17177/pallets_sphinx_themes-2.3.0.tar.gz", hash = "sha256:6293ced11a1d5d3de7268af1acd60428732b5a9e6051a47a596c6d9a083e60d9", size = 21029, upload-time = "2024-10-24T18:52:38.574Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/7d/a4aa06e452e031559dcfb066e035d2615ebfa6148e93514d7c36030004c1/pallets_sphinx_themes-2.3.0-py3-none-any.whl", hash = "sha256:7ed13de3743c462c2804e2aa63d96cc9ffa82cb76d0251cea03de9bcd9f8dbec", size = 24745, upload-time = "2024-10-24T18:52:37.265Z" }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] name = "pre-commit" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, ] [[package]] name = "pre-commit-uv" version = "4.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pre-commit" }, { name = "uv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/6c/c3c1d01698c8abb0b546defc0304971fa7fb2ba84ad35587b9dad095d73f/pre_commit_uv-4.1.4.tar.gz", hash = "sha256:3db606a79b226127b27dbbd8381b78c0e30de3ac775a8492c576a68e9250535c", size = 6493, upload-time = "2024-10-29T23:07:28.918Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/70/1b65f9118ef64f6ffe5d57a67170bbff25d4f4a3d1cb78e8ed3392e16114/pre_commit_uv-4.1.4-py3-none-any.whl", hash = "sha256:7f01fb494fa1caa5097d20a38f71df7cea0209197b2564699cef9b3f3aa9d135", size = 5578, upload-time = "2024-10-29T23:07:27.128Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] [[package]] name = "pyproject-api" version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/66/fdc17e94486836eda4ba7113c0db9ac7e2f4eea1b968ee09de2fe75e391b/pyproject_api-1.9.0.tar.gz", hash = "sha256:7e8a9854b2dfb49454fae421cb86af43efbb2b2454e5646ffb7623540321ae6e", size = 22714, upload-time = "2025-01-21T18:02:00.923Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b0/1d/92b7c765df46f454889d9610292b0ccab15362be3119b9a624458455e8d5/pyproject_api-1.9.0-py3-none-any.whl", hash = "sha256:326df9d68dea22d9d98b5243c46e3ca3161b07a1b9b18e213d1e24fd0e605766", size = 13131, upload-time = "2025-01-21T18:01:58.927Z" }, ] [[package]] name = "pyright" version = "1.1.400" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546, upload-time = "2025-04-24T12:55:18.907Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460, upload-time = "2025-04-24T12:55:17.002Z" }, ] [[package]] name = "pytest" version = "8.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] name = "python-dotenv" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] [[package]] name = "requests" version = "2.32.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] [[package]] name = "roman-numerals-py" version = "3.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, ] [[package]] name = "ruff" version = "0.11.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f5/e7/e55dda1c92cdcf34b677ebef17486669800de01e887b7831a1b8fdf5cb08/ruff-0.11.9.tar.gz", hash = "sha256:ebd58d4f67a00afb3a30bf7d383e52d0e036e6195143c6db7019604a05335517", size = 4132134, upload-time = "2025-05-09T16:19:41.511Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/71/75dfb7194fe6502708e547941d41162574d1f579c4676a8eb645bf1a6842/ruff-0.11.9-py3-none-linux_armv6l.whl", hash = "sha256:a31a1d143a5e6f499d1fb480f8e1e780b4dfdd580f86e05e87b835d22c5c6f8c", size = 10335453, upload-time = "2025-05-09T16:18:58.2Z" }, { url = "https://files.pythonhosted.org/packages/74/fc/ad80c869b1732f53c4232bbf341f33c5075b2c0fb3e488983eb55964076a/ruff-0.11.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66bc18ca783b97186a1f3100e91e492615767ae0a3be584e1266aa9051990722", size = 11072566, upload-time = "2025-05-09T16:19:01.432Z" }, { url = "https://files.pythonhosted.org/packages/87/0d/0ccececef8a0671dae155cbf7a1f90ea2dd1dba61405da60228bbe731d35/ruff-0.11.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd576cd06962825de8aece49f28707662ada6a1ff2db848d1348e12c580acbf1", size = 10435020, upload-time = "2025-05-09T16:19:03.897Z" }, { url = "https://files.pythonhosted.org/packages/52/01/e249e1da6ad722278094e183cbf22379a9bbe5f21a3e46cef24ccab76e22/ruff-0.11.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1d18b4be8182cc6fddf859ce432cc9631556e9f371ada52f3eaefc10d878de", size = 10593935, upload-time = "2025-05-09T16:19:06.455Z" }, { url = "https://files.pythonhosted.org/packages/ed/9a/40cf91f61e3003fe7bd43f1761882740e954506c5a0f9097b1cff861f04c/ruff-0.11.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f3f46f759ac623e94824b1e5a687a0df5cd7f5b00718ff9c24f0a894a683be7", size = 10172971, upload-time = "2025-05-09T16:19:10.261Z" }, { url = "https://files.pythonhosted.org/packages/61/12/d395203de1e8717d7a2071b5a340422726d4736f44daf2290aad1085075f/ruff-0.11.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34847eea11932d97b521450cf3e1d17863cfa5a94f21a056b93fb86f3f3dba2", size = 11748631, upload-time = "2025-05-09T16:19:12.307Z" }, { url = "https://files.pythonhosted.org/packages/66/d6/ef4d5eba77677eab511644c37c55a3bb8dcac1cdeb331123fe342c9a16c9/ruff-0.11.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f33b15e00435773df97cddcd263578aa83af996b913721d86f47f4e0ee0ff271", size = 12409236, upload-time = "2025-05-09T16:19:15.006Z" }, { url = "https://files.pythonhosted.org/packages/c5/8f/5a2c5fc6124dd925a5faf90e1089ee9036462118b619068e5b65f8ea03df/ruff-0.11.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b27613a683b086f2aca8996f63cb3dd7bc49e6eccf590563221f7b43ded3f65", size = 11881436, upload-time = "2025-05-09T16:19:17.063Z" }, { url = "https://files.pythonhosted.org/packages/39/d1/9683f469ae0b99b95ef99a56cfe8c8373c14eba26bd5c622150959ce9f64/ruff-0.11.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e0d88756e63e8302e630cee3ce2ffb77859797cc84a830a24473939e6da3ca6", size = 13982759, upload-time = "2025-05-09T16:19:19.693Z" }, { url = "https://files.pythonhosted.org/packages/4e/0b/c53a664f06e0faab596397867c6320c3816df479e888fe3af63bc3f89699/ruff-0.11.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537c82c9829d7811e3aa680205f94c81a2958a122ac391c0eb60336ace741a70", size = 11541985, upload-time = "2025-05-09T16:19:21.831Z" }, { url = "https://files.pythonhosted.org/packages/23/a0/156c4d7e685f6526a636a60986ee4a3c09c8c4e2a49b9a08c9913f46c139/ruff-0.11.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:440ac6a7029f3dee7d46ab7de6f54b19e34c2b090bb4f2480d0a2d635228f381", size = 10465775, upload-time = "2025-05-09T16:19:24.401Z" }, { url = "https://files.pythonhosted.org/packages/43/d5/88b9a6534d9d4952c355e38eabc343df812f168a2c811dbce7d681aeb404/ruff-0.11.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:71c539bac63d0788a30227ed4d43b81353c89437d355fdc52e0cda4ce5651787", size = 10170957, upload-time = "2025-05-09T16:19:27.08Z" }, { url = "https://files.pythonhosted.org/packages/f0/b8/2bd533bdaf469dc84b45815ab806784d561fab104d993a54e1852596d581/ruff-0.11.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c67117bc82457e4501473c5f5217d49d9222a360794bfb63968e09e70f340abd", size = 11143307, upload-time = "2025-05-09T16:19:29.462Z" }, { url = "https://files.pythonhosted.org/packages/2f/d9/43cfba291788459b9bfd4e09a0479aa94d05ab5021d381a502d61a807ec1/ruff-0.11.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e4b78454f97aa454586e8a5557facb40d683e74246c97372af3c2d76901d697b", size = 11603026, upload-time = "2025-05-09T16:19:31.569Z" }, { url = "https://files.pythonhosted.org/packages/22/e6/7ed70048e89b01d728ccc950557a17ecf8df4127b08a56944b9d0bae61bc/ruff-0.11.9-py3-none-win32.whl", hash = "sha256:7fe1bc950e7d7b42caaee2a8a3bc27410547cc032c9558ee2e0f6d3b209e845a", size = 10548627, upload-time = "2025-05-09T16:19:33.657Z" }, { url = "https://files.pythonhosted.org/packages/90/36/1da5d566271682ed10f436f732e5f75f926c17255c9c75cefb77d4bf8f10/ruff-0.11.9-py3-none-win_amd64.whl", hash = "sha256:52edaa4a6d70f8180343a5b7f030c7edd36ad180c9f4d224959c2d689962d964", size = 11634340, upload-time = "2025-05-09T16:19:35.815Z" }, { url = "https://files.pythonhosted.org/packages/40/f7/70aad26e5877c8f7ee5b161c4c9fa0100e63fc4c944dc6d97b9c7e871417/ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca", size = 10741080, upload-time = "2025-05-09T16:19:39.605Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] name = "snowballstemmer" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] [[package]] name = "sphinx" version = "7.4.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "babel", marker = "python_full_version < '3.10'" }, { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, { name = "docutils", marker = "python_full_version < '3.10'" }, { name = "imagesize", marker = "python_full_version < '3.10'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2", marker = "python_full_version < '3.10'" }, { name = "packaging", marker = "python_full_version < '3.10'" }, { name = "pygments", marker = "python_full_version < '3.10'" }, { name = "requests", marker = "python_full_version < '3.10'" }, { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, { name = "tomli", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, ] [[package]] name = "sphinx" version = "8.1.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "babel", marker = "python_full_version == '3.10.*'" }, { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, { name = "docutils", marker = "python_full_version == '3.10.*'" }, { name = "imagesize", marker = "python_full_version == '3.10.*'" }, { name = "jinja2", marker = "python_full_version == '3.10.*'" }, { name = "packaging", marker = "python_full_version == '3.10.*'" }, { name = "pygments", marker = "python_full_version == '3.10.*'" }, { name = "requests", marker = "python_full_version == '3.10.*'" }, { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, { name = "tomli", marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, ] [[package]] name = "sphinx" version = "8.2.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", ] dependencies = [ { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "babel", marker = "python_full_version >= '3.11'" }, { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, { name = "docutils", marker = "python_full_version >= '3.11'" }, { name = "imagesize", marker = "python_full_version >= '3.11'" }, { name = "jinja2", marker = "python_full_version >= '3.11'" }, { name = "packaging", marker = "python_full_version >= '3.11'" }, { name = "pygments", marker = "python_full_version >= '3.11'" }, { name = "requests", marker = "python_full_version >= '3.11'" }, { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, ] [[package]] name = "sphinx-autobuild" version = "2024.10.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "starlette" }, { name = "uvicorn" }, { name = "watchfiles" }, { name = "websockets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908, upload-time = "2024-10-02T23:15:28.739Z" }, ] [[package]] name = "sphinx-notfound-page" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/b2/67603444a8ee97b4a8ea71b0a9d6bab1727ed65e362c87e02f818ee57b8a/sphinx_notfound_page-1.1.0.tar.gz", hash = "sha256:913e1754370bb3db201d9300d458a8b8b5fb22e9246a816643a819a9ea2b8067", size = 7392, upload-time = "2025-01-28T18:45:02.871Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cd/d4/019fe439c840a7966012bbb95ccbdd81c5c10271749706793b43beb05145/sphinx_notfound_page-1.1.0-py3-none-any.whl", hash = "sha256:835dc76ff7914577a1f58d80a2c8418fb6138c0932c8da8adce4d9096fbcd389", size = 8167, upload-time = "2025-01-28T18:45:00.465Z" }, ] [[package]] name = "sphinx-tabs" version = "3.4.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "pygments" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/53/a9a91995cb365e589f413b77fc75f1c0e9b4ac61bfa8da52a779ad855cc0/sphinx-tabs-3.4.7.tar.gz", hash = "sha256:991ad4a424ff54119799ba1491701aa8130dd43509474aef45a81c42d889784d", size = 15891, upload-time = "2024-10-08T13:37:27.887Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/c6/f47505b564b918a3ba60c1e99232d4942c4a7e44ecaae603e829e3d05dae/sphinx_tabs-3.4.7-py3-none-any.whl", hash = "sha256:c12d7a36fd413b369e9e9967a0a4015781b71a9c393575419834f19204bd1915", size = 9727, upload-time = "2024-10-08T13:37:26.192Z" }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] [[package]] name = "sphinxcontrib-log-cabinet" version = "1.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/75/26/0687391e10c605a4d0c7ebe118c57c51ecc687128bcdae5803d9b96def81/sphinxcontrib-log-cabinet-1.0.1.tar.gz", hash = "sha256:103b2e62df4e57abb943bea05ee9c2beb7da922222c8b77314ffd6ab9901c558", size = 4072, upload-time = "2019-07-05T23:22:34.596Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/e7/dbfc155c1b4c429a9a8149032a56bfb7bab4efabc656abb24ab4619c715d/sphinxcontrib_log_cabinet-1.0.1-py2.py3-none-any.whl", hash = "sha256:3decc888e8e453d1912cd95d50efb0794a4670a214efa65e71a7de277dcfe2cd", size = 4887, upload-time = "2019-07-05T23:22:32.969Z" }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] [[package]] name = "starlette" version = "0.46.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] name = "tox" version = "4.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "chardet" }, { name = "colorama" }, { name = "filelock" }, { name = "packaging" }, { name = "platformdirs" }, { name = "pluggy" }, { name = "pyproject-api" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fe/87/692478f0a194f1cad64803692642bd88c12c5b64eee16bf178e4a32e979c/tox-4.25.0.tar.gz", hash = "sha256:dd67f030317b80722cf52b246ff42aafd3ed27ddf331c415612d084304cf5e52", size = 196255, upload-time = "2025-03-27T15:13:37.519Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/38/33348de6fc4b1afb3d76d8485c8aecbdabcfb3af8da53d40c792332e2b37/tox-4.25.0-py3-none-any.whl", hash = "sha256:4dfdc7ba2cc6fdc6688dde1b21e7b46ff6c41795fb54586c91a3533317b5255c", size = 172420, upload-time = "2025-03-27T15:13:35.703Z" }, ] [[package]] name = "tox-uv" version = "1.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tox" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, { name = "uv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5d/3a/3e445f25978a716ba6674f33f687d9336d0312086a277a778a5e9e9220d7/tox_uv-1.25.0.tar.gz", hash = "sha256:59ee5e694c41fef7bbcf058f22a5f9b6a8509698def2ea60c08554f4e36b9fcc", size = 21114, upload-time = "2025-02-21T16:37:51.796Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a7/f5c29e0e6faaccefcab607f672b176927144e9412c8183d21301ea2a6f6c/tox_uv-1.25.0-py3-none-any.whl", hash = "sha256:50cfe7795dcd49b2160d7d65b5ece8717f38cfedc242c852a40ec0a71e159bf7", size = 16431, upload-time = "2025-02-21T16:37:49.657Z" }, ] [[package]] name = "types-contextvars" version = "2.4.7.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/20/6a0271fe78050f15eaad21f94a4efebbddcfd0cadac0d35b056e8d32b40f/types-contextvars-2.4.7.3.tar.gz", hash = "sha256:a15a1624c709d04974900ea4f8c4fc2676941bf7d4771a9c9c4ac3daa0e0060d", size = 3166, upload-time = "2023-07-07T09:16:39.067Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/5e/770b3dd271a925b4428ee17664536d037695698eb764b4c5ed6fe815169b/types_contextvars-2.4.7.3-py3-none-any.whl", hash = "sha256:bcd8e97a5b58e76d20f5cc161ba39b29b60ac46dcc6edf3e23c1d33f99b34351", size = 2752, upload-time = "2023-07-07T09:16:37.855Z" }, ] [[package]] name = "types-dataclasses" version = "0.6.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4b/6a/dec8fbc818b1e716cb2d9424f1ea0f6f3b1443460eb6a70d00d9d8527360/types-dataclasses-0.6.6.tar.gz", hash = "sha256:4b5a2fcf8e568d5a1974cd69010e320e1af8251177ec968de7b9bb49aa49f7b9", size = 2884, upload-time = "2022-06-30T09:49:21.449Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/31/85/23ab2bbc280266af5bf22ded4e070946d1694d1721ced90666b649eaa795/types_dataclasses-0.6.6-py3-none-any.whl", hash = "sha256:a0a1ab5324ba30363a15c9daa0f053ae4fff914812a1ebd8ad84a08e5349574d", size = 2868, upload-time = "2022-06-30T09:49:19.977Z" }, ] [[package]] name = "typing-extensions" version = "4.13.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, ] [[package]] name = "urllib3" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, ] [[package]] name = "uv" version = "0.7.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/77/9e/4ea6d224f868badecd48b8fed17f83adb0ff62f75bc21785d91dee75c744/uv-0.7.3.tar.gz", hash = "sha256:863ceb63aefc7c2db9918313a1cb3c8bf3fc3d59b656b617db9e4abad90373f3", size = 3242256, upload-time = "2025-05-07T20:01:59.783Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/8b/09a9d9da09d90ec6829dc4b3e9b7ff99222b7f05bc5d292bc30b04b92209/uv-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:f37c8a6b172776fb5305afe0699907aff44a778669de7a8fbe5a9c09c1a88a97", size = 16673361, upload-time = "2025-05-07T20:01:04.641Z" }, { url = "https://files.pythonhosted.org/packages/ba/de/794ea8c9729784c7626f05a98fe91b8367587f57f023cb95adcd8f8a9215/uv-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3e6e1fd5755d4ef4c6e1ce55bd2c6d9dec278a8bef5752703d702ce03704fe29", size = 16755964, upload-time = "2025-05-07T20:01:09.43Z" }, { url = "https://files.pythonhosted.org/packages/df/1b/50922bfbe1631d022e0c6434ade17158b9b4e0bb7fccc77c928e32dd9021/uv-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:db8a5d5995b160158405379deadf0ffccf849a5e7ce048900b73517daf109e2c", size = 15577471, upload-time = "2025-05-07T20:01:12.235Z" }, { url = "https://files.pythonhosted.org/packages/69/39/cba47262d9547695657885391b34e8732cb0c34b5b876b811851cd320f3a/uv-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:d246243f348796730e8ea9736ddd48702d4448d98af5e61693063ed616e30378", size = 16027456, upload-time = "2025-05-07T20:01:14.653Z" }, { url = "https://files.pythonhosted.org/packages/e6/33/1acf89318fb987a6eb9989a6991b76b6c930b6a724ce5f1ed848519d6a5f/uv-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acef117a0c52299e60c6f7a3e60849050cd233704c561f688fac1100d113da2e", size = 16390903, upload-time = "2025-05-07T20:01:17.018Z" }, { url = "https://files.pythonhosted.org/packages/ad/66/2fe8ec6e5390de4cfc6db312464b4f28e5b3d98d576adc42731c0aeb5073/uv-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90990e4c289feee24164c8e463fc0ebc9a336960119cd256acca7c1439f0f536", size = 17167937, upload-time = "2025-05-07T20:01:19.567Z" }, { url = "https://files.pythonhosted.org/packages/a5/8a/dc46e79f5fd068cb841a716a96f0344af62cf2deb2e78f57e0e147de26ac/uv-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4809e5f7f5b2d6423d6573fda5655389c955ca649499fe9750b61af95daf9b7d", size = 18077868, upload-time = "2025-05-07T20:01:22.447Z" }, { url = "https://files.pythonhosted.org/packages/da/af/f7165a205ce8bb5e00f197d86a6fce4b4a317db0e471a31db9137ca1cc2d/uv-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acff7fba5ff40dcb5a42de496db92a3965edac7a3d687d9b013ba6e0336995df", size = 17793072, upload-time = "2025-05-07T20:01:25.942Z" }, { url = "https://files.pythonhosted.org/packages/27/5e/2e9172ec3fa8acfa69642900d6eee8e5021f6c14d135edef524c674b4cfb/uv-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbb2d322d453e498e1431c51421cee597962ecd3f93fcef853b258e9c7e7636c", size = 22181943, upload-time = "2025-05-07T20:01:28.576Z" }, { url = "https://files.pythonhosted.org/packages/f1/b1/8af4ea6d09d05b9edead5e701dd91e04d55971483a7a644bab7a979bb46b/uv-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1414a026c153ae0731daed0812b17bf77d34eafedaeb3a5c72e08181aea116b", size = 17400777, upload-time = "2025-05-07T20:01:32.27Z" }, { url = "https://files.pythonhosted.org/packages/09/ae/ccd123274ae59707e84fc5542776f89887818bad915167fbaeda65ebf52a/uv-0.7.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c976fce3d1068a1d007f50127cc7873d67643c1a60439564970f092d9be41877", size = 16306132, upload-time = "2025-05-07T20:01:36.572Z" }, { url = "https://files.pythonhosted.org/packages/01/5c/99ef96ca53c74552b616bd341cd5d298bc8a603151343c409efeaf1552a0/uv-0.7.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:cc27207c35c959d2e0e873e86a80a2470a77b7a34a4512a831e8d4f7c87f4404", size = 16376728, upload-time = "2025-05-07T20:01:39.357Z" }, { url = "https://files.pythonhosted.org/packages/74/91/07f7e68f08e617d27ae9908a4e8deb756368b942319634956ed92d7cf35c/uv-0.7.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5eb4872888a9fb10b62cc00be8e84822d63d3e622a5f340248e53ecf321dba96", size = 16707670, upload-time = "2025-05-07T20:01:46.816Z" }, { url = "https://files.pythonhosted.org/packages/a9/73/385a5a55fccfebac84a88b629992e301c080640691f2e27a3e3ccee8315e/uv-0.7.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0646e463365e7277f22200ce2d43b7a44e5a3192320500b4983b4fe34d69a5fb", size = 17514613, upload-time = "2025-05-07T20:01:49.245Z" }, { url = "https://files.pythonhosted.org/packages/6a/97/1138bb26038805a14d930c7261faf363a5256757390b4be0aaf6e33a41c0/uv-0.7.3-py3-none-win32.whl", hash = "sha256:44e2f3fcbd1ab519bdb68986449b2e3103d2261be95f985cadcf7ec7c510b595", size = 16897117, upload-time = "2025-05-07T20:01:51.728Z" }, { url = "https://files.pythonhosted.org/packages/64/1b/c9f0ad7c75bf0a04c52c7e766593f5e79b1ac7d97fa1cb34c6ce0cfe3746/uv-0.7.3-py3-none-win_amd64.whl", hash = "sha256:0a446d4e5b10ce8a793156a276727bb7affa96a85e80dc5ad34e0c2de7e71cc8", size = 18323992, upload-time = "2025-05-07T20:01:54.495Z" }, { url = "https://files.pythonhosted.org/packages/47/1b/7ca1b8ec4bcf1c807f61e6ced7ca704791843cf1297db5edb54db07bd1db/uv-0.7.3-py3-none-win_arm64.whl", hash = "sha256:cb2547fd1466698e9b4f11de5eef7055b8cbcc3c693d79f6d747e3f8e6be2ab7", size = 17017988, upload-time = "2025-05-07T20:01:57.283Z" }, ] [[package]] name = "uvicorn" version = "0.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, ] [[package]] name = "virtualenv" version = "20.31.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, ] [[package]] name = "watchfiles" version = "1.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/03/e2/8ed598c42057de7aa5d97c472254af4906ff0a59a66699d426fc9ef795d7/watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9", size = 94537, upload-time = "2025-04-08T10:36:26.722Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/4d/d02e6ea147bb7fff5fd109c694a95109612f419abed46548a930e7f7afa3/watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40", size = 405632, upload-time = "2025-04-08T10:34:41.832Z" }, { url = "https://files.pythonhosted.org/packages/60/31/9ee50e29129d53a9a92ccf1d3992751dc56fc3c8f6ee721be1c7b9c81763/watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb", size = 395734, upload-time = "2025-04-08T10:34:44.236Z" }, { url = "https://files.pythonhosted.org/packages/ad/8c/759176c97195306f028024f878e7f1c776bda66ccc5c68fa51e699cf8f1d/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11", size = 455008, upload-time = "2025-04-08T10:34:45.617Z" }, { url = "https://files.pythonhosted.org/packages/55/1a/5e977250c795ee79a0229e3b7f5e3a1b664e4e450756a22da84d2f4979fe/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487", size = 459029, upload-time = "2025-04-08T10:34:46.814Z" }, { url = "https://files.pythonhosted.org/packages/e6/17/884cf039333605c1d6e296cf5be35fad0836953c3dfd2adb71b72f9dbcd0/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256", size = 488916, upload-time = "2025-04-08T10:34:48.571Z" }, { url = "https://files.pythonhosted.org/packages/ef/e0/bcb6e64b45837056c0a40f3a2db3ef51c2ced19fda38484fa7508e00632c/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85", size = 523763, upload-time = "2025-04-08T10:34:50.268Z" }, { url = "https://files.pythonhosted.org/packages/24/e9/f67e9199f3bb35c1837447ecf07e9830ec00ff5d35a61e08c2cd67217949/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358", size = 502891, upload-time = "2025-04-08T10:34:51.419Z" }, { url = "https://files.pythonhosted.org/packages/23/ed/a6cf815f215632f5c8065e9c41fe872025ffea35aa1f80499f86eae922db/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614", size = 454921, upload-time = "2025-04-08T10:34:52.67Z" }, { url = "https://files.pythonhosted.org/packages/92/4c/e14978599b80cde8486ab5a77a821e8a982ae8e2fcb22af7b0886a033ec8/watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f", size = 631422, upload-time = "2025-04-08T10:34:53.985Z" }, { url = "https://files.pythonhosted.org/packages/b2/1a/9263e34c3458f7614b657f974f4ee61fd72f58adce8b436e16450e054efd/watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d", size = 625675, upload-time = "2025-04-08T10:34:55.173Z" }, { url = "https://files.pythonhosted.org/packages/96/1f/1803a18bd6ab04a0766386a19bcfe64641381a04939efdaa95f0e3b0eb58/watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff", size = 277921, upload-time = "2025-04-08T10:34:56.318Z" }, { url = "https://files.pythonhosted.org/packages/c2/3b/29a89de074a7d6e8b4dc67c26e03d73313e4ecf0d6e97e942a65fa7c195e/watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92", size = 291526, upload-time = "2025-04-08T10:34:57.95Z" }, { url = "https://files.pythonhosted.org/packages/39/f4/41b591f59021786ef517e1cdc3b510383551846703e03f204827854a96f8/watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827", size = 405336, upload-time = "2025-04-08T10:34:59.359Z" }, { url = "https://files.pythonhosted.org/packages/ae/06/93789c135be4d6d0e4f63e96eea56dc54050b243eacc28439a26482b5235/watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4", size = 395977, upload-time = "2025-04-08T10:35:00.522Z" }, { url = "https://files.pythonhosted.org/packages/d2/db/1cd89bd83728ca37054512d4d35ab69b5f12b8aa2ac9be3b0276b3bf06cc/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d", size = 455232, upload-time = "2025-04-08T10:35:01.698Z" }, { url = "https://files.pythonhosted.org/packages/40/90/d8a4d44ffe960517e487c9c04f77b06b8abf05eb680bed71c82b5f2cad62/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63", size = 459151, upload-time = "2025-04-08T10:35:03.358Z" }, { url = "https://files.pythonhosted.org/packages/6c/da/267a1546f26465dead1719caaba3ce660657f83c9d9c052ba98fb8856e13/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418", size = 489054, upload-time = "2025-04-08T10:35:04.561Z" }, { url = "https://files.pythonhosted.org/packages/b1/31/33850dfd5c6efb6f27d2465cc4c6b27c5a6f5ed53c6fa63b7263cf5f60f6/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9", size = 523955, upload-time = "2025-04-08T10:35:05.786Z" }, { url = "https://files.pythonhosted.org/packages/09/84/b7d7b67856efb183a421f1416b44ca975cb2ea6c4544827955dfb01f7dc2/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6", size = 502234, upload-time = "2025-04-08T10:35:07.187Z" }, { url = "https://files.pythonhosted.org/packages/71/87/6dc5ec6882a2254cfdd8b0718b684504e737273903b65d7338efaba08b52/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25", size = 454750, upload-time = "2025-04-08T10:35:08.859Z" }, { url = "https://files.pythonhosted.org/packages/3d/6c/3786c50213451a0ad15170d091570d4a6554976cf0df19878002fc96075a/watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5", size = 631591, upload-time = "2025-04-08T10:35:10.64Z" }, { url = "https://files.pythonhosted.org/packages/1b/b3/1427425ade4e359a0deacce01a47a26024b2ccdb53098f9d64d497f6684c/watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01", size = 625370, upload-time = "2025-04-08T10:35:12.412Z" }, { url = "https://files.pythonhosted.org/packages/15/ba/f60e053b0b5b8145d682672024aa91370a29c5c921a88977eb565de34086/watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246", size = 277791, upload-time = "2025-04-08T10:35:13.719Z" }, { url = "https://files.pythonhosted.org/packages/50/ed/7603c4e164225c12c0d4e8700b64bb00e01a6c4eeea372292a3856be33a4/watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096", size = 291622, upload-time = "2025-04-08T10:35:15.071Z" }, { url = "https://files.pythonhosted.org/packages/a2/c2/99bb7c96b4450e36877fde33690ded286ff555b5a5c1d925855d556968a1/watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed", size = 283699, upload-time = "2025-04-08T10:35:16.732Z" }, { url = "https://files.pythonhosted.org/packages/2a/8c/4f0b9bdb75a1bfbd9c78fad7d8854369283f74fe7cf03eb16be77054536d/watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2", size = 401511, upload-time = "2025-04-08T10:35:17.956Z" }, { url = "https://files.pythonhosted.org/packages/dc/4e/7e15825def77f8bd359b6d3f379f0c9dac4eb09dd4ddd58fd7d14127179c/watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f", size = 392715, upload-time = "2025-04-08T10:35:19.202Z" }, { url = "https://files.pythonhosted.org/packages/58/65/b72fb817518728e08de5840d5d38571466c1b4a3f724d190cec909ee6f3f/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec", size = 454138, upload-time = "2025-04-08T10:35:20.586Z" }, { url = "https://files.pythonhosted.org/packages/3e/a4/86833fd2ea2e50ae28989f5950b5c3f91022d67092bfec08f8300d8b347b/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21", size = 458592, upload-time = "2025-04-08T10:35:21.87Z" }, { url = "https://files.pythonhosted.org/packages/38/7e/42cb8df8be9a37e50dd3a818816501cf7a20d635d76d6bd65aae3dbbff68/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512", size = 487532, upload-time = "2025-04-08T10:35:23.143Z" }, { url = "https://files.pythonhosted.org/packages/fc/fd/13d26721c85d7f3df6169d8b495fcac8ab0dc8f0945ebea8845de4681dab/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d", size = 522865, upload-time = "2025-04-08T10:35:24.702Z" }, { url = "https://files.pythonhosted.org/packages/a1/0d/7f9ae243c04e96c5455d111e21b09087d0eeaf9a1369e13a01c7d3d82478/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6", size = 499887, upload-time = "2025-04-08T10:35:25.969Z" }, { url = "https://files.pythonhosted.org/packages/8e/0f/a257766998e26aca4b3acf2ae97dff04b57071e991a510857d3799247c67/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234", size = 454498, upload-time = "2025-04-08T10:35:27.353Z" }, { url = "https://files.pythonhosted.org/packages/81/79/8bf142575a03e0af9c3d5f8bcae911ee6683ae93a625d349d4ecf4c8f7df/watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2", size = 630663, upload-time = "2025-04-08T10:35:28.685Z" }, { url = "https://files.pythonhosted.org/packages/f1/80/abe2e79f610e45c63a70d271caea90c49bbf93eb00fa947fa9b803a1d51f/watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663", size = 625410, upload-time = "2025-04-08T10:35:30.42Z" }, { url = "https://files.pythonhosted.org/packages/91/6f/bc7fbecb84a41a9069c2c6eb6319f7f7df113adf113e358c57fc1aff7ff5/watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249", size = 277965, upload-time = "2025-04-08T10:35:32.023Z" }, { url = "https://files.pythonhosted.org/packages/99/a5/bf1c297ea6649ec59e935ab311f63d8af5faa8f0b86993e3282b984263e3/watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705", size = 291693, upload-time = "2025-04-08T10:35:33.225Z" }, { url = "https://files.pythonhosted.org/packages/7f/7b/fd01087cc21db5c47e5beae507b87965db341cce8a86f9eb12bf5219d4e0/watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417", size = 283287, upload-time = "2025-04-08T10:35:34.568Z" }, { url = "https://files.pythonhosted.org/packages/c7/62/435766874b704f39b2fecd8395a29042db2b5ec4005bd34523415e9bd2e0/watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d", size = 401531, upload-time = "2025-04-08T10:35:35.792Z" }, { url = "https://files.pythonhosted.org/packages/6e/a6/e52a02c05411b9cb02823e6797ef9bbba0bfaf1bb627da1634d44d8af833/watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763", size = 392417, upload-time = "2025-04-08T10:35:37.048Z" }, { url = "https://files.pythonhosted.org/packages/3f/53/c4af6819770455932144e0109d4854437769672d7ad897e76e8e1673435d/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40", size = 453423, upload-time = "2025-04-08T10:35:38.357Z" }, { url = "https://files.pythonhosted.org/packages/cb/d1/8e88df58bbbf819b8bc5cfbacd3c79e01b40261cad0fc84d1e1ebd778a07/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563", size = 458185, upload-time = "2025-04-08T10:35:39.708Z" }, { url = "https://files.pythonhosted.org/packages/ff/70/fffaa11962dd5429e47e478a18736d4e42bec42404f5ee3b92ef1b87ad60/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04", size = 486696, upload-time = "2025-04-08T10:35:41.469Z" }, { url = "https://files.pythonhosted.org/packages/39/db/723c0328e8b3692d53eb273797d9a08be6ffb1d16f1c0ba2bdbdc2a3852c/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f", size = 522327, upload-time = "2025-04-08T10:35:43.289Z" }, { url = "https://files.pythonhosted.org/packages/cd/05/9fccc43c50c39a76b68343484b9da7b12d42d0859c37c61aec018c967a32/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a", size = 499741, upload-time = "2025-04-08T10:35:44.574Z" }, { url = "https://files.pythonhosted.org/packages/23/14/499e90c37fa518976782b10a18b18db9f55ea73ca14641615056f8194bb3/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827", size = 453995, upload-time = "2025-04-08T10:35:46.336Z" }, { url = "https://files.pythonhosted.org/packages/61/d9/f75d6840059320df5adecd2c687fbc18960a7f97b55c300d20f207d48aef/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a", size = 629693, upload-time = "2025-04-08T10:35:48.161Z" }, { url = "https://files.pythonhosted.org/packages/fc/17/180ca383f5061b61406477218c55d66ec118e6c0c51f02d8142895fcf0a9/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936", size = 624677, upload-time = "2025-04-08T10:35:49.65Z" }, { url = "https://files.pythonhosted.org/packages/bf/15/714d6ef307f803f236d69ee9d421763707899d6298d9f3183e55e366d9af/watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc", size = 277804, upload-time = "2025-04-08T10:35:51.093Z" }, { url = "https://files.pythonhosted.org/packages/a8/b4/c57b99518fadf431f3ef47a610839e46e5f8abf9814f969859d1c65c02c7/watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11", size = 291087, upload-time = "2025-04-08T10:35:52.458Z" }, { url = "https://files.pythonhosted.org/packages/c5/95/94f3dd15557f5553261e407551c5e4d340e50161c55aa30812c79da6cb04/watchfiles-1.0.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2cfb371be97d4db374cba381b9f911dd35bb5f4c58faa7b8b7106c8853e5d225", size = 405686, upload-time = "2025-04-08T10:35:53.86Z" }, { url = "https://files.pythonhosted.org/packages/f4/aa/b99e968153f8b70159ecca7b3daf46a6f46d97190bdaa3a449ad31b921d7/watchfiles-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3904d88955fda461ea2531fcf6ef73584ca921415d5cfa44457a225f4a42bc1", size = 396047, upload-time = "2025-04-08T10:35:55.232Z" }, { url = "https://files.pythonhosted.org/packages/23/cb/90d3d760ad4bc7290e313fb9236c7d60598627a25a5a72764e48d9652064/watchfiles-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b7a21715fb12274a71d335cff6c71fe7f676b293d322722fe708a9ec81d91f5", size = 456081, upload-time = "2025-04-08T10:35:57.102Z" }, { url = "https://files.pythonhosted.org/packages/3e/65/79c6cebe5bcb695cdac145946ad5a09b9f66762549e82fb2d064ea960c95/watchfiles-1.0.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dfd6ae1c385ab481766b3c61c44aca2b3cd775f6f7c0fa93d979ddec853d29d5", size = 459838, upload-time = "2025-04-08T10:35:58.867Z" }, { url = "https://files.pythonhosted.org/packages/3f/84/699f52632cdaa777f6df7f6f1cc02a23a75b41071b7e6765b9a412495f61/watchfiles-1.0.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b659576b950865fdad31fa491d31d37cf78b27113a7671d39f919828587b429b", size = 489753, upload-time = "2025-04-08T10:36:00.237Z" }, { url = "https://files.pythonhosted.org/packages/25/68/3241f82ad414fd969de6bf3a93805682e5eb589aeab510322f2aa14462f8/watchfiles-1.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1909e0a9cd95251b15bff4261de5dd7550885bd172e3536824bf1cf6b121e200", size = 525015, upload-time = "2025-04-08T10:36:02.159Z" }, { url = "https://files.pythonhosted.org/packages/85/c4/30d879e252f52b01660f545c193e6b81c48aac2e0eeec71263af3add905b/watchfiles-1.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:832ccc221927c860e7286c55c9b6ebcc0265d5e072f49c7f6456c7798d2b39aa", size = 503816, upload-time = "2025-04-08T10:36:03.869Z" }, { url = "https://files.pythonhosted.org/packages/6b/7d/fa34750f6f4b1a70d96fa6b685fe2948d01e3936328ea528f182943eb373/watchfiles-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85fbb6102b3296926d0c62cfc9347f6237fb9400aecd0ba6bbda94cae15f2b3b", size = 456137, upload-time = "2025-04-08T10:36:05.226Z" }, { url = "https://files.pythonhosted.org/packages/8f/0c/a1569709aaeccb1dd74b0dd304d0de29e3ea1fdf11e08c78f489628f9ebb/watchfiles-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:15ac96dd567ad6c71c71f7b2c658cb22b7734901546cd50a475128ab557593ca", size = 632673, upload-time = "2025-04-08T10:36:06.752Z" }, { url = "https://files.pythonhosted.org/packages/90/b6/645eaaca11f3ac625cf3b6e008e543acf0bf2581f68b5e205a13b05618b6/watchfiles-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b6227351e11c57ae997d222e13f5b6f1f0700d84b8c52304e8675d33a808382", size = 626659, upload-time = "2025-04-08T10:36:08.18Z" }, { url = "https://files.pythonhosted.org/packages/3a/c4/e741d9b92b0a2c74b976ff78bbc9a1276b4d904c590878e8fe0ec9fecca5/watchfiles-1.0.5-cp39-cp39-win32.whl", hash = "sha256:974866e0db748ebf1eccab17862bc0f0303807ed9cda465d1324625b81293a18", size = 278471, upload-time = "2025-04-08T10:36:10.546Z" }, { url = "https://files.pythonhosted.org/packages/50/1b/36b0cb6add99105f78931994b30bc1dd24118c0e659ab6a3ffe0dd8734d4/watchfiles-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:9848b21ae152fe79c10dd0197304ada8f7b586d3ebc3f27f43c506e5a52a863c", size = 292027, upload-time = "2025-04-08T10:36:11.901Z" }, { url = "https://files.pythonhosted.org/packages/1a/03/81f9fcc3963b3fc415cd4b0b2b39ee8cc136c42fb10a36acf38745e9d283/watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d", size = 405947, upload-time = "2025-04-08T10:36:13.721Z" }, { url = "https://files.pythonhosted.org/packages/54/97/8c4213a852feb64807ec1d380f42d4fc8bfaef896bdbd94318f8fd7f3e4e/watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034", size = 397276, upload-time = "2025-04-08T10:36:15.131Z" }, { url = "https://files.pythonhosted.org/packages/78/12/d4464d19860cb9672efa45eec1b08f8472c478ed67dcd30647c51ada7aef/watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965", size = 455550, upload-time = "2025-04-08T10:36:16.635Z" }, { url = "https://files.pythonhosted.org/packages/90/fb/b07bcdf1034d8edeaef4c22f3e9e3157d37c5071b5f9492ffdfa4ad4bed7/watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57", size = 455542, upload-time = "2025-04-08T10:36:18.655Z" }, { url = "https://files.pythonhosted.org/packages/5b/84/7b69282c0df2bf2dff4e50be2c54669cddf219a5a5fb077891c00c00e5c8/watchfiles-1.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:554389562c29c2c182e3908b149095051f81d28c2fec79ad6c8997d7d63e0009", size = 405783, upload-time = "2025-04-08T10:36:20.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/ae/03fca0545d99b7ea21df49bead7b51e7dca9ce3b45bb6d34530aa18c16a2/watchfiles-1.0.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a74add8d7727e6404d5dc4dcd7fac65d4d82f95928bbee0cf5414c900e86773e", size = 397133, upload-time = "2025-04-08T10:36:22.439Z" }, { url = "https://files.pythonhosted.org/packages/1a/07/c2b6390003e933b2e187a3f7070c00bd87da8a58d6f2393e039b06a88c2e/watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb1489f25b051a89fae574505cc26360c8e95e227a9500182a7fe0afcc500ce0", size = 456198, upload-time = "2025-04-08T10:36:23.884Z" }, { url = "https://files.pythonhosted.org/packages/46/d3/ecc62cbd7054f0812f3a7ca7c1c9f7ba99ba45efcfc8297a9fcd2c87b31c/watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0901429650652d3f0da90bad42bdafc1f9143ff3605633c455c999a2d786cac", size = 456511, upload-time = "2025-04-08T10:36:25.42Z" }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, { url = "https://files.pythonhosted.org/packages/36/db/3fff0bcbe339a6fa6a3b9e3fbc2bfb321ec2f4cd233692272c5a8d6cf801/websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5", size = 175424, upload-time = "2025-03-05T20:02:56.505Z" }, { url = "https://files.pythonhosted.org/packages/46/e6/519054c2f477def4165b0ec060ad664ed174e140b0d1cbb9fafa4a54f6db/websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a", size = 173077, upload-time = "2025-03-05T20:02:58.37Z" }, { url = "https://files.pythonhosted.org/packages/1a/21/c0712e382df64c93a0d16449ecbf87b647163485ca1cc3f6cbadb36d2b03/websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b", size = 173324, upload-time = "2025-03-05T20:02:59.773Z" }, { url = "https://files.pythonhosted.org/packages/1c/cb/51ba82e59b3a664df54beed8ad95517c1b4dc1a913730e7a7db778f21291/websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770", size = 182094, upload-time = "2025-03-05T20:03:01.827Z" }, { url = "https://files.pythonhosted.org/packages/fb/0f/bf3788c03fec679bcdaef787518dbe60d12fe5615a544a6d4cf82f045193/websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb", size = 181094, upload-time = "2025-03-05T20:03:03.123Z" }, { url = "https://files.pythonhosted.org/packages/5e/da/9fb8c21edbc719b66763a571afbaf206cb6d3736d28255a46fc2fe20f902/websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054", size = 181397, upload-time = "2025-03-05T20:03:04.443Z" }, { url = "https://files.pythonhosted.org/packages/2e/65/65f379525a2719e91d9d90c38fe8b8bc62bd3c702ac651b7278609b696c4/websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee", size = 181794, upload-time = "2025-03-05T20:03:06.708Z" }, { url = "https://files.pythonhosted.org/packages/d9/26/31ac2d08f8e9304d81a1a7ed2851c0300f636019a57cbaa91342015c72cc/websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed", size = 181194, upload-time = "2025-03-05T20:03:08.844Z" }, { url = "https://files.pythonhosted.org/packages/98/72/1090de20d6c91994cd4b357c3f75a4f25ee231b63e03adea89671cc12a3f/websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880", size = 181164, upload-time = "2025-03-05T20:03:10.242Z" }, { url = "https://files.pythonhosted.org/packages/2d/37/098f2e1c103ae8ed79b0e77f08d83b0ec0b241cf4b7f2f10edd0126472e1/websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411", size = 176381, upload-time = "2025-03-05T20:03:12.77Z" }, { url = "https://files.pythonhosted.org/packages/75/8b/a32978a3ab42cebb2ebdd5b05df0696a09f4d436ce69def11893afa301f0/websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4", size = 176841, upload-time = "2025-03-05T20:03:14.367Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, { url = "https://files.pythonhosted.org/packages/b7/48/4b67623bac4d79beb3a6bb27b803ba75c1bdedc06bd827e465803690a4b2/websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940", size = 173106, upload-time = "2025-03-05T20:03:29.404Z" }, { url = "https://files.pythonhosted.org/packages/ed/f0/adb07514a49fe5728192764e04295be78859e4a537ab8fcc518a3dbb3281/websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e", size = 173339, upload-time = "2025-03-05T20:03:30.755Z" }, { url = "https://files.pythonhosted.org/packages/87/28/bd23c6344b18fb43df40d0700f6d3fffcd7cef14a6995b4f976978b52e62/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9", size = 174597, upload-time = "2025-03-05T20:03:32.247Z" }, { url = "https://files.pythonhosted.org/packages/6d/79/ca288495863d0f23a60f546f0905ae8f3ed467ad87f8b6aceb65f4c013e4/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b", size = 174205, upload-time = "2025-03-05T20:03:33.731Z" }, { url = "https://files.pythonhosted.org/packages/04/e4/120ff3180b0872b1fe6637f6f995bcb009fb5c87d597c1fc21456f50c848/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f", size = 174150, upload-time = "2025-03-05T20:03:35.757Z" }, { url = "https://files.pythonhosted.org/packages/cb/c3/30e2f9c539b8da8b1d76f64012f3b19253271a63413b2d3adb94b143407f/websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123", size = 176877, upload-time = "2025-03-05T20:03:37.199Z" }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]] name = "werkzeug" version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, ] [[package]] name = "zipp" version = "3.21.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545, upload-time = "2024-11-10T15:05:20.202Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630, upload-time = "2024-11-10T15:05:19.275Z" }, ]