pax_global_header00006660000000000000000000000064152446005470014521gustar00rootroot0000000000000052 comment=6b318bd0c35c2e39eb7d55754bf00c17a1957028 pallets-quart-6b318bd/000077500000000000000000000000001524460054700147325ustar00rootroot00000000000000pallets-quart-6b318bd/.editorconfig000066400000000000000000000003511524460054700174060ustar00rootroot00000000000000root = 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 pallets-quart-6b318bd/.github/000077500000000000000000000000001524460054700162725ustar00rootroot00000000000000pallets-quart-6b318bd/.github/ISSUE_TEMPLATE/000077500000000000000000000000001524460054700204555ustar00rootroot00000000000000pallets-quart-6b318bd/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000011471524460054700230700ustar00rootroot00000000000000--- name: Bug report about: Report a bug in Quart (not other projects which depend on Quart) --- Environment: - Python version: - Quart version: pallets-quart-6b318bd/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000007411524460054700224470ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Security issue url: https://github.com/pallets/quart/security/advisories/new about: Do not report security issues publicly. Create a private advisory. - name: Questions url: https://github.com/pallets/quart/discussions/ about: Ask questions about your own code on the Discussions tab. - name: Questions on url: https://discord.gg/pallets about: Ask questions about your own code on our Discord chat. pallets-quart-6b318bd/.github/ISSUE_TEMPLATE/feature-request.md000066400000000000000000000006401524460054700241200ustar00rootroot00000000000000--- name: Feature request about: Suggest a new feature for Quart --- pallets-quart-6b318bd/.github/pull_request_template.md000066400000000000000000000006511524460054700232350ustar00rootroot00000000000000 pallets-quart-6b318bd/.github/workflows/000077500000000000000000000000001524460054700203275ustar00rootroot00000000000000pallets-quart-6b318bd/.github/workflows/pre-commit.yaml000066400000000000000000000017271524460054700232760ustar00rootroot00000000000000name: 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@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 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() }} pallets-quart-6b318bd/.github/workflows/publish.yaml000066400000000000000000000030031524460054700226550ustar00rootroot00000000000000name: Publish on: push: tags: ['*'] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 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 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: path: ./dist create-release: needs: [build] 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 }} artifact/* env: GH_TOKEN: ${{ github.token }} publish-pypi: needs: [build] environment: name: publish url: https://pypi.org/project/Quart/${{ 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/ pallets-quart-6b318bd/.github/workflows/tests.yaml000066400000000000000000000032521524460054700223570ustar00rootroot00000000000000name: 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.14t'} - {python: '3.14'} - {name: Windows, python: '3.14', os: windows-latest} - {name: Mac, python: '3.14', os: macos-latest} - {python: '3.13'} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 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@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 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 pallets-quart-6b318bd/.gitignore000066400000000000000000000001551524460054700167230ustar00rootroot00000000000000.idea/ .vscode/ __pycache__/ dist/ .coverage* htmlcov/ .tox/ .hypothesis/ docs/reference/source docs/_build/ pallets-quart-6b318bd/.pre-commit-config.yaml000066400000000000000000000011661524460054700212170ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: 9aeda5d1f4bbd212c557da1ea78eca9e8c829e19 # frozen: v0.11.13 hooks: - id: ruff - id: ruff-format - repo: https://github.com/astral-sh/uv-pre-commit rev: a621b109bab2e7e832d98c88fd3e83399f4e6657 # frozen: 0.7.12 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 pallets-quart-6b318bd/.readthedocs.yaml000066400000000000000000000004221524460054700201570ustar00rootroot00000000000000version: 2 build: os: ubuntu-24.04 tools: python: '3.13' commands: - asdf plugin add uv - asdf install uv latest - asdf global uv latest # TODO fix warnings and add -W - uv run --group docs sphinx-build -b dirhtml docs $READTHEDOCS_OUTPUT/html pallets-quart-6b318bd/CHANGES.md000066400000000000000000000701751524460054700163360ustar00rootroot00000000000000## Version 0.23.1 Released 2026-08-29 ## Version 0.23.0 Released 2026-08-29 - Improve the development reloader - it now works in a similar way to Flask's with the app run in a subprocess such that it reloads after syntax errors etc... are fixed rather than crashing as now. - Provide backpressure to the ASGI server and clean up the request/websocket ASGI handling. - Ensure all teardown functions are called as per Flask. - Merge contexts into a single AppContext as per Flask. This could be a breaking change if the other contexts where used directly. - Drop support for Python versions less than 3.13. ## Version 0.22.0 Released 2026-08-19 - Use Quart's logger rather than Hypercorn's in app.run - Document serving function call ordering - Bugfix ensure the path is correct when using root_path - Bugfix Correct traceback/error behaviour ## Version 0.21.0 Released 2026-07-23 - Support 3.14 and drop 3.10 and 3.9 - Fix non-port-needing URI schemes with app run. - Fix propagating exceptions. - Add --debug/--no-debug flag for quart run. - Base Quart's default_config on Flask's - Remove invalid uses of AnyStr. - Improve file type hints. - Flask backport: Fix signing key selection order when key rotation is enabled via ``SECRET_KEY_FALLBACKS``. - Support partitioned cookies - Don't apply max_form_parts to non-multipart forms ## Version 0.20.0 Released 2024-12-23 - Drop support for Python 3.8. - Fix deprecated `asyncio.iscoroutinefunction` for Python 3.14. - Allow `AsyncIterable` to be passed to `Response`. - Support max_form_parts and max_form_memory_size. ## Version 0.19.9 Released 2024-11-14 - Fix missing `PROVIDE_AUTOMATIC_OPTIONS` config for compatibility with Flask 3.1. ## Version 0.19.8 Released 2024-10-25 - Fix missing check that caused the previous fix to raise an error. ## Version 0.19.7 Released 2024-10-25 - Fix how `max_form_memory_size` is applied when parsing large non-file fields. ## Version 0.19.6 Released 2024-05-19 - Use `ContentRange` in the right way. - Hold a strong reference to background tasks. - Avoid `ResourceWarning` in `DataBody.__aiter__`. ## Version 0.19.5 Released 2024-04-01 - Address `DeprecationWarning` from `datetime.utcnow()`. - Ensure request files are closed. - Fix development server restarting when commands are passed. - Restore `teardown_websocket` methods. - Correct the `config_class` type. - Allow `kwargs` to be passed to the test client (matches Flask API). ## Version 0.19.4 Released 2023-11-19 - Fix program not closing on Ctrl+C in Windows. - Fix the typing for `AfterWebsocket` functions. - Improve the typing of the `ensure_async` method. - Add a shutdown event to the app. ## Version 0.19.3 Released 2023-10-04 - Update the default config to better match Flask. ## Version 0.19.2 Released 2023-10-01 - Restore the app `after_`/`before_websocket` methods. - Correctly set the `cli` group in Quart. ## Version 0.19.1 Released 2023-09-30 - Remove `QUART_ENV` and env usage. ## Version 0.19.0 Released 2023-09-30 - Remove Flask-Patch. It has been replaced with the Quart-Flask-Patch extension. - Remove references to first request, as per Flask. - Await the background tasks before calling the after serving functions. - Don't copy the app context into the background task. - Allow background tasks a grace period to complete during shutdown. - Base Quart on Flask, utilising Flask code where possible. This introduces a dependency on Flask. - Fix trailing slash issue in URL concatenation for empty `path`. - Use only CR in SSE documentation. - Fix typing for websocket to accept auth data. - Ensure subdomains apply to nested blueprints. - Ensure `make_response` errors if the value is incorrect. - Fix propagated exception handling. - Ensure exceptions propagate before logging. - Cope with `scope` extension value being `None`. - Ensure the conditional 304 response is empty. - Handle empty path in URL concatenation. - Corrected typing hint for `abort` method. - Fix `root_path` usage. - Fix Werkzeug deprecation warnings. - Add `.svg` to Jinja's autoescaping. - Improve the `WebsocketResponse` error, by including the response. - Add a file `mode` parameter to the `config.from_file` method. - Show the subdomain or host in the `routes` command output. - Upgrade to Blinker 1.6. - Require Werkzeug 3.0.0 and Flask 3.0.0. - Use `tomllib` rather than `toml`. ## Version 0.18.4 Released 2023-04-09 - Restrict Blinker to < 1.6 for 0.18.x versions to ensure it works with Quart's implementation. ## Version 0.18.3 Released 2022-10-08 - Corrected `quart.json.loads` type annotation. - Fix signal handling on Windows. - Add missing globals to Flask-Patch. ## Version 0.18.2 Released 2022-10-04 - Use `add_signal_handler` not `signal.signal`. ## Version 0.18.1 Released 2022-10-03 - Fix static hosting with resource path escaping the root. - Adopt the Werkzeug/Flask `make_conditional` API/functionality. - Restore the reloader to Quart. - Support subdomains when testing. - Fix the signal handling to work on Windows. ## Version 0.18.0 Released 2022-07-23 - Remove Quart's `safe_join`, use Werkzeug's version instead. - Drop toml dependency, as it isn't required in Quart (use `config.from_file` as desired). - Change `websocket.send_json` to match `jsonify`'s options. - Allow while serving decorators on blueprints. - Support synchronous background tasks, they will be run on a thread. - Follow Flask's API and allow empty argument `Response` construction. - Add `get_root_path` to helpers to match Flask. - Support `silent` argument in `config.from_envvar`. - Adopt Flask's logging setup. - Add `stream_template` and `stream_template_string` functions to stream a large template in parts. - Switch to Flask's top level name export style. - Add `aborter` object to app to allow for `abort` customisation. - Add `redirect` method to app to allow for `redirect` customisation. - Remove usage of `LocalStacks`, using `ContextVars` more directly. This should improve performance, but introduces backwards incompatibility. `_*_ctx_stack` globals are removed, use `_context` instead. Extensions should store on `g` as appropriate. Requires Werkzeug >= 2.2.0. - Returned lists are now jsonified. - Move `url_for` to the app to allow for `url_for` customisation. - Remove `config.from_json`, use `from_file` instead. - Match the Flask views classes and API. - Adopt the Flask cli code adding `--app`, `--env`, and `-debug` options to the CLI. - Adopt the Flask JSON provider interface, use instead of JSON encoders and decoders. - Switch to being a Pallets project. - Requires at least Click version 8. ## Version 0.17.0 Released 2022-03-26 - Raise startup and shutdown errors. - Allow loading of environment variables into the config. - Switch to Werkzeug's `redirect`. - Import `Markup` and `escape` from MarkupSafe. ## Version 0.16.3 Released 2022-02-02 - Ensure auth is sent on test client requests. ## Version 0.16.2 Released 2021-12-14 - Await background task shutdown after shutdown functions. - Use the before websocket not request functions. ## Version 0.16.1 Released 2021-11-17 - Add missing serving exception handling. ## Version 0.16.0 Released 2021-11-09 - Support an `auth` argument in the test client. - Support Python 3.10. - Utilise `ensure_async` in the copy context functions. - Add support for background tasks via `app.add_background_task`. - Give a clearer error for invalid response types. - Make `open_resource` and `open_instance_resource` async. - Allow `save_session` to accept None as a response value. - Rename errors to have an `Error` suffix. - Fix typing of before (first) request callables. - Support view sync handlers. - Fix import of method `redirect` from Flask. - Fix registering a blueprint twice with differing names. - Support `ctx.pop()` without passing `exc` explicitly. - A request timeout error should be raised on timeout. - Remove Jinja warnings. - Use the websocket context in the websocket method. - Raise any lifespan startup failures when testing. - Fix handler call order based on blueprint nesting. - Allow for generators that yield strings to be used. - Reorder acceptance to prevent race conditions. - Prevent multiple task form body parsing via a lock. ## Version 0.15.1 Released 2021-05-24 - Improve the `g` `AppGlobals` typing. - Fix nested blueprint `url_prefixes`. - Ensure the session is created before url matching. - Fix Flask Patch sync wrapping async. - Don't try to parse the form data multiple times. - Fix blueprint naming allowing blueprints to be registered with a different name. - Fix teardown callable typing. ## Version 0.15.0 Released 2021-05-11 - Add the `quart routes` to output the routes in the app. - Add the ability to close websocket connections with a reason if supported by the server. - Revert `AppContext` lifespan interaction change in 0.14. It is not possible to introduce this and match Flask's `g` usage. - Add syntactic sugar for route registration allowing `app.get`, `app.post`, etc. for app and blueprint instances. - Support handlers returning a Werkzeug `Response`. - Remove Quart's exceptions and use Werkzeug's. This may cause incompatibility to fix import from `werkzeug.exceptions` instead of `quart.exceptions`. - Switch to Werkzeug's locals and Sans-IO wrappers. - Allow for files to be sent via test client, via a `files` argument. - Make the `NoAppException` clearer. - Support nested blueprints. - Support `while_serving` functionality. - Correct routing host case matching. - Cache flashed message on `request.flashes`. - Fix debug defaults and overrides using `run`. - Adopt Werkzeug's timestamp parsing. - Only show the traceback response if propagating exceptions. - Fix unhandled exception handling. - Support `url_for` in websocket contexts. - Fix cookie jar handling in test client. - Support `SERVER_NAME` configuration for the `run` method. - Correctly support `root_paths`. - Support str and bytes streamed responses. - Match Flask and consume the raw data when form parsing. ## Version 0.14.1 Released 2020-12-13 - Add missing receive to test request connection and docs. - Add the `templates_auto_reload` API. - Setting the `debug` property on the app now also sets the auto reloading for the Jinja environment. ## Version 0.14.0 Released 2020-12-05 - Add `user_agent` property to requests/websockets to easily extract the user agent using Werkzeug's `UserAgent` class. - Set the content length when using send file instead of using chunked transfer encoding. - Introduce a `test_app` method, this should be used to ensure that the startup & shutdown functions are run during testing. - Prevent local data sharing. - Officially support Python 3.9. - Add send and receive json to the test websocket client, allows a simpler way for json to be sent and received using the app's encoder and decoder. - Add signals for websocket message receipt and sending, specifically the `websocket_received` and `websocket_sent` signals. - Add `dump` and `load` functions to the json module, as matching Flask. - Enhance the dev server output. - Change `AppContext` lifespan interaction - this pushes the app context on startup and pops on shutdown meaning `g` is available throughout without being cleared. - Major refactor of the testing system - this ensures that any middleware and lifespans are correctly tested. It also introduces a`request` method on the test client for a request connection (like the websocket connection) for testing streaming. ## Version 0.13.1 Released 2020-09-09 - Add the `data` property to the patched request attributes. - Fix WebSocket ASGI rejection (for servers that don't support the ASGI WebSocket response extension). - Don't wrap commands in `with_appcontext` by default. - Fix CSP parsing for the report-only header. - Wait for tasks to complete when cancelled. - Clean up the generator when the response exits. - Fix request data handling with Flask-Patch. ## Version 0.13.0 Released 2020-07-14 - Set cookies from the testing jar for websockets. - Restore Flask-Patch sync handling to pre 0.11. This means that sync route handlers, before request, and more, are **not** run in a thread if Flask-Patch is used. This restores Flask-SQLAlchemy support (with Flask-Patch). - Accept additional attributes to the delete cookie. ## Version 0.12.0 Released 2020-05-21 - Add `certfile` and `keyfile` arguments to cli. - `Request.host` value returns an empty string rather than `None` for HTTP/1.0 requests without a `Host` header. - Fix type of query string argument to Werkzeug `Map` fixing a `TypeError`. - Add ASGI `scope` dictionary to `request`. - Ensure that `FlaskGroup` exists when using `flask_patch` by patching the `flask.cli` module from quart. - Add `quart.cli.with_appcontext` matching the Flask API. - Make `quart.Blueprint` registration compatible with `flask.Blueprint`. - Make the `add_url_rule` API match the Flask API. - Resolve error handlers by most specific first (matches Flask). - Support test sessions and context preservation when testing. - Add `lookup_app` and `lookup_request` to Flask patch globals. - Make `quart.Blueprint` constructor fully compatible with `flask.Blueprint`. - Ensure url defaults aren't copied between blueprint routes. ## Version 0.11.5 Released 2020-03-31 - Ensure any exceptions are raised in the ASGI handling code. - Support url defaults in the blueprint API. ## Version 0.11.4 Released 2020-03-29 - Add a testing patch to ensure `FlaskClient` exists. - Security fix for the `htmlsafe` function. - Default to the map's strict slashes setting. - Fix host normalisation for route matching. - Add subdomain to the blueprint API. ## Version 0.11.3 Released 2020-02-26 - Lowercase header names passed to cgi `FieldStorage`. ## Version 0.11.2 Released 2020-02-10 - Fix debug traceback rendering. - Fix `multipart/form-data` parsing. - Uncomment cookie parameters. - Add `await` to the `LocalProxy` mappings. ## Version 0.11.1 Released 2020-02-09 - Fix cors header accessors and setters. - Fix `iscoroutinefunction` with Python3.7. - Fix `after_request`/`_websocket` function typing. ## Version 0.11.0 Released 2020-02-08 *This contains all the bug fixes from the 0.6 branch.* - Allow relative `root_path` values. - Add a `TooManyRequests`, 429, exception. - Run synchronous code via a `ThreadPoolExecutor`. This means that sync route handlers, before request, and more, are run in a thread. **This is a major change.** - Add an `asgi_app` method for middleware usage, for example `quart_app.asgi_app = Middleware(quart_app.asgi_app)`. - Add a `run_sync` function to run synchronous code in a thread pool with the Quart contexts present. - Set cookies on redirects when testing. - Follow the Flask API for `dumps`/`loads`. - Support loading configuration with a custom loader, `from_file` this allows for toml format configurations (among others). - Match the Werkzeug API in `redirect`. - Respect `QUART_DEBUG` when using `quart run`. - Follow the Flask exception propagation rules, ensuring exceptions are propagated in testing. - Support Python 3.8. - Redirect with a 308 rather than 301 (following Flask/Werkzeug). - Add a `_QUART_PATCHED` marker to all patched modules. - Ensure multiple cookies are respected during testing. - Switch to Werkzeug for datastructures and header parsing and dumping. **This is a major change.** - Make the lock class customisable by the app subclass, this allows Quart-Trio to override the lock type. - Add a `run_task` method to `Quart` (app) class. This is a task based on the `run` method assumptions that can be awaited or run as desired. - Switch JSON tag datetime format to allow reading of Flask encoded tags. - Switch to Werkzeug's cookie code. **This is a major change.** - Switch to Werkzeug's routing code. **This is a major change.** - Add signal handling to `run` method, but not the `run_task` method. ## Version 0.6.15 Released 2019-10-17 **This is the final 0.6 release and the final release to support Python3.6, Python3.8 is now available.** - Handle `http.request` without a `body` key ## Version 0.10.0 Released 2019-08-30 *This contains all the bug fixes from the 0.6 branch.* - Support aborting with a `Response` argument. - Fix JSON type hints to match typeshed. - Update to Hypercorn 0.7.0 as minimum version. - Ensure the default response timeout is set. - Allow returning dictionaries from view functions, this follows a new addition to Flask. - Ensure the response timeout has a default. - Correct testing websocket typing. - Accept `json`, `data`, or `form` arguments to `test_request_context`. - Support `send_file` sending a `BytesIO` object. - Add `samesite` cookie support (requires Python 3.8). - Add a `ContentSecurityPolicy` datastructure, this follows a new addition to Werkzeug. - Unblock logging I/O by logging in separate threads. - Support ASGI `root_path` as a prepended path to all routes. ## Version 0.6.14 Released 2019-08-30 - Follow Werkzeug `LocalProxy` name API. - Ensure multiple files are correctly loaded. - Ensure `make_response` status code is an int. - Be clear about header encoding. - Ensure loading form/files data is timeout protected. - Add missing `Unauthorized`, `Forbidden`, and `NotAcceptable` exception classes. ## Version 0.9.1 Released 2019-05-12 - Unquote the path in the test client, following the ASGI standard. - Follow Werkzeug `LocalProxy` name API. - Ensure multiple files are correctly loaded. ## Version 0.9.0 Released 2019-04-22 *This contains all the bug fixes from the 0.6 and 0.8 branches.* - Highlight the traceback line of code when using the debug system. - Ensure `debug` has an effect when passed to `app.run`. - Change the `test_request_context` arguments to match the test client `open` arguments. - Fix form data loading limit type. - Support async Session Interfaces (with continued support for sync interfaces). - Added `before_app_websocket`, and `after_app_websocket` methods to `Blueprint`. - Support sending headers on WebSocket acceptance (this requires ASGI server support, the default Hypercorn supports this). - Support async teardown functions (with continued support for sync functions). - Match the Flask API argument order for `send_file` adding a `mimetype` argument and supporting attachment sending. - Make the requested subprotocols available via the websocket class, `websocket.requested_subprotocols`. - Support session saving with WebSockets (errors for cookie sessions if the WebSocket connection has been accepted). - Switch to be an ASGI 3 framework (this requires ASGI server support, the default Hypercorn supports this). - Refactor push promise API, removes the `response.push_promises` attribute. - Accept `Path` types throughout and switch to `Path` types internally. ## Version 0.6.13 Released 2019-04-22 - Fix multipart parsing. - Added `Map.iter_rules(endpoint)` method. - Cope if there is no source code when using the debug system. ## Version 0.8.1 Released 2019-02-09 - Make the `safe_join` function stricter. - Parse `multipart/form-data` correctly. - Add missing `await`. ## Version 0.8.0 Released 2019-01-29 *This contains all the bug fixes from the 0.6 and 0.7 branches.* - Raise an error if the loaded app is not a Quart instance. - Remove unused `AccessLogAtoms`. - Change the `Quart.run` method interface, this reduces the available options for simplicity. See hypercorn for an extended set of deployment configuration. - Utilise the Hypercorn `serve` function, requires Hypercorn >= 0.5.0. - Added `list_templates` method to `DispatchingJinjaLoader`. - Add additional methods to the `Accept` datastructure, specifically keyed accessors. - Expand the `abort` functionality and signature, to allow for the `description` and `name` to be optionally specified. - Add a `make_push_promise` function, to allow for push promises to be sent at any time during the request handling e.g. pre-emptive pushes. - Rethink the Response Body structure to allow for more efficient handling of file bodies and the ability to extend how files are managed (for Quart-Trio and others). - Add the ability to send conditional 206 responses. Optionally, a response can be made conditional by awaiting the `make_conditional` method with an argument of the request range. - Recommend Mangum for serverless deployments. - Added `instance_path` and `instance_relative_config` to allow for an instance folder to be used. ## Version 0.6.12 Released 2019-01-29 - Raise a `BadRequest` if the body encoding is wrong. - Limit Hypercorn to versions < 0.6. - Fix matching of `MIMEAccept` values. - Handle the special routing case of `/`. - Ensure sync functions work with async signals. - Ensure redirect location headers are full URLs. - Ensure open-ended `Range` header works. - Ensure `RequestEntityTooLarge` errors are correctly raised. ## Version 0.7.2 Released 2019-01-03 - Fix the url display bug. - Avoid crash in `flask_patch` isinstance. - Cope with absolute paths sent in the scope. ## Version 0.7.1 Released 2018-12-18 - Fix Flask patching step definition. ## Version 0.7.0 Released 2018-12-16 - Support only Python 3.7, see the 0.6.X releases for continued Python 3.6 support. - Introduce `ContextVar` for local storage. - Change default redirect status code to 302. - Support integer/float cookie expires. - Specify cookie date format (differs to Flask). - Remove the Gunicorn workers, please use a ASGI server instead. - Remove Gunicorn compatibility. - Introduce a `Headers` data structure. - Implement `follow_redirects` in Quart test client. - Adopt the ASGI lifespan protocol. ## Version 0.6.11 Released 2018-12-09 - Support static files in blueprints. - Ensure automatic options API matches Flask and works. - Fix `app.run` SSL usage and Hypercorn compatibility. ## Version 0.6.10 Released 2018-11-12 - Fix async body iteration cleanup. ## Version 0.6.9 Released 2018-11-10 - Fix async body iteration deadlock. - Fix ASGI handling to ensure completion. ## Version 0.6.8 Released 2018-10-21 - Ensure an event loop is specified on `app.run`. - Ensure handler responses are finalized. - Ensure the ASGI callable returns on completion. ## Version 0.6.7 Released 2018-09-23 - Fix ASGI conversion of websocket data (str or bytes). - Ensure redirect url includes host when host matching. - Ensure query strings are present in redirect urls. - Ensure header values are string types. - Fix incorrect endpoint override error for synchronous view functions. ## Version 0.6.6 Released 2018-08-27 - Add type conversion to `getlist` (on multidicts) - Correct ASGI client usage (allows for `None`) - Ensure overlapping requests work without destroying the other contexts. - Ensure only integer status codes are accepted. ## Version 0.6.5 Released 2018-08-05 - Change default redirect status code to 302. - Support query string parsing from test client paths. - Support int/float cookie expires values. - Correct the cookie date format to RFC 822. - Copy `sys.modules` to prevent dictionary changed errors. - Ensure request body iteration returns all data. - Set `Host` header (if missing) for HTTP/1.0. - Set the correct defaults for `_external` in `url_for`. ## Version 0.6.4 Released 2018-07-15 - Correctly handle request query strings. - Restore log output when running in development mode. - Allow for multiple query string values when building urls, e.g. `a=1&a=2`. - Ensure the Flask Patch system works with Python 3.7. ## Version 0.6.3 Released 2018-07-05 - Ensure compatibility with Python 3.7 ## Version 0.6.2 Released 2018-06-24 - Remove class member patching from flask-patch system, as it was unreliable. - Ensure ASGI websocket handler closes on disconnect. - Cope with optional client values in ASGI scope. ## Version 0.6.1 Released 2018-06-18 - Accept `PathLike` objects to the `send_file` function. - Fix mutable methods in blueprint routes or url rule addition. - Don't lowercase header values. - Support automatic options on `View` classes. ## Version 0.6.0 Released 2018-06-11 - Quart is now an ASGI framework, and requires an ASGI server to serve requests. [Hypercorn](https://github.com/pgjones/hypercorn) is used in development and is recommended for production. Hypercorn is a continuation of the Quart serving code. - Add before and after serving functionality, this is provisional. - Add caching, last modified and etag information to static files served via `send_file`. - Fix date formatting in response headers. - `make_response` should error if response is `None`. - Deprecate the Gunicorn workers, see ASGI servers (e.g. Uvicorn). - Ensure shell context processors work. - Change template context processors to be async, this is backwards incompatible. - Change websocket API to be async, this is backwards incompatible. - Allow the websocket class to be configurable by users. - Catch signals on Windows. - Preserve context in Flask-Patch system. - Add the websocket API to blueprints. - Add host, subdomain, and default options to websocket routes. - Support `defaults` on `route` or `add_url_rule` usage. - Introduce a more useful `BuildError` - Match Flask after request function execution order. - Support `required_methods` on view functions. - Added CORS, Access Control, datastructures to request and response objects. - Allow type conversion in (CI)MultiDict get. ## Version 0.5.0 Released 2018-04-13 - Further API compatibility with Flask, specifically submodules, wrappers, and the app. - Ensure error handlers work. - Await `get_data` in Flask Patch system. - Fix rule building, specifically additional arguments as query strings. - Ability to add defaults to routes on definition. - Allow set_cookie to accept bytes arguments. - Ensure mimetype are returned. - Add host matching, and subdomains for routes. - Introduce implicit sequence conversion to response data. - URL and host information on requests. - Add a debug page, which shows tracebacks on errors. - Fix accept header parsing. - Cope with multi lists in forms. - Add cache control, etag and range header structures. - Add host, url, scheme and path correctly to path wrappers. - Fix CLI module parsing. - Add auto reloading on file changes. - Ignore invalid upgrade headers. - Fix h2c requests when there is a body (to not upgrade). - Refactor of websocket API, matching the request API as an analogue. - Refactor to mitigate DOS attacks, add documentation section. - Allow event loop to be specified when running apps. - Ensure automatic options work. - Rename `TestClient` -> `QuartClient` to match Flask naming. ## Version 0.4.1 Released 2018-01-27 - Fix HTTP/2 support and pass h2spec compliance testing. - Fix Websocket support and pass autobahn fuzzy test compliance testing. - Fix HEAD request support (don't try to send a body). - Fix content-type (remove forced override). ## Version 0.4.0 Released 2018-01-14 - Change to async signals and context management. This allows the signal receivers to be async (which is much more useful) but requires changes to any current usage (notably test contexts). - Add initial support of websockets. - Support HTTP/1.1 to HTTP/2 (h2c) upgrades, includes supporting HTTP/2 without SSL (note browsers don't support this). - Add timing to access logging. - Add a new logo :) thanks to @koddr. - Support streaming of the request body. - Add initial CLI support, using click. - Add context copying helper functions and clarify how to stream a response. - Improved tutorials. - Allow the request to be limited to prevent DOS attacks. ## Version 0.3.1 Released 2017-10-25 - Fix incorrect error message for HTTP/1.1 requests. - Fix HTTP/1.1 pipelining support and error handling. ## Version 0.3.0 Released 2017-10-10 - Change `flask_ext` name to `flask_patch` to clarify that it is not the pre-existing `flask_ext` system and that it patches Quart to provide Flask imports. - Added support for views. - Match Werkzeug API for FileStorage. - Support HTTP/2 pipelining. - Add access logging. - Add HTTP/2 Server push, see the `push_promises` set on a `Response` object. - Add idle timeouts. ## Version 0.2.0 Released 2017-07-22 *This is still an alpha version of Quart.* - Support for Flask extensions via the `flask_ext` module (if imported). - Initial documentation setup and actual documentation including API docstrings. - Closer match to the Flask API, most modules now match the Flask public API. ## Version 0.1.0 Released 2017-05-21 - Released initial pre alpha version. pallets-quart-6b318bd/CONTRIBUTING.md000066400000000000000000000074251524460054700171730ustar00rootroot00000000000000# Contributing Quick Reference This document assumes you have some familiarity with Git, GitHub, and Python virutalenvs. We are working on a more thorough guide about the different ways to contribute with in depth explanations, which will be available soon. These instructions will work with at least Bash and PowerShell, and should work on other shells. On Windows, use PowerShell, not CMD. You need Python and Git installed, as well as the [GitHub CLI]. Log in with `gh auth login`. Choose and install an editor; we suggest [PyCharm] or [VS Code]. [GitHub CLI]: https://cli.github.com/ [PyCharm]: https://www.jetbrains.com/pycharm/ [VS Code]: https://code.visualstudio.com/ ## Set Up the Repository Fork and clone the project's repository ("pallets/flask" for example). To work on a bug or documentation fix, switch to the "stable" branch (if the project has it), otherwise switch to the "main" branch. To update this target branch, pull from "upstream". Create a work branch with a short descriptive name. ``` $ gh repo fork --clone pallets/flask $ cd flask $ git switch stable $ git pull upstream $ git switch -c short-descriptive-name ``` ## Install Development Dependencies Create a virtualenv and activate it. Install the dev dependencies, and the project in editable mode. Install the pre-commit hooks. Create a virtualenv (Mac and Linux): ``` $ python3 -m venv .venv $ . .venv/bin/activate ``` Create a virtualenv (Windows): ``` > py -m venv .venv > .\.venv\Scripts\activate ``` Install (all platforms): ``` $ pip install -r requirements/dev.txt && pip install -e . $ pre-commit install --install-hooks ``` Any time you open a new terminal, you need to activate the virtualenv again. If you've pulled from upstream recently, you can re-run the `pip` command above to get the current dev dependencies. ## Run Tests These are the essential test commands you can run while developing: * `pytest` - Run the unit tests. * `mypy` - Run the main type checker. * `tox run -e docs` - Build the documentation. These are some more specific commands if you need them: * `tox parallel` - Run all test environments that will be run in CI, in parallel. Python versions that are not installed are skipped. * `pre-commit` - Run the linter and formatter tools. Only runs against changed files that have been staged with `git add -u`. This will run automatically before each commit. * `pre-commit run --all-files` - Run the pre-commit hooks against all files, including unchanged and unstaged. * `tox run -e py3.11` - Run unit tests with a specific Python version. The version must be installed. `-e pypy` will run against PyPy. * `pyright` - A second type checker. * `tox run -e typing` - Run all typing checks. This includes `pyright` and its export check as well. * `python -m http.server -b 127.0.0.1 -d docs/_build/html` - Serve the documentation. ## Create a Pull Request Make your changes and commit them. Add tests that demonstrate that your code works, and ensure all tests pass. Change documentation if needed to reflect your change. Adding a changelog entry is optional, a maintainer will write one if you're not sure how to. Add the entry to the end of the relevant section, match the writing and formatting style of existing entries. Don't add an entry for changes that only affect documentation or project internals. Use the GitHub CLI to start creating your pull request. Specify the target branch with `-B`. The "stable" branch is the target for bug and documentation fixes, otherwise the target is "main". ``` $ gh pr create --web --base stable ``` CI will run after you create the PR. If CI fails, you can click to see the logs and address those failures, pushing new commits. Once you feel your PR is ready, click the "Ready for review" button. A maintainer will review and merge the PR when they are available. pallets-quart-6b318bd/LICENSE.txt000066400000000000000000000020441524460054700165550ustar00rootroot00000000000000MIT License Copyright 2017 Pallets Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pallets-quart-6b318bd/README.md000066400000000000000000000052511524460054700162140ustar00rootroot00000000000000
# Quart Quart is an async Python web application framework. Using Quart you can, - render and serve HTML templates, - write (RESTful) JSON APIs, - serve WebSockets, - stream request and response data, - do pretty much anything over the HTTP or WebSocket protocols. ## Quickstart Install from PyPI using an installer such as pip. ``` $ pip install quart ``` Save the following as `app.py`. This shows off rendering a template, returning JSON data, and using a WebSocket. ```python from quart import Quart, render_template, websocket app = Quart(__name__) @app.route("/") async def hello(): return await render_template("index.html") @app.route("/api") async def json(): return {"hello": "world"} @app.websocket("/ws") async def ws(): while True: await websocket.send("hello") await websocket.send_json({"hello": "world"}) ``` ``` $ quart run * Running on http://127.0.0.1:5000 (CTRL + C to quit) ``` To deploy this app in a production setting see the [deployment] documentation. [deployment]: https://quart.palletsprojects.com/en/latest/tutorials/deployment.html ## Contributing Quart is developed on [GitHub]. If you come across a bug, or have a feature request, please open an [issue]. To contribute a fix or implement a feature, follow our [contributing guide]. [GitHub]: https://github.com/pallets/quart [issue]: https://github.com/pallets/quart/issues [contributing guide]: https://github.com/pallets/quart/blob/main/CONTRIBUTING.md ## Help If you need help with your code, the Quart [documentation] and [cheatsheet] are the best places to start. You can ask for help on the [Discussions tab] or on our [Discord chat]. [documentation]: https://quart.palletsprojects.com [cheatsheet]: https://quart.palletsprojects.com/en/latest/reference/cheatsheet.html [Discussions tab]: https://github.com/pallets/quart/discussions [Discord chat]: https://discord.gg ## Relationship with Flask Quart is an asyncio reimplementation of the popular [Flask] web application framework. This means that if you understand Flask you understand Quart. Like Flask, Quart has an ecosystem of extensions for more specific needs. In addition, a number of the Flask extensions work with Quart. [Flask]: https://flask.palletsprojects.com ### Migrating from Flask It should be possible to migrate to Quart from Flask by a find and replace of `flask` to `quart` and then adding `async` and `await` keywords. See the [migration] documentation for more help. [migration]: https://quart.palletsprojects.com/en/latest/how_to_guides/flask_migration.html pallets-quart-6b318bd/docs/000077500000000000000000000000001524460054700156625ustar00rootroot00000000000000pallets-quart-6b318bd/docs/Makefile000066400000000000000000000011361524460054700173230ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx SPHINXPROJ = Quart 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) pallets-quart-6b318bd/docs/_static/000077500000000000000000000000001524460054700173105ustar00rootroot00000000000000pallets-quart-6b318bd/docs/_static/quart-icon.svg000066400000000000000000000036121524460054700221150ustar00rootroot00000000000000 pallets-quart-6b318bd/docs/_static/quart-logo.svg000066400000000000000000000056371524460054700221360ustar00rootroot00000000000000 pallets-quart-6b318bd/docs/_static/quart-name-dark.svg000066400000000000000000000121651524460054700230270ustar00rootroot00000000000000 pallets-quart-6b318bd/docs/_static/quart-name.svg000066400000000000000000000120621524460054700221040ustar00rootroot00000000000000 pallets-quart-6b318bd/docs/changes.md000066400000000000000000000000521524460054700176110ustar00rootroot00000000000000# Changes ```{include} ../CHANGES.md ``` pallets-quart-6b318bd/docs/conf.py000066400000000000000000000033321524460054700171620ustar00rootroot00000000000000import importlib.metadata import os from sphinx.ext import apidoc # Project -------------------------------------------------------------- project = "Quart" copyright = "2017 Pallets" version = release = importlib.metadata.version("quart").partition(".dev")[0] # General -------------------------------------------------------------- default_role = "code" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "myst_parser", ] autodoc_member_order = "bysource" autodoc_typehints = "description" autodoc_preserve_defaults = True myst_enable_extensions = [ "fieldlist", ] myst_heading_anchors = 2 # HTML ----------------------------------------------------------------- html_theme = "pydata_sphinx_theme" html_theme_options = { "logo": {"text": "Quart"}, "external_links": [ {"name": "Source code", "url": "https://github.com/pallets/quart"}, {"name": "Issues", "url": "https://github.com/pallets/quart/issues"}, ], "icon_links": [ { "name": "Github", "url": "https://github.com/pallets/quart", "icon": "fab fa-github", }, ], } html_static_path = ["_static"] html_favicon = "_static/quart-icon.svg" html_logo = "_static/quart-logo.svg" def run_apidoc(_): # generate API documentation via sphinx-apidoc # https://www.sphinx-doc.org/en/master/man/sphinx-apidoc.html base_path = os.path.abspath(os.path.dirname(__file__)) apidoc.main( [ "-f", "-e", "-o", f"{base_path}/reference/source", f"{base_path}/../src/quart", f"{base_path}/../src/quart/datastructures.py", ] ) def setup(app): app.connect("builder-inited", run_apidoc) pallets-quart-6b318bd/docs/discussion/000077500000000000000000000000001524460054700200455ustar00rootroot00000000000000pallets-quart-6b318bd/docs/discussion/async_compatibility.rst000066400000000000000000000044231524460054700246500ustar00rootroot00000000000000.. _async_compatibility: Async compatibility =================== Synchronous and asynchronous code are not directly compatible in that the functions must be called differently depending on the type. This limits what can be done, for example in how Quart interacts with Flask extensions and any effort to make Flask directly asynchronous. In my opinion it is much easier to start with an asynchronous codebase that calls synchronous code than vice versa in Python. I will try and reason why below. Calling sync code from async functions -------------------------------------- This is mostly easy in that you can either call, or via a simple wrapper await a synchronous function, .. code-block:: python async def example(): sync_call() await asyncio.coroutine(sync_call)() whilst this doesn't actually change the nature, the call is synchronous, it does work. Calling async code from sync functions -------------------------------------- This is where things get difficult, as it is only possible to create a single event loop. Hence this can only be used once, .. code-block:: python def example(): loop = asyncio.get_event_loop() loop.run_until_complete(async_call()) therefore if you are not at the very outer scope it isn't really possible to call asynchronous code from a synchronous function. This is problematic when dealing with Flask extensions as for example the extension may have something like, .. code-block:: python @app.route('/') def route(): data = request.form return render_template_string("{{ name }}", name=data['name']) whilst the route function can be wrapped with the ``asyncio.coroutine`` function and hence awaited, there is no (easy?) way to insert the ``await`` before the ``request.form`` and ``render_template`` calls. It is for this reason that Quart-Flask-Patch creates sync wrapped versions for the Flask extensions. The former adding synchronous request methods and the other providing synchronous functions. Quart monkey patches a ``sync_wait`` method onto the base event loop allowing for definitions such as, .. code-block:: python from quart.templating import render_template as quart_render_template def render_template(*args): return asyncio.sync_wait(quart_render_template(*args)) pallets-quart-6b318bd/docs/discussion/background_tasks.rst000066400000000000000000000007321524460054700241250ustar00rootroot00000000000000.. _background_task_discussion: Background tasks ================ The API for background tasks follows Sanic and Starlette by taking a callable and arguments. However, Quart will ensure that the tasks finish during the shutdown (unless the server times out and cancels). This is as you'd hope in a production environment. Errors raised in a background task are logged but otherwise ignored allowing the app to continue - much like with request/websocket handling errors. pallets-quart-6b318bd/docs/discussion/contexts.rst000066400000000000000000000053211524460054700224470ustar00rootroot00000000000000.. _contexts: Contexts ======== Quart, like Flask, has two contexts the *application context* and the *request context*. Both of these contexts exist per request and allow the global proxies ``current_app``, ``request``, etc... to be resolved. Note that these contexts are task local, and hence will not exist if a task is spawned by ``ensure_future`` or ``create_task``. The design principle of these contexts is that they are likely needed in all routes, and hence rather than pass these objects around they are made available via global proxies. This has its downsides, notably all the arguments relating to global variables. Therefore, it is recommended that these proxies are only used within routes so as to isolate the scope. Application Context ------------------- The application context is a reference point for any information that isn't specifically related to a request. This includes the app itself, the ``g`` global object and a ``url_adapter`` bound only to the app. The context is created and destroyed implicitly by the request context. Request Context --------------- The request context is a reference point for any information that is related to a request. This includes the request itself, a ``url_adapter`` bound to the request and the session. It is created and destroyed by the :func:`~quart.Quart.handle_request` method per request. Websocket Context ----------------- The websocket context is analogous to the request context, but is related only to websocket requests. It is created and destroyed by the :func:`~quart.Quart.handle_websocket_request` method per websocket connection. Tasks and contexts ------------------ Context is bound to a ContextVar and will be copied to tasks created from an existing task. To explicitly copy a context Quart provides the decorators :func:`~quart.ctx.copy_current_request_context` and :func:`copy_current_websocket_context` which can be used as so, .. code-block:: python @app.route('/') async def index(): @copy_current_request_context async def background_task(): method = request.method ... asyncio.ensure_future(background_task()) ... If you need to provide the ``request`` context in an asynchronous generator, use the :func:`quart.helpers.stream_with_context` decorator as discussed in :ref:`streaming_response`: .. code-block:: python @app.route('/') async def index(): @stream_with_context async def async_generator(): async for data in request.body: yield data await consume_data(async_generator()) ... .. note:: These decorators must be used within an existing context, hence the background task is defined as a nested function. pallets-quart-6b318bd/docs/discussion/design_choices.rst000066400000000000000000000005251524460054700235470ustar00rootroot00000000000000.. _design_choices: Design Choices ============== Coroutines or functions ----------------------- It is quite easy to call sync and trigger async execution from a coroutine and hard to trigger async execution from a function, see :ref:`async_compatibility`. For this reason coroutines are preferred even in cases where IO seems unlikely. pallets-quart-6b318bd/docs/discussion/dos_mitigations.rst000066400000000000000000000126171524460054700240020ustar00rootroot00000000000000.. _dos_mitigations: Denial Of Service mitigations ============================= There are multiple ways a client can overload a server and deny service to other clients. A simple example can simply be to call an expensive route at a rate high enough that the server's resources are exhausted. Whilst this could happen innocently (if a route was popular) it takes a lot of client resource, there are malicious methods to exhaust server resources. There are two attack methods mitigated and discussed here, the first aims to open as many connections to the server as possible without freeing them, thereby eventually exhausting all the connections and preventing other clients from connecting. As most request response cycles last milliseconds before the connection is closed the key is to somehow hold the connection open. The second aims to exhaust the server's memory and hence either slow the server to a crawl or kill the server application, thereby preventing the server from replying to other clients. The key here is to somehow make the server write a lot of information to memory. Inactive connection ------------------- This attack is of the first type and aims to exhaust the server's connections. It works by opening connections to the server and then doing nothing with the connection. A poorly configured server would simply wait for the client to do something therefore holding the connection open. It is up to the ASGI server to guard against this attack, typically via a configurable keep alive or timeout setting. Large request body ------------------ This attack is of the second type and aims to exhaust the server's memory by inviting it to receive a large request body (and hence write the body to memory). A poorly configured server would have no limit on the request body size and potentially allow a single request to exhaust the server. To mitigate this Quart limits the request body size to the value set in the application config['MAX_CONTENT_LENGTH']. Any request with a body larger than this limit will trigger a Request Entity Too Large, 413, response. The default value for MAX_CONTENT_LENGTH is 16 MB, which is chosen as it is the limit discussed in the Flask documentation. Technically this limit refers to the maximum amount of data Quart will allow in memory for a request body. This allows larger bodies to be received and consumed if desired. The key being that the data is consumed and not otherwise stored in memory. An example is, .. code-block:: python async def route(): async for data in request.body: # Do something with the data ... it is advisable to add a timeout within each chunk if streaming the request. Slow request body ----------------- This attack is of the first type and aims to exhaust the server's connections by inviting it to wait a long time for the request's body. A poorly configured server would wait indefinitely for the request body. To mitigate this Quart by default will not wait for the request body, allowing the route handler to response if possible, for example if the request does not have the correct authorisation. If the body is needed, then the application config['BODY_TIMEOUT'] defines the number of seconds to wait for the body to be completely received. If the timeout is exhausted Quart would respond with RequestTimeout, 408. The default value is 60 seconds, which is chosen as it is the typical default timeout value. Technically the timeout is only applied to the Response data methods (get_data, data, json, get_json, form, and files) and not the body attribute. This allows for known streaming requests to be consumed as desired, see :ref:`request_body`. No response consumption ----------------------- This attack is of the second type and aims to exhaust the server's memory by failing to consume the data sent to the client. This failure results in backpressure on the server that leads to the response being written to memory rather than the connection. A poorly configured server would ignore the backpressure and exhaust its memory. (Note this requires a route that responds with a lot of data, e.g. video streaming). It is up to the ASGI server to guard against this attack. Slow response consumption ------------------------- This attack is of the first type and aims to exhaust the server's connections by inviting the server to take a long time sending the response, for example by applying backpressure indefinitely. A poorly configured server would simply wait indefinitely trying to send the response. To mitigate this Quart will limit the time it will try to send the response to that set in the application config['RESPONSE_TIMEOUT']. If this time is exhausted the connection is closed abruptly. The default value is 60 seconds, which is chosen as it is the typical default timeout value. Technically this limit can be configured per response if desired, e.g. if there is a single route that returns large files. This is an example how, .. code-block:: python async def route(): response = await make_response(...) response.timeout = 120 return response Large websocket message ----------------------- This attack is of the second type and aims to exhaust the server's memory by inviting it to receive very large websocket messages. A poorly configured server would have no limit on the message size and potentially allow a single message to exhaust the server. It is up to the ASGI server to guard against the attack. pallets-quart-6b318bd/docs/discussion/flask_evolution.rst000066400000000000000000000014341524460054700240050ustar00rootroot00000000000000.. _flask_evolution: Flask evolution =============== (The author) sees Quart as an evolution of Flask, primarily to support asyncio and secondarily to support websockets and HTTP/2. These additions are designed following (the author's interpretation) of Flask's design choices. It is for this reason that the websocket context and global exist, rather than as an argument to the route handler. Omissions from the Flask API ---------------------------- There are parts of the Flask API that I've decided to either not implement, these are, request.stream ^^^^^^^^^^^^^^ The ``stream`` method present on Flask request instances allows the request body to be 'streamed' via a file like interface. In Quart :ref:`request_body` is done differently in order to make use of the ``async`` keyword. pallets-quart-6b318bd/docs/discussion/globals.rst000066400000000000000000000007521524460054700222260ustar00rootroot00000000000000.. _globals: Globals ======= As Quart follows the Flask API, it also has globals, specifically ``current_app, g, request, session``. These are globals as they are likely needed in all request handlers. Locals ------ As is rather confusing from a naming point of view, the globals are local to the task being executed. This is detailed from a contextual view in :ref:`contexts`. This is necessary to ensure that Quart can asynchronously handle many requests with a single global object. pallets-quart-6b318bd/docs/discussion/index.rst000066400000000000000000000004151524460054700217060ustar00rootroot00000000000000=========== Discussions =========== .. toctree:: :maxdepth: 1 async_compatibility.rst background_tasks.rst contexts.rst design_choices.rst dos_mitigations.rst flask_evolution.rst globals.rst python_versions.rst websockets_discussion.rst pallets-quart-6b318bd/docs/discussion/python_versions.rst000066400000000000000000000002331524460054700240460ustar00rootroot00000000000000.. _python_versions: Python version support ====================== The main branch and releases >= 0.22.0 onwards only support Python 3.13.0 or greater. pallets-quart-6b318bd/docs/discussion/websockets_discussion.rst000066400000000000000000000045121524460054700252150ustar00rootroot00000000000000.. _websockets_discussion: Websockets ========== Websockets start as a GET request that can either be upgraded to a websocket via a 101, switching protocols, response or any other response. The choice of what to do, or how to respond, is often not possible in other frameworks and is one of the motivating aims in Quart. In addition as websockets are very similar to requests, Quart aims to have analogues functionality between websockets and requests. Request analogues ----------------- Websockets are very similar to GET requests, to the extent that is was tempting to simply extend the Flask request API to include websocket functionality. This would likely cause surprise to users of Flask-Sockets or Flask-SocketIO which set the de-facto Flask standard. Therefore I decided to introduce the websocket functionality alongside the existing request functionality. As websockets are so similar to GET requests it makes sense to produce an analogue for all the functionality available for requests. For example. :meth:`~quart.app.Quart.before_request` and :meth:`~quart.app.Quart.before_websocket` and there is a *websocket context* alongside the *request context*. Response or Upgrade ------------------- The utility of being able to choose how to respond, or whether to upgrade, is best shown when considering authentication. In the example below a typical login_required decorator can be used to prevent unauthorised usage of the websocket. .. code-block:: python def login_required(func): @wraps(func) async def wrapper(*args, **kwargs): if websocket.authentication == (...): return await func(*args, **kwargs) else: abort(401) return wrapper @app.websocket('/ws') @login_required async def ws(): while True: await websocket.receive() ... Quart also allows for the acceptance response (101) to be manually sent via :meth:`~quart.wrappers.Websocket.accept` as this gives the framework user full control. .. note:: This functionality is only useable with ASGI servers that implement the ``Websocket Denial Response`` extension. If the server does not support this extension Quart will instruct the server to close the connection without a response. Hypercorn, the recommended ASGI server, supports this extension. pallets-quart-6b318bd/docs/how_to_guides/000077500000000000000000000000001524460054700205215ustar00rootroot00000000000000pallets-quart-6b318bd/docs/how_to_guides/background_tasks.rst000066400000000000000000000050761524460054700246070ustar00rootroot00000000000000.. _background_tasks: Background tasks ================ If you have a task to perform where the outcome or result isn't required you can utilise a background task to run it. Background tasks run concurrently with the route handlers etc, i.e. in the background. Background tasks are very useful when they contain actions that take a lot of time to complete, as they allow a response to be sent to the client whilst the task itself is carried out. Equally some tasks just don't need to be completed before the response is sent and instead can be done in the background. Background tasks in Quart are created via the ``add_background_task`` method: .. code-block:: python async def background_task(): ... @app.route('/jobs/', methods=['POST']) async def create_job(): app.add_background_task(background_task) return 'Success' @app.before_serving async def startup(): app.add_background_task(background_task) The background tasks will have access to the app context. The tasks will be awaited during shutdown to ensure they complete before the app shuts down. If your task does not complete within the config ``BACKGROUND_TASK_SHUTDOWN_TIMEOUT`` it will be cancelled. Note ``BACKGROUND_TASK_SHUTDOWN_TIMEOUT`` should ideally be less than any server shutdown timeout. Synchronous background tasks are supported and will run in a separate thread. .. warning:: As Quart is based on asyncio it will run on a single execution and switch between tasks as they become blocked on waiting on IO, if a task does not need to wait on IO it will instead block the event loop and Quart could become unresponsive. Additionally the task will consume the same CPU resources as the server and hence could slow the server. Testing background tasks ------------------------ To ensure that background tasks complete in tests utilise the ``test_app`` context manager. This will wait for any background tasks to complete before allowing the test to continue: .. code-block:: python async def test_tasks_complete(): async with app.test_app(): app.add_background_task(...) # Background task has completed here assert task_has_done_something Note when testing an app the ``test_client`` usage should be within the ``test_app`` context block. The background task coroutine function can be tested by creating an app context and await the function, .. code-block:: python async def test_background_task(): async with app.app_context(): await background_task() assert something_to_test pallets-quart-6b318bd/docs/how_to_guides/blueprints.rst000066400000000000000000000030351524460054700234430ustar00rootroot00000000000000.. _blueprints: Blueprints ========== Blueprints allow for modular code, they should be used whenever the routes start to span multiple modules. Blueprints, like the app can have template and static files, therefore a typical folder structure for a blueprint termed ``store`` would be, :: blueprints/ blueprints/store/__init__.py blueprints/store/templates/ blueprints/store/templates/index.html blueprints/store/static/ the ``__init__.py`` file should contain something like, .. code-block:: python from quart import Blueprint blueprint = Blueprint('store', __name__) @blueprint.route('/') async def index(): return await render_template('index.html') the endpoint is then identified as ``store.index`` for example when using ``url_for('store.index')``. Nested 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 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. pallets-quart-6b318bd/docs/how_to_guides/command_line.rst000066400000000000000000000033451524460054700237050ustar00rootroot00000000000000.. _command_line: Custom Command Line Commands ============================ The ``quart`` command can be customised by the app to add additional functionality. A very typical use case is to add a database initialisation command, .. code-block:: python import click @app.cli.command() def initdb(): click.echo('Database is migrating') ... which will then work as, .. code-block:: console $ quart initdb Database is migrating .. note:: Unlike Flask the Quart commands do not run within an app context, as click commands are synchronous rather than asynchronous. Asynchronous usage ------------------ The best way to use some asynchronous code in a custom command is to create an event loop and run it manually, for example, .. code-block:: python import asyncio @app.cli.command() def fetch_db_data(): result = asyncio.get_event_loop().run_until_complete(_fetch()) async def _fetch(): return await db.execute(...) Including a CLI Command in an extension or another module --------------------------------------------------------- To include CLI commands in a Quart extension or blueprint, register the methods in the "run" factory function .. code-block:: python from quart import Quart from my_extension import my_cli def create_app(): app = Quart(__name__) app = my_cli(app) return app And in your module or extension: .. code-block:: python import click def my_cli(app): # @click.option("--my-option") @app.cli.command("mycli") def my_cli_command(): print("quart ran this command") return app This can be run with: .. code-block:: console $ quart mycli $ quart ran this command pallets-quart-6b318bd/docs/how_to_guides/configuration.rst000066400000000000000000000044111524460054700241220ustar00rootroot00000000000000.. _configuration: Configuration ============= A common pattern is to store configuration values in the environment. Quart supports this via :meth:`~quart.Config.from_prefixed_env` which can be used to load environment variables into the configuration. Only environment variables starting with the prefix, default ``QUART_`` will be loaded. For example if the environment variable ``QUART_TESTING=true`` is set then, .. code-block:: python app = Quart(__name__) app.config.from_prefixed_env() assert app.config["TESTING"] is True Another common pattern for configuration loading is to use class inheritance to define common settings with production and development overrides, for example, .. code-block:: python class Config: DEBUG = False TESTING = False SECRET_KEY = 'secret' class Development(Config): DEBUG = True class Production(Config): SECRET_KEY = 'an actually secret key' This can then be loaded in say a ``create_app`` function, for example: .. code-block:: python def create_app(mode='Development'): """In production create as app = create_app('Production')""" app = Quart(__name__) app.config.from_object(f"config.{mode}") return app Custom configuration class -------------------------- The :attr:`~quart.Quart.config_class` can be set to a custom class, however it must be changed before the app is initialised as the :meth:`~quart.Quart.make_config` is called on construction. Instance folders ---------------- An instance folder is a deployment specific location to store files and configuration settings. As opposed to loading files relative to the app root path :meth:`~quart.Quart.open_resource` you can load files relative to an instance path :meth:`~quart.Quart.open_instance_resource` including the configuration. To load the configuration from this folder, instead of relative to the app root path simply provide the ``instance_relative_config`` argument as ``True`` when initialising the app ``app = Quart(__name__, instance_relative_config=True)``. The instance path can be specified when initialising the app, or found automatically if it exists. The search locations are:: /app.py /instance/ or if the app has been installed:: $PREFIX/var/app-instance/ pallets-quart-6b318bd/docs/how_to_guides/developing.rst000066400000000000000000000055011524460054700234100ustar00rootroot00000000000000.. _developing: Developing with Quart ===================== When developing it is best to have your Quart app running so you can test any changes you make directly. This is made easier by the reloader which reloads your app whenever a file is changed. The reloader is active if you use ``app.run()``, ``quart run`` command or ``run_task`` method. Quart run --------- The ``quart run`` command is the recommended way to develop with Quart and will run whichever app is specified by the ``QUART_APP`` environment variable. For example, .. code-block:: python :caption: run.py from quart import Quart app = Quart(__name__) ... .. code-block:: console $ QUART_APP=run:app quart run The ``quart run`` command comes with ``--host``, and ``--port`` to specify where the app is served, and ``--certfile`` and ``--keyfile`` to specify the SSL certificates to use. app.run() --------- The Quart class, instances typically named ``app``, has a :meth:`~quart.Quart.run` method. This method runs a development server, automatically turning on debug mode and code reloading. This can be used to run the app via this snippet, .. code-block:: python :caption: run.py from quart import Quart app = Quart(__name__) ... if __name__ == "__main__": app.run() with the ``if`` ensuring that this code only runs if the file is run directly, i.e. .. code-block:: console $ python run.py which ensures that it doesn't run in production. The :meth:`~quart.Quart.run` method has options to set the ``host``, and ``port`` the app will be served over, to turn off the reloader via ``use_reloader=False``, and to add specify SSL certificates via the ``certfile`` and ``keyfile`` options. .. note:: The :meth:`~quart.Quart.run` method will create a new event loop, use ``run_task`` instead if you wish to control the event loop. app.run_task ------------ The Quart class also has a :meth:`~quart.Quart.run_task` method with the same options as the :meth:`~quart.Quart.run` method. The ``run_task`` returns an asyncio task that when awaited will run the app. This is as useful as it makes no alterations to the event loop. The ``run_task`` can be used as so, .. code-block:: python :caption: run.py import asyncio from quart import Quart app = Quart(__name__) ... if __name__ == "__main__": asyncio.run(app.run_task()) with the ``if`` ensuring that this code only runs if the file is run directly, i.e. .. code-block:: console $ python run.py which ensures that it doesn't run in production. Curl ---- To test the app locally I like to use a web browser, and the curl command line tool. I'd recommend reading the curl `documentation `_ and always using the ``-v``, ``--verbose`` option. For example, .. code-block:: console $ curl -v localhost:5000/ pallets-quart-6b318bd/docs/how_to_guides/disconnections.rst000066400000000000000000000021511524460054700242740ustar00rootroot00000000000000.. _detecting_disconnection: Detecting disconnection ======================= If in your route or websocket handler (or code called from within it) you are awaiting and the client disconnects the await will raise a ``CancelledError``. This can be used to detect when a client disconnects, to allow for cleanup, for example the sse handler from the :ref:`broadcast_tutorial` uses this to remove clients on disconnect, .. code-block:: python @app.route('/sse') async def sse(): queue = asyncio.Queue() app.clients.add(queue) async def send_events(): while True: try: data = await queue.get() event = ServerSentEvent(data) yield event.encode() except asyncio.CancelledError: app.clients.remove(queue) or with only the relevant parts, .. code-block:: python @app.route('/sse') async def sse(): try: await ... except asyncio.CancelledError: # Has disconnected The same applies for WebSockets, streaming the request, etc... pallets-quart-6b318bd/docs/how_to_guides/event_loop.rst000066400000000000000000000037311524460054700234310ustar00rootroot00000000000000.. _event_loop: Customise the Event Loop ======================== Customising the event loop is often desired in order to use Quart with another library whilst ensuring both use the same loop. The best practice is to create/initialise the third party within the loop created by Quart, by using :ref:`startup_shutdown` ``before_serving`` functions as so, .. code-block:: python @app.before_serving async def startup(): loop = asyncio.get_event_loop() app.smtp_server = loop.create_server(aiosmtpd.smtp.SMTP, port=1025) loop.create_task(app.smtp_server) @app.after_serving async def shutdown(): app.smtp_server.close() Do not follow this pattern, typically seen in examples, because this creates a new loop separate from the Quart loop for ThirdParty, .. code-block:: python loop = asyncio.get_event_loop() third_party = ThirdParty(loop) app.run() # A new loop is created by default Controlling the event loop -------------------------- It is the ASGI server running Quart that owns the event loop that Quart runs within, by default the server is Hypercorn. Both Quart and Hypercorn allow the loop to be specified, the Quart shortcut in development is to pass the loop to the ``app.run`` method, .. code-block:: python loop = asyncio.get_event_loop() third_party = ThirdParty(loop) app.run(loop=loop) or to use the ``app.run_task`` method, .. code-block:: python loop = asyncio.get_event_loop() third_party = ThirdParty(loop) loop.run_until_complete(app.run_task()) the Hypercorn (production) solution is to utilise the `Hypercorn API `_ to do the following, .. code-block:: python from hypercorn.asyncio import serve from hypercorn.config import Config ... loop = asyncio.get_event_loop() third_party = ThirdParty(loop) loop.run_until_complete(serve(app, Config())) # or even await serve(app, config) pallets-quart-6b318bd/docs/how_to_guides/flask_extensions.rst000066400000000000000000000006541524460054700246370ustar00rootroot00000000000000.. _flask_extensions: Using Flask Extensions ====================== Some Flask extensions can be used with Quart by patching Quart to act as Flask, to patch Quart see the `Quart-Flask-Patch `_ extension. This was part of Quart until release 0.19.0. Reference --------- More information about Flask extensions can be found `here `_. pallets-quart-6b318bd/docs/how_to_guides/flask_migration.rst000066400000000000000000000047401524460054700244310ustar00rootroot00000000000000.. _flask_migration: Migration from Flask ==================== As Quart is compatible with the Flask public API it should be relatively straight forward to migrate to Quart from Flask. This migration basically consists of two steps, firstly replacing Flask imports with Quart imports and secondly inserting the relevant ``async`` and ``await`` keywords. Import changes -------------- Any import of a module from the flask package can be changed to be an import from the same module in the quart package. For example the following in Flask, .. code-block:: python from flask import Flask, g, request from flask.helpers import make_response becomes in Quart, .. code-block:: python from quart import Quart, g, request from quart.helpers import make_response noting that the imported objects have the same name in both packages except for the ``Quart`` and ``Flask`` classes themselves. This can largely be automated via the use of find and replace. Async and Await --------------- As Quart is an asynchronous framework based on asyncio, it is necessary to explicitly add ``async`` and ``await`` keywords. The most notable place in which to do this is route functions, for example the following in Flask, .. code-block:: python @app.route('/') def route(): data = request.get_json() return render_template_string("Hello {{name}}", name=data['name']) becomes in Quart, .. code-block:: python @app.route('/') async def route(): data = await request.get_json() return await render_template_string("Hello {{name}}", name=data['name']) If you have sufficient test coverage it is possible to search for awaitables by searching for ``RuntimeWarning: coroutine 'XX' was never awaited``. The following common lines require awaiting, note that these must be awaited in functions/methods that are async. Awaiting in a non-async function/method is a syntax error. .. code-block:: python await request.data await request.get_data() await request.json await request.get_json() await request.form await request.files await render_template() await render_template_string() Testing ------- The test client also requires the usage of async and await keywords, mostly to await test requests i.e. .. code-block:: python await test_client.get('/') await test_client.post('/') await test_client.open('/', 'PUT') Extensions ---------- To use a Flask extension with Quart see the :ref:`flask_extensions` documentation. pallets-quart-6b318bd/docs/how_to_guides/index.rst000066400000000000000000000010611524460054700223600ustar00rootroot00000000000000============= How to guides ============= .. toctree:: :maxdepth: 1 background_tasks.rst blueprints.rst command_line.rst configuration.rst developing.rst disconnections.rst event_loop.rst flask_extensions.rst flask_migration.rst json_encoding.rst logging.rst middleware.rst request_body.rst routing.rst server_sent_events.rst session_storage.rst startup_shutdown.rst streaming_response.rst sync_code.rst templating.rst testing.rst quart_extensions.rst using_http2.rst websockets.rst pallets-quart-6b318bd/docs/how_to_guides/json_encoding.rst000066400000000000000000000033541524460054700240770ustar00rootroot00000000000000.. _json_encoding: JSON Encoding ============= It is often useful to be able to control how objects are encoded to and decoded from JSON. Quart makes this possible via a JSONProvider :class:`~quart.json.provider.JSONProvider`. Money example ------------- As an example lets consider a Money object, .. code-block:: python class Money: def __init__(self, amount: Decimal, currency: str) -> None: self.amount = amount self.currency = currency which we desire to translate to JSON as, .. code-block:: json { "amount": "10.00", "currency": "GBP" } using encoders and decoders as so, .. code-block:: python from quart.json.provider import _default, DefaultJSONProvider class MoneyJSONProvider(DefaultJSONProvider): @staticmethod def default(object_): if isinstance(object_, date): return http_date(object_) if isinstance(object_, (Decimal, UUID)): return str(object_) if is_dataclass(object_): return asdict(object_) if hasattr(object_, "__html__"): return str(object_.__html__()) if isinstance(object_, Money): return {'amount': object_.amount, 'currency': object_.currency} raise TypeError(f"Object of type {type(object_).__name__} is not JSON serializable") @staticmethod def dict_to_object(dict_): if 'amount' in dict_ and 'currency' in dict_: return Money(Decimal(dict_['amount']), dict_['currency']) else: return dict_ def loads(self, object_, **kwargs): return super().loads(object_, object_hook=self.dict_to_object, **kwargs) pallets-quart-6b318bd/docs/how_to_guides/logging.rst000066400000000000000000000021111524460054700226740ustar00rootroot00000000000000.. _how_to_log: Logging ======= Quart has a standard Python logger sharing the same name as the ``app.name``. To use it, simply make use of :attr:`~quart.app.Quart.logger`, for example: .. code-block:: python app.logger.info('Interesting') app.logger.warning('Easy Now') Configuration ------------- The Quart logger is not created until its first usage, which may occur as the app is created. These loggers on creation respect any existing configuration. This allows the loggers to be configured like any other python logger, for example .. code-block:: python from logging.config import dictConfig dictConfig({ 'version': 1, 'loggers': { 'quart.app': { 'level': 'ERROR', }, }, }) Disabling/removing handlers --------------------------- The handler :attr:`~quart.logging.default_handler` attached to the quart logger can be removed like so, .. code-block:: python from logging import getLogger from quart.logging import default_handler getLogger(app.name).removeHandler(default_handler) pallets-quart-6b318bd/docs/how_to_guides/middleware.rst000066400000000000000000000034211524460054700233700ustar00rootroot00000000000000.. _middleware: Middleware ========== Middleware can be used to wrap a Quart app instance and alter the ASGI process. A very simple example would be to reject requests based on the presence of a header, .. code-block:: python class RejectMiddleware: def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): if "headers" not in scope: return await self.app(scope, receive, send) for header, value in scope['headers']: if header.lower() == b'x-secret' and value == b'very-secret': return await self.app(scope, receive, send) return await self.error_response(receive, send) async def error_response(self, receive, send): await send({ 'type': 'http.response.start', 'status': 401, 'headers': [(b'content-length', b'0')], }) await send({ 'type': 'http.response.body', 'body': b'', 'more_body': False, }) Whilst middleware can always be used as a wrapper around the app instance, it is best to assign to and wrap the ``asgi_app`` attribute, .. code-block:: python quart_app.asgi_app = RejectMiddleware(quart_app.asgi_app) as this ensures that the middleware is applied in any test code. You can combine multiple middleware wrappers, .. code-block:: python quart_app.asgi_app = RejectMiddleware(quart_app.asgi_app) quart_app.asgi_app = AdditionalMiddleware(quart_app.asgi_app) and use any ASGI middleware. .. warning:: Middleware runs before any Quart code, which means that if the middleware returns a response no Quart functionality nor any Quart extensions will run. pallets-quart-6b318bd/docs/how_to_guides/quart_extensions.rst000066400000000000000000000067341524460054700247000ustar00rootroot00000000000000.. _quart_extensions: Using Quart Extensions ====================== There are a number of extensions for Quart, some of which are listed here, - `Quart-Auth `_ Secure cookie sessions, allows login, authentication and logout. - `Quart-Babel `_ Implements i18n and l10n support for Quart. - `Quart-Bcrypt `_ Provides bcrypt hashing utilities for your application. - `Quart-compress `_ compress your application's responses with gzip. - `Quart-compress2 `_ A package to compress responses in your Quart app with gzip . - `Quart-CORS `_ Cross Origin Resource Sharing (access control) support. - `Quart-DB `_ Managed connection(s) to postgresql database(s). - `Quart-events `_ event broadcasting via WebSockets or SSE. - `Quart-Login `_ a port of Flask-Login to work natively with Quart. - `Quart-minify `_ minify quart response for HTML, JS, CSS and less. - `Quart-Mongo `_ Bridges Quart, Motor, and Odmantic to create a powerful MongoDB extension. - `Quart-Motor `_ Motor (MongoDB) support for Quart applications. - `Quart-OpenApi `_ RESTful API building. - `Quart-Keycloak `_ Support for Keycloak's OAuth2 OpenID Connect (OIDC). - `Quart-Rapidoc `_ API documentation from OpenAPI Specification. - `Quart-Rate-Limiter `_ Rate limiting support. - `Quart-Redis `_ Redis connection handling - `Webargs-Quart `_ Webargs parsing for Quart. - `Quart-SqlAlchemy `_ Quart-SQLAlchemy provides a simple wrapper for SQLAlchemy. - `Quart-WTF `_ Simple integration of Quart and WTForms. Including CSRF and file uploading. - `Quart-Schema `_ Schema validation and auto-generated API documentation. - `Quart-session `_ server side session support. - `Quart-LibreTranslate `_ Simple integration to use LibreTranslate with your Quart app. - `Quart-Uploads `_ File upload handling for Quart. `Quart-Enciphers `_ Encrypted cookie session using enciphers. Supporting sync code in a Quart Extension ----------------------------------------- Extension authors can support sync functions by utilising the :meth:`quart.Quart.ensure_async` method. For example, if the extension provides a view function decorator add ``ensure_async`` before calling the decorated function, .. code-block:: python def extension(func): @wraps(func) async def wrapper(*args, **kwargs): ... # Extension logic return await current_app.ensure_async(func)(*args, **kwargs) return wrapper pallets-quart-6b318bd/docs/how_to_guides/request_body.rst000066400000000000000000000052111524460054700237570ustar00rootroot00000000000000.. _request_body: Consuming the request body ========================== Requests can come with a body, for example for a POST request the body can include form encoded data from a webpage or JSON encoded data from a client. The body is sent after the request line and headers for both HTTP/1 and HTTP/2. This allows Quart to trigger the app's request handling code before the full body has been received. Additionally the requester can choose to stream the request body, especially if the body is large as is often the case when sending files. Quart follows Flask and provides methods to await the entire body before continuing, .. code-block:: python @app.route('/', methods=['POST']) async def index(): await request.get_data() Advanced usage -------------- You may wish to completely control how the request body is consumed, most likely to consume the data as it is received. To do this quart provides methods to iterate over the body, .. code-block:: python from async_timeout import timeout @app.route('/', methods=['POST']) async def index(): async with timeout(app.config['BODY_TIMEOUT']): async for data in request.body: ... .. note:: The above snippet uses `Async-Timeout `_ to ensure the body is received within the timeout specified. .. warning:: Whilst the other request methods and attributes for accessing the body will timeout if the client takes to long send the request. Usage of :attr:`~quart.wrappers.request.Request.body` will not and it is up to you to wrap usage in a timeout. .. warning:: Iterating over the body consumes the data, so any further usage of the data is not possible unless it is saved during the iteration. Testing ------- To test that a route consumes the body iteratively you will need to use the :meth:`~quart.testing.client.QuartClient.request` method, .. code-block:: python async def test_stream() -> None: test_client = app.test_client() async with test_client.request(...) as connection: await connection.send(b"data") await connection.send_complete() response = await connection.as_response() assert response ... it also makes sense to check the code cleans up as you expect if the client disconnects whilst the request body is being sent, .. code-block:: python async def test_stream_closed() -> None: test_client = app.test_client() async with test_client.request(...) as connection: await connection.send(b"partial") await connection.disconnect() # Check cleanup markers.... pallets-quart-6b318bd/docs/how_to_guides/routing.rst000066400000000000000000000062351524460054700227500ustar00rootroot00000000000000.. _routing: Routing ======= Quart allows for multiple and complex routes to be defined, allowing a client to trigger specific code depending on the method and path requested. The simplest routing is simple static rules, such as the following, .. code-block:: python @app.route('/') async def index(): ... @app.route('/about') async def about(): ... which is often sufficient for mostly static websites. Dynamic routing --------------- Dynamic routing can be achieved by using ```` markers which specify that part of the route can be matched rather than pre-defined. For example, .. code-block:: python @app.route('/page/') async def page(page_no): ... will match paths ``/page/1``, ``/page/2``, and ``/page/jeff`` with the ``page_no`` argument set to ``'1'``, ``'2'``, and ``'jeff'`` respectively. Converters ^^^^^^^^^^ It is often necessary and useful to specify how the variable should convert and by implication match paths. This works by adding the converter name before the variable name separated by a colon, ````. Adapting the example above to, .. code-block:: python @app.route('/page/') async def page(page_no): ... will match paths ``/page/1``, and ``/page/2`` with the ``page_no`` argument set to ``1``, and ``2`` (note types) but will no longer match ``/page/jeff`` as ``jeff`` cannot be converted to an int. The available converters are, ========== ========================================== ``float`` positive floating point numbers ``int`` positive integers ``path`` like ``string`` with slashes ``string`` (default) any text without a slash ``uuid`` UUID strings ========== ========================================== note that additional converters can be added to the :attr:`~quart.app.Quart.url_map` :attr:`~quart.routing.Map.converters` dictionary. Catch all route ^^^^^^^^^^^^^^^ A ``/`` route definition will catch all requests that do not match any other routes. Default values -------------- Variable usage can sometimes prove annoying to users, for example ``/page/`` will not match ``/page`` forcing the user to specify ``/page/1``. This can be solved by specifying a default value, .. code-block:: python @app.route('/page', defaults={'page_no': 1}) @app.route('/page/') async def page(page_no): ... which allows ``/page`` to match with ``page_no`` set to ``1``. Host matching, host and subdomain --------------------------------- Routes can be added to the app with an explicit ``host`` or ``subdomain`` to match if the app has host matching enabled. This results in the routes only matching if the host header matches, for example ``host='quart.com'`` will allow the route to match any request with a host header of ``quart.com`` and otherwise 404. The ``subdomain`` option can only be used if the app config ``SERVER_NAME`` is set, as the host will be built up as ``{subdomain}.{SERVER_NAME}``. Note that the variable converters can be used in the host or subdomain options. See also -------- Quart uses `Werkzeug's router `_ pallets-quart-6b318bd/docs/how_to_guides/server_sent_events.rst000066400000000000000000000046321524460054700252030ustar00rootroot00000000000000.. _server_sent_events: Server Sent Events ================== Quart supports streaming `Server Sent Events `_, which you may decide to use as an alternative to WebSockets - especially if the communication is one way. Server sent events must be encoded in a specific way, as shown by this helper class: .. code-block:: python from dataclasses import dataclass @dataclass class ServerSentEvent: data: str event: str | None = None id: int | None = None retry: int | None = None def encode(self) -> bytes: message = f"data: {self.data}" if self.event is not None: message = f"{message}\nevent: {self.event}" if self.id is not None: message = f"{message}\nid: {self.id}" if self.retry is not None: message = f"{message}\nretry: {self.retry}" message = f"{message}\n\n" return message.encode('utf-8') To use a GET route that returns a streaming generator is required. This generator, ``send_events`` in the code below, must yield the encoded Server Sent Event. The route itself also needs to check the client will accept ``text/event-stream`` responses and set the response headers appropriately: .. code-block:: python from quart import abort, make_response, request @app.get("/sse") async def sse(): if "text/event-stream" not in request.accept_mimetypes: abort(400) async def send_events(): while True: data = ... # Up to you where the events are from event = ServerSentEvent(data) yield event.encode() response = await make_response( send_events(), { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Transfer-Encoding': 'chunked', }, ) response.timeout = None return response Quart by default will timeout long responses to protect against possible denial of service attacks, see :ref:`dos_mitigations`. For this reason the timeout is disabled. This can be done globally, however that could make other routes DOS vulnerable, therefore the recommendation is to set the timeout attribute on the specific response to ``None``. See also -------- :ref:`streaming_response` pallets-quart-6b318bd/docs/how_to_guides/session_storage.rst000066400000000000000000000036101524460054700244620ustar00rootroot00000000000000.. _session_storage: Session Storage =============== It is often useful to store information relevant to a user of the app for the duration of that usage session. For example the user may choose to want to save a option or be remembered as logged in. This information can either be stored client side or server side and Quart provides a system to store the information client side via Secure Cookie Sessions. Secure Cookie Sessions ---------------------- Secure Cookie Sessions store the session information on the Cookie in plain text with a signature to ensure that the information is not altered by the client. They can be used in Quart so long as the :attr:`~quart.app.Quart.secret_key` is set to a **secret** value. An example usage to store a users colour preference would be, .. code-block:: python from quart import session ... @app.route('/') async def index(): return await render_template( 'index.html', colour=session.get('colour', 'black'), ) @app.route('/colour/', methods=['POST']) async def set_colour(): ... session['colour'] = colour return redirect(url_for('index')) Permanent Sessions ------------------ The cookies used by default are not set to be permanent (deleted when the browser's session ends) to have permanent cookies ``session.permanent`` must be ``True`` when the session is modified. To set this as the default use this snippet, .. code-block:: python @app.before_request def make_session_permanent(): session.permanent = True WebSockets ---------- Sessions can be used with WebSockets with an important caveat about cookies. A cookie can only be set on a HTTP response, and an accepted WebSocket connection cannot return a HTTP response. Therefore the default implementation, being based on cookies, will lose any modifications made during an accepted WebSocket connection. pallets-quart-6b318bd/docs/how_to_guides/startup_shutdown.rst000066400000000000000000000056511524460054700247170ustar00rootroot00000000000000.. _startup_shutdown: Startup and Shutdown ==================== The `ASGI lifespan specification`_ includes the ability for awaiting coroutines before the first byte is received and after the final byte is sent, through the ``startup`` and ``shutdown`` lifespan events. This is particularly useful for creating and destroying connection pools. Quart supports this via the decorators :func:`~quart.app.Quart.before_serving`, :func:`~quart.app.Quart.after_serving`, and :func:`~quart.app.Quart.while_serving` which expects a function that returns a generator. .. _ASGI lifespan specification: https://github.com/django/asgiref/blob/master/specs/lifespan.rst The decorated functions are all called within the app context, allowing ``current_app`` and ``g`` to be used. .. warning:: Use ``g`` with caution, as it will reset after startup, i.e. after all the ``before_serving`` functions complete and after the initial yield in a while serving generator. It can still be used within this context. If you want to create something used in routes, try storing it on the app instead. To use this functionality simply do the following: .. code-block:: python @app.before_serving async def create_db_pool(): app.db_pool = await ... g.something = something @app.before_serving async def use_g(): g.something.do_something() @app.while_serving async def lifespan(): ... # startup yield ... # shutdown @app.route("/") async def index(): app.db_pool.execute(...) # g.something is not available here @app.after_serving async def create_db_pool(): await app.db_pool.close() Invocation order ---------------- The before serving functions will be called first in the order they are registered. Each before serving coroutine will be awaited before the next is started. After the before serving functions have been awaited the while serving generators will be awaited in registration order. After serving the while serving generators will be awaited in reverse registration order. Then the after serving functions will be awaited in the order they were registered. Testing ------- Quart's test client works on a request lifespan and hence does not call ``before_serving``, or ``after_serving`` functions, nor advance the ``while_serving`` generator. Instead Quart's test app can be used, for example .. code-block:: python @pytest_asyncio.fixture(name="app", scope="function") async def _app(): app = create_app() # Initialize app async with app.test_app() as test_app: yield test_app The app fixture can then be used as normal, knowing that the ``before_serving``, and ``after_serving`` functions have been called, and the ``while_serving`` generator has been advanced, .. code-block:: python async def test_index(app): test_client = app.test_client() await test_client.get("/") ... pallets-quart-6b318bd/docs/how_to_guides/streaming_response.rst000066400000000000000000000051321524460054700251630ustar00rootroot00000000000000.. _streaming_response: Streaming responses =================== Quart supports responses that are meant to be streamed to the client, rather than received in one block. If you are interested in streaming the request data see :ref:`request_body` or for duplex streaming see :ref:`websockets`. To stream a response the view-function should return an asynchronous generator that yields bytes. This generator can be returned with a status code and headers as normal. For example to stream the time every second, .. code-block:: python @app.route('/') async def stream_time(): async def async_generator(): time = datetime.isoformat() yield time.encode() return async_generator(), 200, {'X-Something': 'value'} With context ------------ If you want to make use of the ``request`` context whilst streaming you will need to use the :func:`quart.helpers.stream_with_context` decorator, .. code-block:: python @app.route('/') async def stream_time(): @stream_with_context async def async_generator(): time = datetime.isoformat() yield time.encode() return async_generator(), 200, {'X-Something': 'value'} Timeout ------- Quart by default will timeout long responses to protect against possible denial of service attacks, see :ref:`dos_mitigations`. This may be undesired for streaming responses, e.g. an indefinite stream. The timeout can be disabled globally, however this could make other routes DOS vulnerable, therefore the recommendation is to set the timeout attribute on a specific response to ``None``, .. code-block:: python from quart import make_response @app.route('/sse') async def stream_time(): ... response = await make_response(async_generator()) response.timeout = None # No timeout for this route return response Testing ------- The test client :meth:`~quart.testing.client.QuartClient.get` and associated methods will collate the entire streamed response. If you want to test that the route actually streams the response, or to test routes that stream until the client disconnects you will need to use the :meth:`~quart.testing.client.QuartClient.request` method, .. code-block:: python async def test_stream() -> None: test_client = app.test_client() async with test_client.request(..) as connection: data = await connection.receive() assert data ... assert connection.status_code == 200 ... await connection.disconnect() # For infinite streams See also -------- :ref:`server_sent_events` pallets-quart-6b318bd/docs/how_to_guides/sync_code.rst000066400000000000000000000052701524460054700232250ustar00rootroot00000000000000.. _sync_code: Run synchronous code ==================== Synchronous code will block the event loop and degrade the performance of the Quart application it is run in. This is because the synchronous code will block the task it is run in and in addition block the event loop. It is for this reason that synchronous code is best avoided, with asynchronous versions used in preference. It is likely though that you will need to use a third party library that is synchronous as there is no asynchronous version to use in its place. In this situation it is common to run the synchronous code in a thread pool executor so that it doesn't block the event loop and hence degrade the performance of the Quart application. This can be a bit tricky to do, so Quart provides some helpers to do so. Firstly any synchronous route will be run in an executor, i.e. .. code-block:: python @app.route("/") def sync(): method = request.method ... will result in the sync function being run in a thread. Note that you are still within the :ref:`contexts`, and hence you can still access the ``request``, ``current_app`` and other globals. The following functionality accepts synchronous functions and will run them in a thread, - Route handlers - Endpoint handlers - Error handlers - Context processors - Before request - Before websocket - Before first request - Before serving - After request - After websocket - After serving - Teardown request - Teardown websocket - Teardown app context - Open session - Make null session - Save session Context usage ------------- Whilst you can access the ``request`` and other globals in synchronous routes you will be unable to await coroutine functions. To work around this Quart provides :meth:`~quart.app.Quart.run_sync` which can be used as so, .. code-block:: python @app.route("/") async def sync_within(): data = await request.get_json() def sync_processor(): # does something with data ... result = await run_sync(sync_processor)() return result this is similar to utilising the asyncio run_in_executor function, .. code-block:: python @app.route("/") async def sync_within(): data = await request.get_json() def sync_processor(): # does something with data ... result = await asyncio.get_running_loop().run_in_executor( None, sync_processor ) return result .. note:: The run_in_executor function does not copy the current context, whereas the run_sync method does. It is for this reason that the latter is recommended. Without the copied context the ``request`` and other globals will not be accessible. pallets-quart-6b318bd/docs/how_to_guides/templating.rst000066400000000000000000000060411524460054700234200ustar00rootroot00000000000000.. _templating: Templates ========= Quart uses the `Jinja `_ templating engine, which is well `documented `_. Quart adds a standard context, and some standard filters to the Jinja defaults. Quart also adds the ability to define custom filters, tests and contexts at an app and blueprint level. There are two functions to use when templating, :func:`~quart.templating.render_template` and :func:`~quart.templating.render_template_string`, both must be awaited. The return value from either function is a string and can form a route response directly or be otherwise combined. Both functions take an variable number of additional keyword arguments to pass to the template as context, for example, .. code-block:: python @app.route('/') async def index(): return await render_template('index.html', hello='world') Quart standard extras --------------------- The standard context includes the ``config``, ``request``, ``session``, and ``g`` with these objects referencing the ``current_app.config`` and those defined in :mod:`~quart.globals` respectively. The can be accessed as expected, .. code-block:: python @app.route('/') async def index(): return await render_template_string("{{ request.endpoint }}") The standard global functions are :func:`~quart.helpers.url_for` and :func:`~quart.helpers.get_flashed_messages`. These can be used as expected, .. code-block:: python @app.route('/') async def index(): return await render_template_string(" None: test_client = app.test_client() async with test_client.request(...) as connection: await connection.send(b"data") await connection.send_complete() ... # receive a chunk of the response data = await connection.receive() ... # assemble the rest of the response without the first bit response = await connection.as_response() To learn more about streaming requests and responses, read :ref:`request_body` and :ref:`streaming_response`. Context testing --------------- It is often necessary to test something within the app or request contexts. This is simple enough for the app context, .. code-block:: python async def test_app_context(app): async with app.app_context(): current_app.[use] for the request context however the request context has to be faked, at a minimum this means the method and path must be supplied, e.g. .. code-block:: python async def test_app_context(app): async with app.test_request_context("/", method="GET"): request.[use] .. note:: Any ``before_request`` or ``after_request`` functions are not called when using the ``test_request_context``. You can add ``await app.preprocess_request()`` to ensure the ``before_request`` functions are called. .. code-block:: python async def test_app_context(app): async with app.test_request_context("/", method="GET"): await app.preprocess_request() # The before_request functions have now been called request.[use] pallets-quart-6b318bd/docs/how_to_guides/using_http2.rst000066400000000000000000000052531524460054700235260ustar00rootroot00000000000000.. _using_http2: Using HTTP/2 ============ `HTTP/2 `__ is the second major version of the Hyper Text Transfer Protocol used to transfer web data. .. note:: Not all ASGI Servers support HTTP/2. The recommended ASGI server, Hypercorn, does. To use HTTP/2 in development you will need to create some SSL certificates and run Quart with SSL. Server push or push promises ---------------------------- With `HTTP/2 `__ the server can choose to pre-emptively push additional responses to the client, this is termed a server push and the response itself is called a push promise. Server push is very useful when the server knows the client will likely initiate a request, say for the css or js referenced in a html response. .. note:: Browsers are deprecating support for server push, and usage is not recommended. This section is kept for reference. In Quart server push can be initiated during a request via the function :func:`~quart.helpers.make_push_promise`, for example, .. code-block:: python async def index(): await make_push_promise(url_for('static', filename='css/minimal.css')) return await render_template('index.html') The push promise will include (copy) header values present in the request that triggers the push promise. These are to ensure that the push promise is responded too as if the request had made it. A good example is the ``Accept`` header. The full set of copied headers are ``SERVER_PUSH_HEADERS_TO_COPY`` in the request module. .. note:: This functionality is only useable with ASGI servers that implement the ``HTTP/2 Server Push`` extension. If the server does not support this extension Quart will ignore the push promises (as with HTTP/1 connections). Hypercorn, the recommended ASGI server, supports this extension. When testing server push,the :class:`~quart.testing.QuartClient` ``push_promises`` list will contain every push promise as a tuple of the path and headers, for example, .. code-block:: python async def test_push_promise(): test_client = app.test_client() await test_client.get("/push") assert test_client.push_promises[0] == ("/", {}) HTTP/2 clients -------------- At the time of writing there aren't that many HTTP/2 clients. The best option is to use a browser and inspect the network connections (turn on the protocol information). Otherwise curl can be used, if HTTP/2 support is `installed `_, as so, .. code-block:: console $ curl --http2 ... If you wish to communicate via HTTP/2 in python `httpx `_ is the best choice. pallets-quart-6b318bd/docs/how_to_guides/websockets.rst000066400000000000000000000107321524460054700234270ustar00rootroot00000000000000.. _websockets: Using websockets ================ To use a websocket declare a websocket function rather than a route function, like so, .. code-block:: python @app.websocket('/ws') async def ws(): while True: data = await websocket.receive() await websocket.send(data) ``websocket`` is a global like ``request`` and shares many of the same attributes such as ``headers``. Manually rejecting or accepting websockets ------------------------------------------ A websocket connection is created by accepting a HTTP upgrade request, however a server can choose to reject a websocket request. To do so just return from the websocket function as you would with a route function, .. code-block:: python @app.websocket('/ws') async def ws(): if ( websocket.authorization.username != USERNAME or websocket.authorization.password != PASSWORD ): return 'Invalid password', 403 # or abort(403) else: websocket.accept() # Automatically invoked by receive or send ... Sending and receiving independently ----------------------------------- The first example given requires the client to send a message for the server to respond. To send and receive independently requires independent tasks, .. code-block:: python async def sending(): while True: await websocket.send(...) async def receiving(): while True: data = await websocket.receive() ... @app.websocket('/ws') async def ws(): producer = asyncio.create_task(sending()) consumer = asyncio.create_task(receiving()) await asyncio.gather(producer, consumer) The gather line is critical, as without it the websocket function would return triggering Quart to send a HTTP response. Detecting disconnection ----------------------- When a client disconnects a ``CancelledError`` is raised, which can be caught to handle the disconnect, .. code-block:: python @app.websocket('/ws') async def ws(): try: while True: data = await websocket.receive() await websocket.send(data) except asyncio.CancelledError: # Handle disconnection here raise .. warning:: The ``CancelledError`` must be re-raised. Closing the connection ---------------------- An connection can be closed by awaiting the ``close`` method with the appropriate Websocket error code, .. code-block:: python @app.websocket('/ws') async def ws(): await websocket.accept() await websocket.close(1000) if the websocket is closed before it is accepted the server will respond with a 403 HTTP response. Testing websockets ------------------ To test a websocket route use the test_client like so, .. code-block:: python test_client = app.test_client() async with test_client.websocket('/ws/') as test_websocket: await test_websocket.send(data) result = await test_websocket.receive() If the websocket route returns a response the test_client will raise a :class:`~quart.testing.WebsocketResponseError` exception with a :attr:`~quart.testing.WebsocketResponseError.response` attribute. For example, .. code-block:: python test_client = app.test_client() try: async with test_client.websocket('/ws/') as test_websocket: await test_websocket.send(data) except WebsocketResponseError as error: assert error.response.status_code == 401 Sending and receiving Bytes or String ------------------------------------- The WebSocket protocol allows for either bytes or strings to be sent with a frame marker indicating which. The :meth:`~quart.wrappers.request.Websocket.receive` method will return either ``bytes`` or ``str`` depending on what the client sent i.e. if the client sent a string it will be returned from the method. Equally you can send bytes or strings. Mixing websocket and HTTP routes -------------------------------- Quart allows for a route to be defined both as for websockets and for http requests. This allows responses to be sent depending upon the type of request (WebSocket upgrade or not). As so, .. code-block:: python @app.route("/ws") async def http(): return "A HTTP request" @app.websocket("/ws") async def ws(): ... # Use the WebSocket If the http definition is absent Quart will respond with a 400, Bad Request, response for requests to the missing route (rather than a 404). pallets-quart-6b318bd/docs/index.rst000066400000000000000000000054701524460054700175310ustar00rootroot00000000000000:orphan: .. title:: Quart documentation .. image:: _static/quart-name.svg :class: only-light :height: 200px :align: center .. image:: _static/quart-name-dark.svg :class: only-dark :height: 200px :align: center Quart ===== Quart is a Fast Python web microframework. Using Quart you can, * write JSON APIs e.g. :ref:`a RESTful API`, * render and serve HTML e.g. :ref:`a blog`, * serve WebSockets e.g. :ref:`a simple chat`, * stream responses e.g. :ref:`serve video`, * all of the above in a single app, * or do pretty much anything over the HTTP or WebSocket protocols. With all of the above possible using asynchronous (asyncio) libraries/code or :ref:`synchronous` libraries/code. If you are, * new to Python then start by reading :ref:`installation` instructions, * new to Quart then try the :ref:`quickstart`, * new to asyncio see the :ref:`asyncio` guide, * migrating from Flask see :ref:`flask_migration`, * looking for a cheatsheet then look :ref:`here`. Quart is an asyncio reimplementation of the popular `Flask `_ microframework API. This means that if you understand Flask you understand Quart. See :ref:`flask_evolution` to learn more about how Quart builds on Flask. Like Flask Quart has an ecosystem of :ref:`extensions` for more specific needs. In addition a number of the Flask :ref:`extensions` work with Quart. Quart is developed on `Github `_. If you come across an issue, or have a feature request please open an `issue `_.If you want to contribute a fix or the feature-implementation please do (typo fixes welcome), by proposing a `merge request `_. If you want to ask for help try `on discord `_. .. note:: If you can't find documentation for what you are looking for here, remember that Quart is an implementation of the Flask API and hence the `Flask documentation `_ is a great source of help. Quart is also built on the `Jinja `_ template engine and the `Werkzeug `_ toolkit. The Flask documentation is so good that you may be better placed consulting it first then returning here to check how Quart differs. Tutorials --------- .. toctree:: :maxdepth: 2 tutorials/index.rst How to guides ------------- .. toctree:: :maxdepth: 2 how_to_guides/index.rst Discussion ---------- .. toctree:: :maxdepth: 2 discussion/index.rst References ---------- .. toctree:: :maxdepth: 2 reference/index license changes pallets-quart-6b318bd/docs/license.md000066400000000000000000000001201524460054700176170ustar00rootroot00000000000000MIT License =========== ```{literalinclude} ../LICENSE.txt :language: text ``` pallets-quart-6b318bd/docs/make.bat000066400000000000000000000014431524460054700172710ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=python -msphinx ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=Quart if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The Sphinx module was not found. Make sure you have Sphinx installed, echo.then set the SPHINXBUILD environment variable to point to the full echo.path of the 'sphinx-build' executable. Alternatively you may add the echo.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% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd pallets-quart-6b318bd/docs/reference/000077500000000000000000000000001524460054700176205ustar00rootroot00000000000000pallets-quart-6b318bd/docs/reference/api.rst000066400000000000000000000001501524460054700211170ustar00rootroot00000000000000API Reference ============= .. toctree:: :maxdepth: 2 :caption: Contents: source/modules.rst pallets-quart-6b318bd/docs/reference/cheatsheet.rst000066400000000000000000000053111524460054700224670ustar00rootroot00000000000000.. _cheatsheet: Cheatsheet ========== Basic App --------- .. code-block:: python from quart import Quart app = Quart(__name__) @app.route("/hello") async def hello(): return "Hello, World!" if __name__ == "__main__": app.run(debug=True) Routing ------- .. code-block:: python @app.route("/hello/") # example.com/hello/quart async def hello(name): return f"Hello, {name}!" Request Methods --------------- .. code-block:: python @app.route("/get") # GET Only by default @app.route("/get", methods=["GET", "POST"]) # GET and POST @app.route("/get", methods=["DELETE"]) # Just DELETE JSON Responses -------------- .. code-block:: python @app.route("/hello") async def hello(): return {"Hello": "World!"} Template Rendering ------------------ .. code-block:: python from quart import render_template @app.route("/hello") async def hello(): return await render_template("index.html") # Required to be in templates/ Configuration ------------- .. code-block:: python import json import tomllib app.config["VALUE"] = "something" app.config.from_file("filename.toml", tomllib.load) app.config.from_file("filename.json", json.load) Request ------- .. code-block:: python from quart import request @app.route("/hello") async def hello(): request.method request.url request.headers["X-Bob"] request.args.get("a") # Query string e.g. example.com/hello?a=2 await request.get_data() # Full raw body (await request.form)["name"] (await request.get_json())["key"] request.cookies.get("name") WebSocket --------- .. code-block:: python from quart import websocket @app.websocket("/ws") async def ws(): websocket.headers while True: try: data = await websocket.receive() await websocket.send(f"Echo {data}") except asyncio.CancelledError: # Handle disconnect raise Cookies ------- .. code-block:: python from quart import make_response @app.route("/hello") async def hello(): response = await make_response("Hello") response.set_cookie("name", "value") return response Abort ----- .. code-block:: python from quart import abort @app.route("/hello") async def hello(): abort(409) HTTP/2 & HTTP/3 Server Push --------------------------- .. code-block:: python from quart import make_push_promise, url_for @app.route("/hello") async def hello(): await make_push_promise(url_for('static', filename='css/minimal.css')) ... pallets-quart-6b318bd/docs/reference/index.rst000066400000000000000000000002171524460054700214610ustar00rootroot00000000000000========= Reference ========= .. toctree:: :maxdepth: 1 api.rst cheatsheet.rst logo.rst response_values.rst versioning.rst pallets-quart-6b318bd/docs/reference/logo.rst000066400000000000000000000017051524460054700213150ustar00rootroot00000000000000Logo ==== The Quart logo has been kindly provided by Vic Shóstak, @koddr, `Quart-Logo `_. The logo itself has a lot of meaning, as expressed by @koddr, This is looks like Flask fonts and give us to understand what's the quart means?, because 1 quart equal 0.9 litre (and we're assuming: full bottle on logo hold one litre, but it's not full — because hold a quart now). The bung on top of bottle makes it clear — this is production ready Python framework with speed more than Flask, but Flask-like — all in one pack!. Get, open (equal install) and use it! Unstable liquid's state in bottle give us to understand — this liquid is reactivity, like new async features on Python and HTTP/2. And some aggressive gradient's color of liquid say to us hey, I'm alive and can help you to improve yourself... just use me!. .. image:: ../_static/logo.png :alt: Quart logo pallets-quart-6b318bd/docs/reference/response_values.rst000066400000000000000000000051211524460054700235660ustar00rootroot00000000000000.. _response_values: Response Return Values ====================== Response functions can return a number of different types as the response value which will trigger different responses to the client. The possible direct returns are, Response Values --------------- str ^^^ .. code-block:: python return "Hello" return await render_template("index.html") A solitary string return indicates that you intend to return a string mimetype ``text/html``. The string will be encoded using the default :attr:`~quart.wrappers._BaseRequestResponse.charset`. dict ^^^^ .. code-block:: python return {"a": "b"} A solitary dict return indicates that you intend to return json, ``application/json``. The jsonify function will be used to encode the dictionary. list ^^^^ .. code-block:: python return ["a", "b"] A solitary list return indicates that you intend to return json, ``application/json``. The jsonify function will be used to encode the list. Response ^^^^^^^^ .. code-block:: python @app.route('/') async def route_func(): return Response("Hello") Returning a Response instance indicates that you know exactly what you wish to return. AsyncGenerator[bytes, None] ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python @app.route('/') async def route_func(): async def agen(): data = await something yield data return agen() Returning an async generator allows for the response to be streamed to the client, thereby lowing the peak memory usage, if combined with a ``Transfer-Encoding`` header with value ``chunked``. Generator[bytes, None, None] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python @app.route('/') async def route_func(): def gen(): yield data return gen() Returning an generator allows for the response to be streamed to the client, thereby lowing the peak memory usage, if combined with a ``Transfer-Encoding`` header with value ``chunked``. Combinations ------------ Any of the above Response Values can be combined, as described, Tuple[ResponseValue, int] ^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python @app.route('/') async def route_func(): return "Hello", 200 A tuple of a Response Value and a integer indicates that you intend to specify the status code. Tuple[str, int, Dict[str, str]] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python @app.route('/') async def route_func(): return "Hello", 200, {'X-Header': 'Value'} A tuple of a Response Value, integer and dictionary indicates that you intend to specify additional headers. pallets-quart-6b318bd/docs/reference/versioning.rst000066400000000000000000000014211524460054700225330ustar00rootroot00000000000000Versioning ========== Quart uses `Semantic Versioning `_ with the caveat that Quart has yet to reach it's first major release. This means there may be changes between minor versions that would be considered major version changes - these are avoided and indicated in the changelog. Long term support ----------------- Only the latest release is supported and hence likely to receive updates. No previous releases are supported. Please always upgrade to the latest Quart release where possible. Python versions --------------- Quart will support Python versions until they reach end of life. This may, is rare circumstances be achieved by bug fixes to a specific release branch i.e. no new features. This happended with Python 3.6 and the Quart 0.5.X releases. pallets-quart-6b318bd/docs/tutorials/000077500000000000000000000000001524460054700177105ustar00rootroot00000000000000pallets-quart-6b318bd/docs/tutorials/api_tutorial.rst000066400000000000000000000132321524460054700231370ustar00rootroot00000000000000.. _api_tutorial: Tutorial: Building a RESTful API ================================ In this tutorial we will build a RESTful JSON API. It will automatically generate OpenAPI documentation and validate request and response data. This tutorial is meant to serve as an introduction to building APIs in Quart. If you want to skip to the end the code is on `Github `_. 1: Creating the project ----------------------- We need to create a project for our api server, I like to use `Poetry `_ to do this. Poetry is installed via pip (or via `Brew `_): .. code-block:: console pip install poetry We can then use Poetry to create a new api project: .. code-block:: console poetry new --src api Our project can now be developed in the *api* directory, and all subsequent commands should be in run the *api* directory. 2: Adding the dependencies -------------------------- To start we only need Quart to build the api server, which we can install as a dependency of the project by running the following: .. code-block:: console poetry add quart Poetry will ensure that this dependency is present and the paths are correct by running: .. code-block:: console poetry install 3: Creating the app ------------------- We need a Quart app to be our web server, which is created by the following addition to *src/api/__init__.py*: .. code-block:: python :caption: src/api/__init__.py from quart import Quart app = Quart(__name__) def run() -> None: app.run() To make the app easy to run we can call the run method from a poetry script, by adding the following to *pyproject.toml*: .. code-block:: toml :caption: pyproject.toml [tool.poetry.scripts] start = "api:run" Which allows the following command to start the app: .. code-block:: console poetry run start 4: Adding simple JSON API ------------------------- To start we can add a route that reads the JSON sent to it and echos it back in a response by adding the following to *src/api/__init__.py*: .. code-block:: python :caption: src/api/__init__.py from quart import request @app.post("/echo") async def echo(): data = await request.get_json() return {"input": data, "extra": True} We can test this using curl: .. code-block:: console curl --json '{"hello": "world"}' http://localhost:5000/echo Which gives the following result: .. code-block:: console {"extra":true,"input":{"hello":"world"}} To be explicit any dictionary returned from a route handler will be returned in the response as JSON with the correct Content-Type header. If you want to return a top level array as the JSON response the ``jsonify`` function can be used as so: .. code-block:: python from quart import jsonify @app.get("/example") async def example(): return jsonify(["a", "b"]) 5: Adding schemas ----------------- Using the `Quart-Schema `_ extension we can define schemas to validate the request and response data. In addition Quart-Schema will then utilise these schemas to auto-generate OpenAPI (Swagger) documentation. To start we need to install quart-schema: .. code-block:: console poetry add quart-schema We can then add schemas for a Todo object by adding the following to *src/api/__init__.py*: .. code-block:: python :caption: src/api/__init__.py from dataclasses import dataclass from datetime import datetime from quart import Quart from quart_schema import QuartSchema, validate_request, validate_response app = Quart(__name__) QuartSchema(app) @dataclass class TodoIn: task: str due: datetime | None @dataclass class Todo(TodoIn): id: int @app.post("/todos/") @validate_request(TodoIn) @validate_response(Todo) async def create_todo(data: TodoIn) -> Todo: return Todo(id=1, task=data.task, due=data.due) The OpenAPI schema is then available at http://localhost:5000/openapi.json and the docs themselves at http://localhost:5000/docs. 6: Testing ---------- To test our app we need to check that the echo route returns the JSON data sent to it and the create_todo route creates a todo. This is done by adding the following to *tests/test_api.py*: .. code-block:: python :caption: tests/test_api.py from api import app, TodoIn async def test_echo() -> None: test_client = app.test_client() response = await test_client.post("/echo", json={"a": "b"}) data = await response.get_json() assert data == {"extra":True,"input":{"a":"b"}} async def test_create_todo() -> None: test_client = app.test_client() response = await test_client.post("/todos/", json=TodoIn(task="Abc", due=None)) data = await response.get_json() assert data == {"id": 1, "task": "Abc", "due": None} As the test is an async function we need to install `pytest-asyncio `_ by running the following: .. code-block:: console poetry add --dev pytest-asyncio Once installed it needs to be configured by adding the following to *pyproject.toml*: .. code-block:: toml [tool.pytest.ini_options] asyncio_mode = "auto" Finally we can run the tests via this command: .. code-block:: console poetry run pytest tests/ If you are running this in the Quart example folder you'll need to add a ``-c pyproject.toml`` option to prevent pytest from using the Quart pytest configuration. 7: Summary ---------- We've built a RESTful API server with autogenerated OpenAPI docs and validation. You can now take this code and build any API. pallets-quart-6b318bd/docs/tutorials/asyncio.rst000066400000000000000000000046031524460054700221120ustar00rootroot00000000000000.. _asyncio: Introduction to asyncio ======================= Asyncio is the part of the Python standard library that provides an event loop with IO (input/output) operations. It exists to allow concurrent programming in Python, whereby the event loop switches to another task whilst the previous task waits on IO. This concurrency allows for greater CPU utilisation and hence greater throughput performance. The easiest way to understand this is to consider something concrete, namely a demonstrative simulation In the following we fetch a url with a simulated IO delay, .. code-block:: python import asyncio async def simulated_fetch(url, delay): await asyncio.sleep(delay) print(f"Fetched {url} after {delay}") return f"{url}" def main(): loop = asyncio.get_event_loop() results = loop.run_until_complete(asyncio.gather( simulated_fetch('http://google.com', 2), simulated_fetch('http://bbc.co.uk', 1), )) print(results) you should see the following output, >>> Fetched http://bbc.co.uk after 1 >>> Fetched http://google.com after 2 >>> ['http://google.com', 'http://bbc.co.uk'] which indicates that despite calling the ``google.com`` fetch first, the ``bbc.co.uk`` actually completed first i.e. the code ran concurrently. Additionally the code runs in a little over 2 seconds rather than over 3 as expected with synchronous code. Relevance to web servers ------------------------ Web servers by definition do IO, in that they receive and respond to requests from the network. This means that asyncio is a very good fit even if the code within the framework does no IO itself. Yet in practice IO is present, for example when loading a template from a file, or contacting a database or another server. Common pitfalls --------------- It is very easy to await the wrong thing, for example, .. code-block:: python await awaitable.attribute will not await the ``awaitable`` object as you might expect, but rather attempt to resolve the attribute and then await it. This is quite commonly seen as, .. code-block:: python await request.form.get('key') which fails with an error that the coroutine wrapper has no get attribute. To work around this simply use brackets to indicate what must be awaited first, .. code-block:: python (await awaitable).attribute (await request.form).get('key') pallets-quart-6b318bd/docs/tutorials/blog_tutorial.rst000066400000000000000000000206461524460054700233200ustar00rootroot00000000000000.. _blog_tutorial: Tutorial: Building a simple blog ================================ In this tutorial we will build a simple blog with entries stored in a database. We'll then render these posts on the server and serve the HTML directly to the user. This tutorial is meant to serve as an introduction to building server rendered websites in Quart. If you want to skip to the end the code is on `Github `_. 1: Creating the project ----------------------- We need to create a project for our blog server, I like to use `Poetry `_ to do this. Poetry is installed via pip (or via `Brew `_): .. code-block:: console pip install poetry We can then use Poetry to create a new blog project: .. code-block:: console poetry new --src blog Our project can now be developed in the *blog* directory, and all subsequent commands should be in run the *blog* directory. 2: Adding the dependencies -------------------------- To start we only need Quart to build the blog server, which we can install as a dependency of the project by running the following: .. code-block:: console poetry add quart Poetry will ensure that this dependency is present and the paths are correct by running: .. code-block:: console poetry install 3: Creating the app ------------------- We need a Quart app to be our web server, which is created by the following addition to *src/blog/__init__.py*: .. code-block:: python :caption: src/blog/__init__.py from quart import Quart app = Quart(__name__) def run() -> None: app.run() To make the app easy to run we can call the run method from a poetry script, by adding the following to *pyproject.toml*: .. code-block:: toml :caption: pyproject.toml [tool.poetry.scripts] start = "blog:run" Which allows the following command to start the app: .. code-block:: console poetry run start 4: Creating the database ------------------------ There are many database management systems to choose from depending upon the needs and requirements. In this case we need only the simplest system, and Python’s standard library includes SQLite making it the easiest. To initialise the database we need the following SQL to create the correct table, as added to *src/blog/schema.sql*: .. code-block:: sql :caption: src/blog/schema.sql DROP TABLE IF EXISTS post; CREATE TABLE post ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, 'text' TEXT NOT NULL ); Next we need to be able to create the database on command, which we can do by adding the command code to *src/blog/__init__.py*: .. code-block:: python :caption: src/blog/__init__.py from pathlib import Path from sqlite3 import dbapi2 as sqlite3 app.config.update({ "DATABASE": Path(app.root_path) / "blog.db", }) def _connect_db(): engine = sqlite3.connect(app.config["DATABASE"]) engine.row_factory = sqlite3.Row return engine def init_db(): db = _connect_db() with open(Path(app.root_path) / "schema.sql", mode="r") as file_: db.cursor().executescript(file_.read()) db.commit() Next we need to update the poetry scripts in *pyproject.toml* to be: .. code-block:: toml :caption: pyproject.toml [tool.poetry.scripts] init_db = "blog:init_db" start = "blog:run" Now we can run the following to create and update the database: .. code-block:: console poetry run init_db .. warning:: Running this command will wipe any existing data. 5: Displaying posts in the database ----------------------------------- With can now display the posts present in the database. To do so we first need a template to render the posts as HTML. This is as follows and should be added to *src/blog/templates/posts.html*: .. code-block:: html :caption: src/blog/templates/posts.html
{% for post in posts %}

{{ post.title }}

{{ post.text|safe }}

{% else %}

No posts available

{% endfor %}
Now we need a route to query the database, retrieve the messages, and render the template. As done with the following code which should be added to *src/blog/__init__.py*: .. code-block:: python :caption: src/blog/__init__.py from quart import render_template, g def _get_db(): if not hasattr(g, "sqlite_db"): g.sqlite_db = _connect_db() return g.sqlite_db @app.get("/") async def posts(): db = _get_db() cur = db.execute( """SELECT title, text FROM post ORDER BY id DESC""", ) posts = cur.fetchall() return await render_template("posts.html", posts=posts) 6: Creating a new post ---------------------- To create blog posts we first need a form into which a user can enter the post details. This is done via the following template code that should be added to *src/blog/templates/create.html*: .. code-block:: html :caption: src/blog/templates/create.html
The styling ensures that the elements of the form are arranged verically with a gap and sensible maximum width. To allow a visitor to create a blog post we need to accept the POST request generated by this form in the browser. To do so the following should be added to *src/blog/__init__.py*: .. code-block:: python :caption: src/blog/__init__.py from quart import redirect, request, url_for @app.route("/create/", methods=["GET", "POST"]) async def create(): if request.method == "POST": db = _get_db() form = await request.form db.execute( "INSERT INTO post (title, text) VALUES (?, ?)", [form["title"], form["text"]], ) db.commit() return redirect(url_for("posts")) else: return await render_template("create.html") This route handler will render the creation form in response to a GET request e.g. via navigation in the browser. However, for a POST request it will extract the form data to create a blog post before redirecting the user to the page with the posts. 7: Testing ---------- To test our app we need to check that a blog post can be created, and once done so shows on the posts page. Firstly we need to create a temporary database for testing, which we can do using a pytest fixture placed in *tests/conftest.py*: .. code-block:: python :caption: tests/conftest.py import pytest from blog import app, init_db @pytest.fixture(autouse=True) def configure_db(tmpdir): app.config['DATABASE'] = str(tmpdir.join('blog.db')) init_db() This fixture will run automatically before our tests, thereby setting up a database we can use in the tests. To test the creation and display we can add the following to *tests/test_blog.py*: .. code-block:: python :caption: tests/test_blog.py from blog import app async def test_create_post(): test_client = app.test_client() response = await test_client.post("/create/", form={"title": "Post", "text": "Text"}) assert response.status_code == 302 response = await test_client.get("/") text = await response.get_data() assert b"

Post

" in text assert b"

Text

" in text As the test is an async function we need to install `pytest-asyncio `_ by running the following: .. code-block:: console poetry add --dev pytest-asyncio Once installed it needs to be configured by adding the following to *pyproject.toml*: .. code-block:: toml [tool.pytest.ini_options] asyncio_mode = "auto" Finally we can run the tests via this command: .. code-block:: console poetry run pytest tests/ If you are running this in the Quart example folder you'll need to add a ``-c pyproject.toml`` option to prevent pytest from using the Quart pytest configuration. 8: Summary ---------- We've built a simple database backed blog server. This should be a good starting point to building any type of server rendered app. pallets-quart-6b318bd/docs/tutorials/chat_tutorial.rst000066400000000000000000000166211524460054700233120ustar00rootroot00000000000000.. _chat_tutorial: Tutorial: Building a basic chat server ====================================== In this tutorial we will build a very basic chat server. It will allow anyone to send messages to everyone else currently connected to the server. This tutorial is meant to serve as an introduction to WebSockets in Quart. If you want to skip to the end the code is on `Github `_. 1: Creating the project ----------------------- We need to create a project for our chat server, I like to use `Poetry `_ to do this. Poetry is installed via pip (or via `Brew `_): .. code-block:: console pip install poetry We can then use Poetry to create a new chat project: .. code-block:: console poetry new --src chat Our project can now be developed in the *chat* directory, and all subsequent commands should be in run the *chat* directory. 2: Adding the dependencies -------------------------- We only need Quart to build this simple chat server, which we can install as a dependency of the project by running the following: .. code-block:: console poetry add quart Poetry will ensure that this dependency is present and the paths are correct by running: .. code-block:: console poetry install 3: Creating the app ------------------- We need a Quart app to be our web server, which is created by the following addition to *src/chat/__init__.py*: .. code-block:: python :caption: src/chat/__init__.py from quart import Quart app = Quart(__name__) def run() -> None: app.run() To make the app easy to run we can call the run method from a poetry script, by adding the following to *pyproject.toml*: .. code-block:: toml :caption: pyproject.toml [tool.poetry.scripts] start = "chat:run" Which allows the following command to start the app: .. code-block:: console poetry run start 4: Serving the UI ----------------- When users visit our chat website we will need to show them a UI which they can use to enter and receive messages. The following HTML template should be added to *src/chat/templates/index.html*: .. code-block:: html :caption: src/chat/templates/index.html
    This is a very basic UI both in terms of the styling, but also as there is no error handling for the WebSocket. We can now serve this template for the root path i.e. ``/`` by adding the following to *src/chat/__init__.py*: .. code-block:: python from quart import render_template @app.get("/") async def index(): return await render_template("index.html") 5: Building a broker -------------------- Before we can add the websocket route we need to be able to pass messages from one connected client to another. For this we will need a message-broker. To start we'll build our own in memory broker by adding the following to *src/chat/broker.py*: .. code-block:: python :caption: src/chat/broker.py import asyncio from typing import AsyncGenerator from quart import Quart class Broker: def __init__(self) -> None: self.connections = set() async def publish(self, message: str) -> None: for connection in self.connections: await connection.put(message) async def subscribe(self) -> AsyncGenerator[str, None]: connection = asyncio.Queue() self.connections.add(connection) try: while True: yield await connection.get() finally: self.connections.remove(connection) This ``Broker`` has a publish-subscribe pattern based interface, with clients expected to publish messages to other clients whilst subscribing to any messages sent. 6: Implementing the websocket ----------------------------- We can now implement the websocket route, by adding the following to *src/chat/__init__.py*: .. code-block:: python :caption: src/chat/__init__.py import asyncio from quart import websocket from chat.broker import Broker broker = Broker() async def _receive() -> None: while True: message = await websocket.receive() await broker.publish(message) @app.websocket("/ws") async def ws() -> None: try: task = asyncio.ensure_future(_receive()) async for message in broker.subscribe(): await websocket.send(message) finally: task.cancel() await task The ``_receive`` coroutine must run as a separate task to ensure that sending and receiving run concurrently. In addition this task must be properly cancelled and cleaned up. When the user disconnects a CancelledError will be raised breaking the while loops and triggering the finally blocks. 7: Testing ---------- To test our app we need to check that messages sent via the websocket route are echoed back. This is done by adding the following to *tests/test_chat.py*: .. code-block:: python :caption: tests/test_chat.py import asyncio from quart.testing.connections import TestWebsocketConnection as _TestWebsocketConnection from chat import app async def _receive(test_websocket: _TestWebsocketConnection) -> str: return await test_websocket.receive() async def test_websocket() -> None: test_client = app.test_client() async with test_client.websocket("/ws") as test_websocket: task = asyncio.ensure_future(_receive(test_websocket)) await test_websocket.send("message") result = await task assert result == "message" As the test is an async function we need to install `pytest-asyncio `_ by running the following: .. code-block:: console poetry add --dev pytest-asyncio Once installed it needs to be configured by adding the following to *pyproject.toml*: .. code-block:: toml [tool.pytest.ini_options] asyncio_mode = "auto" Finally we can run the tests via this command: .. code-block:: console poetry run pytest tests/ If you are running this in the Quart example folder you'll need to add a ``-c pyproject.toml`` option to prevent pytest from using the Quart pytest configuration. 8: Summary ---------- The message-broker we've built so far only works in memory, which means that messages are only shared with users connected to the same server instance. To share messages across server instances we need to use a third party broker, such as redis via the `aioredis `_ library which supports a pub/sub interface. pallets-quart-6b318bd/docs/tutorials/deployment.rst000066400000000000000000000032621524460054700226250ustar00rootroot00000000000000.. _deployment: Deploying Quart =============== It is not recommended to run Quart directly (via :meth:`~quart.app.Quart.run`) in production. Instead it is recommended that Quart be run using `Hypercorn `_ or an alternative ASGI server. This is because the :meth:`~quart.app.Quart.run` enables features that help development yet slow production performance. Hypercorn is installed with Quart and will be used to serve requests in development mode by default (e.g. with :meth:`~quart.app.Quart.run`). To use Quart with an ASGI server simply point the server at the Quart application, for example, .. code-block:: python :caption: example.py from quart import Quart app = Quart(__name__) @app.route('/') async def hello(): return 'Hello World' you can run with Hypercorn using, .. code-block:: bash hypercorn example:app See the `Hypercorn docs `_. Alternative ASGI Servers ------------------------ Alongside `Hypercorn `_, `Daphne `_, and `Uvicorn `_ are available ASGI servers that work with Quart. Serverless deployment --------------------- To deploy Quart in an AWS Lambda & API Gateway setting you will need to use a specialised ASGI function adapter. `Mangum `_ is recommended for this and can be as simple as, .. code-block:: python from mangum import Mangum from quart import Quart app = Quart(__name__) @app.route("/") async def index(): return "Hello, world!" handler = Mangum(app) # optionally set debug=True pallets-quart-6b318bd/docs/tutorials/index.rst000066400000000000000000000003301524460054700215450ustar00rootroot00000000000000========= Tutorials ========= .. toctree:: :maxdepth: 1 installation.rst quickstart.rst asyncio.rst api_tutorial.rst blog_tutorial.rst chat_tutorial.rst video_tutorial.rst deployment.rst pallets-quart-6b318bd/docs/tutorials/installation.rst000066400000000000000000000017741524460054700231540ustar00rootroot00000000000000.. _installation: Installation ============ Quart is only compatible with Python 3.13 or higher and can be installed using pip or your favorite python package manager: .. code-block:: console pip install quart Dependencies ------------ Quart dependends on the following packages, which will automatically be installed with Quart: - aiofiles, to load files in an asyncio compatible manner, - blinker, to manage signals, - click, to manage command line arguments - hypercorn, an ASGI server for development, - itsdangerous, for signing secure cookies, - jinja2, for template rendering, - markupsafe, for markup rendering, - werkzeug, as the basis of many Quart classes. You can choose to install with the dotenv extra: .. code-block:: console pip install quart[dotenv] Which will install the ``python-dotenv`` package which enables support for automatically loading environment variables when running ``quart`` commands. See also -------- `Poetry `_ for project management. pallets-quart-6b318bd/docs/tutorials/quickstart.rst000066400000000000000000000011271524460054700226350ustar00rootroot00000000000000.. _quickstart: Quickstart ========== Hello World ----------- A very simple app that simply returns a response containing ``hello`` is, (file ``hello-world.py``) .. code-block:: python from quart import Quart app = Quart(__name__) @app.route('/') async def hello(): return 'hello' app.run() and is simply run via .. code-block:: console python hello-world.py or alternatively .. code-block:: console $ export QUART_APP=hello-world:app $ quart run and tested by .. code-block:: sh curl localhost:5000 See also -------- :ref:`cheatsheet` pallets-quart-6b318bd/docs/tutorials/video_tutorial.rst000066400000000000000000000114401524460054700234730ustar00rootroot00000000000000.. _video_tutorial: Tutorial: Serving video ======================= In this tutorial we will build a very basic video server. It will serve a video directly. This tutorial is meant to serve as an introduction to serving large files with conditional responses in Quart. If you want to skip to the end the code is on `Github `_. 1: Creating the project ----------------------- We need to create a project for our video server, I like to use `Poetry `_ to do this. Poetry is installed via pip (or via `Brew `_): .. code-block:: console pip install poetry We can then use Poetry to create a new video project: .. code-block:: console poetry new --src video Our project can now be developed in the *video* directory, and all subsequent commands should be in run the *video* directory. 2: Adding the dependencies -------------------------- We only need Quart to build this simple video server, which we can install as a dependency of the project by running the following: .. code-block:: console poetry add quart Poetry will ensure that this dependency is present and the paths are correct by running: .. code-block:: console poetry install 3: Creating the app ------------------- We need a Quart app to be our web server, which is created by the following addition to *src/video/__init__.py*: .. code-block:: python :caption: src/video/__init__.py from quart import Quart app = Quart(__name__) def run() -> None: app.run() To make the app easy to run we can call the run method from a poetry script, by adding the following to *pyproject.toml*: .. code-block:: toml :caption: pyproject.toml [tool.poetry.scripts] start = "video:run" Which allows the following command to start the app: .. code-block:: console poetry run start 4: Serving the UI ----------------- When users visit our website we will show them the same video served directly, and via chunks. The following HTML template should be added to *src/video/templates/index.html*: .. code-block:: html :caption: src/video/templates/index.html This is a very basic UI in terms of the styling. We can now serve this template for the root path i.e. ``/`` by adding the following to *src/video/__init__.py*: .. code-block:: python from quart import render_template @app.get("/") async def index(): return await render_template("index.html") 5: Implementing the routes -------------------------- As we are serving a large file we should allow for conditional responses. This is where the data returned in the response is conditional on what the request asked for. This is done via the ``Range`` header field which can be inspected via the ``request.range`` attribute. Quart has in-built methods to make a response conditional on the request range. The first is to use the conditional argument when sending a file, the second is to use the response ``make_conditional`` method. The former is shown below, which should be added to *src/video/__init__.py*: .. code-block:: python :caption: src/video/__init__.py @app.route("/video.mp4") async def auto_video(): return await send_file(app.static_folder / "video.mp4", conditional=True) 6: Testing ---------- To test our app we need to check that the full video is returned unless a conditional range request is made. This is done by adding the following to *tests/test_video.py*: .. code-block:: python :caption: tests/test_video.py from video import app async def test_auto_video() -> None: test_client = app.test_client() response = await test_client.get("/video.mp4") data = await response.get_data() assert len(data) == 255_849 response = await test_client.get("/video.mp4", headers={"Range": "bytes=200-1000"}) data = await response.get_data() assert len(data) == 801 As the test is an async function we need to install `pytest-asyncio `_ by running the following: .. code-block:: console poetry add --dev pytest-asyncio Once installed it needs to be configured by adding the following to *pyproject.toml*: .. code-block:: toml [tool.pytest.ini_options] asyncio_mode = "auto" Finally we can run the tests via this command: .. code-block:: console poetry run pytest tests/ If you are running this in the Quart example folder you'll need to add a ``-c pyproject.toml`` option to prevent pytest from using the Quart pytest configuration. 7: Summary ---------- We've built a server that will serve large files conditionally as requested by the client, including the ability to limit the maximum partial size. pallets-quart-6b318bd/examples/000077500000000000000000000000001524460054700165505ustar00rootroot00000000000000pallets-quart-6b318bd/examples/api/000077500000000000000000000000001524460054700173215ustar00rootroot00000000000000pallets-quart-6b318bd/examples/api/README.rst000066400000000000000000000004721524460054700210130ustar00rootroot00000000000000Tutorial: Building a RESTful API ================================ This is the example code for the api tutorial. It is managed using `Poetry `_ with the following commands, .. code-block:: console poetry install poetry run start poetry run pytest -c pyproject.toml tests/ pallets-quart-6b318bd/examples/api/pyproject.toml000066400000000000000000000007031524460054700222350ustar00rootroot00000000000000[tool.poetry] name = "api" version = "0.1.0" description = "" authors = ["pgjones "] [tool.poetry.dependencies] python = "^3.10" quart = "*" quart-schema = "*" [tool.poetry.dev-dependencies] pytest = "*" pytest-asyncio = "^0.18.3" [tool.poetry.scripts] start = "api:run" [tool.pytest.ini_options] asyncio_mode = "auto" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" pallets-quart-6b318bd/examples/api/src/000077500000000000000000000000001524460054700201105ustar00rootroot00000000000000pallets-quart-6b318bd/examples/api/src/api/000077500000000000000000000000001524460054700206615ustar00rootroot00000000000000pallets-quart-6b318bd/examples/api/src/api/__init__.py000066400000000000000000000013521524460054700227730ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from datetime import datetime from quart_schema import QuartSchema from quart_schema import validate_request from quart_schema import validate_response from quart import Quart from quart import request app = Quart(__name__) QuartSchema(app) @app.post("/echo") async def echo(): data = await request.get_json() return {"input": data, "extra": True} @dataclass class TodoIn: task: str due: datetime | None @dataclass class Todo(TodoIn): id: int @app.post("/todos/") @validate_request(TodoIn) @validate_response(Todo) async def create_todo(data: Todo) -> Todo: return Todo(id=1, task=data.task, due=data.due) def run() -> None: app.run() pallets-quart-6b318bd/examples/api/tests/000077500000000000000000000000001524460054700204635ustar00rootroot00000000000000pallets-quart-6b318bd/examples/api/tests/__init__.py000066400000000000000000000000001524460054700225620ustar00rootroot00000000000000pallets-quart-6b318bd/examples/api/tests/test_api.py000066400000000000000000000010131524460054700226400ustar00rootroot00000000000000from api import app from api import TodoIn async def test_echo() -> None: test_client = app.test_client() response = await test_client.post("/echo", json={"a": "b"}) data = await response.get_json() assert data == {"extra": True, "input": {"a": "b"}} async def test_create_todo() -> None: test_client = app.test_client() response = await test_client.post("/todos/", json=TodoIn(task="Abc", due=None)) data = await response.get_json() assert data == {"id": 1, "task": "Abc", "due": None} pallets-quart-6b318bd/examples/blog/000077500000000000000000000000001524460054700174735ustar00rootroot00000000000000pallets-quart-6b318bd/examples/blog/README.rst000066400000000000000000000004731524460054700211660ustar00rootroot00000000000000Tutorial: Building a simple blog ================================ This is the example code for the blog tutorial. It is managed using `Poetry `_ with the following commands, .. code-block:: console poetry install poetry run start poetry run pytest -c pyproject.toml tests/ pallets-quart-6b318bd/examples/blog/pyproject.toml000066400000000000000000000007051524460054700224110ustar00rootroot00000000000000[tool.poetry] name = "blog" version = "0.1.0" description = "" authors = ["pgjones "] [tool.poetry.dependencies] python = "^3.10" quart = "*" [tool.poetry.dev-dependencies] pytest = "*" pytest-asyncio = "*" [tool.poetry.scripts] init_db = "blog:init_db" start = "blog:run" [tool.pytest.ini_options] asyncio_mode = "auto" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" pallets-quart-6b318bd/examples/blog/src/000077500000000000000000000000001524460054700202625ustar00rootroot00000000000000pallets-quart-6b318bd/examples/blog/src/blog/000077500000000000000000000000001524460054700212055ustar00rootroot00000000000000pallets-quart-6b318bd/examples/blog/src/blog/__init__.py000066400000000000000000000026401524460054700233200ustar00rootroot00000000000000from pathlib import Path from sqlite3 import dbapi2 as sqlite3 from quart import g from quart import Quart from quart import redirect from quart import render_template from quart import request from quart import url_for app = Quart(__name__) app.config.update( { "DATABASE": Path(app.root_path) / "blog.db", } ) def _connect_db(): engine = sqlite3.connect(app.config["DATABASE"]) engine.row_factory = sqlite3.Row return engine def _get_db(): if not hasattr(g, "sqlite_db"): g.sqlite_db = _connect_db() return g.sqlite_db @app.get("/") async def posts(): db = _get_db() cur = db.execute( """SELECT title, text FROM post ORDER BY id DESC""", ) posts = cur.fetchall() return await render_template("posts.html", posts=posts) @app.route("/create/", methods=["GET", "POST"]) async def create(): if request.method == "POST": db = _get_db() form = await request.form db.execute( "INSERT INTO post (title, text) VALUES (?, ?)", [form["title"], form["text"]], ) db.commit() return redirect(url_for("posts")) else: return await render_template("create.html") def init_db(): db = _connect_db() with open(Path(app.root_path) / "schema.sql") as file_: db.cursor().executescript(file_.read()) db.commit() def run() -> None: app.run() pallets-quart-6b318bd/examples/blog/src/blog/schema.sql000066400000000000000000000002271524460054700231670ustar00rootroot00000000000000DROP TABLE IF EXISTS post; CREATE TABLE post ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, 'text' TEXT NOT NULL ); pallets-quart-6b318bd/examples/blog/src/blog/templates/000077500000000000000000000000001524460054700232035ustar00rootroot00000000000000pallets-quart-6b318bd/examples/blog/src/blog/templates/create.html000066400000000000000000000004161524460054700253350ustar00rootroot00000000000000
    pallets-quart-6b318bd/examples/blog/src/blog/templates/posts.html000066400000000000000000000003021524460054700252340ustar00rootroot00000000000000
    {% for post in posts %}

    {{ post.title }}

    {{ post.text|safe }}

    {% else %}

    No posts available

    {% endfor %}
    pallets-quart-6b318bd/examples/blog/tests/000077500000000000000000000000001524460054700206355ustar00rootroot00000000000000pallets-quart-6b318bd/examples/blog/tests/__init__.py000066400000000000000000000000001524460054700227340ustar00rootroot00000000000000pallets-quart-6b318bd/examples/blog/tests/conftest.py000066400000000000000000000002751524460054700230400ustar00rootroot00000000000000import pytest from blog import app from blog import init_db @pytest.fixture(autouse=True) def configure_db(tmpdir): app.config["DATABASE"] = str(tmpdir.join("blog.db")) init_db() pallets-quart-6b318bd/examples/blog/tests/test_blog.py000066400000000000000000000005751524460054700232000ustar00rootroot00000000000000from blog import app async def test_create_post(): test_client = app.test_client() response = await test_client.post( "/create/", form={"title": "Post", "text": "Text"} ) assert response.status_code == 302 response = await test_client.get("/") text = await response.get_data() assert b"

    Post

    " in text assert b"

    Text

    " in text pallets-quart-6b318bd/examples/chat/000077500000000000000000000000001524460054700174675ustar00rootroot00000000000000pallets-quart-6b318bd/examples/chat/README.rst000066400000000000000000000005021524460054700211530ustar00rootroot00000000000000Tutorial: Building a chat server ================================ This is the example code for the chat server tutorial. It is managed using `Poetry `_ with the following commands, .. code-block:: console poetry install poetry run start poetry run pytest -c pyproject.toml tests/ pallets-quart-6b318bd/examples/chat/pyproject.toml000066400000000000000000000006541524460054700224100ustar00rootroot00000000000000[tool.poetry] name = "chat" version = "0.1.0" description = "" authors = ["pgjones "] [tool.poetry.dependencies] python = "^3.10" quart = "*" [tool.poetry.dev-dependencies] pytest = "*" pytest-asyncio = "*" [tool.poetry.scripts] start = "chat:run" [tool.pytest.ini_options] asyncio_mode = "auto" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" pallets-quart-6b318bd/examples/chat/src/000077500000000000000000000000001524460054700202565ustar00rootroot00000000000000pallets-quart-6b318bd/examples/chat/src/chat/000077500000000000000000000000001524460054700211755ustar00rootroot00000000000000pallets-quart-6b318bd/examples/chat/src/chat/__init__.py000066400000000000000000000012421524460054700233050ustar00rootroot00000000000000import asyncio from chat.broker import Broker from quart import Quart from quart import render_template from quart import websocket app = Quart(__name__) broker = Broker() @app.get("/") async def index(): return await render_template("index.html") async def _receive() -> None: while True: message = await websocket.receive() await broker.publish(message) @app.websocket("/ws") async def ws() -> None: try: task = asyncio.ensure_future(_receive()) async for message in broker.subscribe(): await websocket.send(message) finally: task.cancel() await task def run(): app.run(debug=True) pallets-quart-6b318bd/examples/chat/src/chat/broker.py000066400000000000000000000010551524460054700230340ustar00rootroot00000000000000import asyncio from collections.abc import AsyncGenerator class Broker: def __init__(self) -> None: self.connections = set() async def publish(self, message: str) -> None: for connection in self.connections: await connection.put(message) async def subscribe(self) -> AsyncGenerator[str]: connection = asyncio.Queue() self.connections.add(connection) try: while True: yield await connection.get() finally: self.connections.remove(connection) pallets-quart-6b318bd/examples/chat/src/chat/templates/000077500000000000000000000000001524460054700231735ustar00rootroot00000000000000pallets-quart-6b318bd/examples/chat/src/chat/templates/index.html000066400000000000000000000014311524460054700251670ustar00rootroot00000000000000
      pallets-quart-6b318bd/examples/chat/tests/000077500000000000000000000000001524460054700206315ustar00rootroot00000000000000pallets-quart-6b318bd/examples/chat/tests/__init__.py000066400000000000000000000000001524460054700227300ustar00rootroot00000000000000pallets-quart-6b318bd/examples/chat/tests/test_chat.py000066400000000000000000000010561524460054700231630ustar00rootroot00000000000000import asyncio from chat import app from quart.testing.connections import ( TestWebsocketConnection as _TestWebsocketConnection, ) async def _receive(test_websocket: _TestWebsocketConnection) -> str: return await test_websocket.receive() async def test_websocket() -> None: test_client = app.test_client() async with test_client.websocket("/ws") as test_websocket: task = asyncio.ensure_future(_receive(test_websocket)) await test_websocket.send("message") result = await task assert result == "message" pallets-quart-6b318bd/examples/video/000077500000000000000000000000001524460054700176565ustar00rootroot00000000000000pallets-quart-6b318bd/examples/video/README.rst000066400000000000000000000004611524460054700213460ustar00rootroot00000000000000Tutorial: Serving video ======================= This is the example code for the video server tutorial. It is managed using `Poetry `_ with the following commands, .. code-block:: console poetry install poetry run start poetry run pytest -c pyproject.toml tests/ pallets-quart-6b318bd/examples/video/pyproject.toml000066400000000000000000000006421524460054700225740ustar00rootroot00000000000000[tool.poetry] name = "video" version = "0.1.0" description = "" authors = ["pgjones "] [tool.poetry.dependencies] python = "^3.10" [tool.poetry.dev-dependencies] pytest = "*" pytest-asyncio = "*" [tool.poetry.scripts] start = "video:run" [tool.pytest.ini_options] asyncio_mode = "auto" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" pallets-quart-6b318bd/examples/video/src/000077500000000000000000000000001524460054700204455ustar00rootroot00000000000000pallets-quart-6b318bd/examples/video/src/video/000077500000000000000000000000001524460054700215535ustar00rootroot00000000000000pallets-quart-6b318bd/examples/video/src/video/__init__.py000066400000000000000000000005431524460054700236660ustar00rootroot00000000000000from quart import Quart from quart import render_template from quart import send_file app = Quart(__name__) @app.get("/") async def index(): return await render_template("index.html") @app.route("/video.mp4") async def auto_video(): return await send_file(app.static_folder / "video.mp4", conditional=True) def run() -> None: app.run() pallets-quart-6b318bd/examples/video/src/video/static/000077500000000000000000000000001524460054700230425ustar00rootroot00000000000000pallets-quart-6b318bd/examples/video/src/video/static/video.mp4000066400000000000000000007635511524460054700246130ustar00rootroot00000000000000 ftypisomisomiso2avc1mp41”moovlmvhdè+ @¥trak\tkhd+ @uUUb$edtselst+ mdia mdhd2(Ç-hdlrvideVideoHandler Èminfvmhd$dinfdref url  ˆstbl¬stsdœavc1vbHHÿÿ6avcCdÿágd¬Ù@ /æˆÿÀììDÈþ€§ÍF.ýáS’`÷1˜ñÚ)i‘ê›ârµ@?þ$}¸¦?$v7‹üÖ�•æ7Úß_ÝÒ¾oŠ8|€¾º˜ a».²¦-º¤š%ˆ<�pÃÖ2Å^¾üc\γ_‡8jÀSˆ*ž• �  T>>0=(ï55Ð`0{\C> × stco¼{udtasmeta!hdlrmdirapplFilst©daydata0%©toodataLavf57.71.100×µmdatäÿÿàÜE齿ÙH·–,Ø Ù#îïx264 - core 148 - H.264/MPEG-4 AVC codec - Copyleft 2003-2017 - http://www.videolan.org/x264.html - options: cabac=0 ref=2 deblock=1:0:0 analyse=0x3:0x111 me=hex subme=4 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=11 lookahead_threads=2 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=1 keyint=30 keyint_min=3 scenecut=40 intra_refresh=0 rc_lookahead=20 rc=cbr mbtree=1 bitrate=700 ratetol=1.0 qcomp=0.60 qpmin=10 qpmax=51 qpstep=4 vbv_maxrate=700 vbv_bufsize=700 nal_hrd=none filler=0 ip_ratio=1.40 aq=1:1.00€Ceˆ„ûÿþ¢€‚—Ž QÀKNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®½ÿÃw‡x${^ƒ ¤ªT™§ ‘ìj.¢ýà¹sWCþsó¿á*C‚€i€@ÂË`ψ˜×2uÀm01r¦JêXSâ*ÜDW±C Ü«0 ðú�%JWfU› $�G‰\§z¬j’�ß< €VÏ”S÷‚ÐÀÜmË ], € ðP@@) Fh•Û¡0àR6ã H†› ŽÿÒ ĤÔ¼õþ€#¦,�ð¾ñP EÎð=@¹äÉùð0X(chW.|½„0)fÓiªBû„$»ÿ t„¤?E¯5Jà´'\¹À€`"&P/½axÄ–à8ÅÛ­¾û,�x@€ÎXY–©ƒÊÁ8Á>#8/ôº…»!�ŠA¿¨‰ùª:ê°énå° Œm] x’lqÊ¥ôA Vß8¸|'âË`ŸýÙ:�÷ßþ l p€x܇ŠñýÏü F9yÂÐV7H!cîOü C«ŠÃê)u×]u×]u×]u×]u×]u×]u×]u×]{ÿòx.À1#œ¬#1/J•àâúx¬�®p±~ЮÓA–±)Á¿Ø 4Ð@x÷ û©€G…Ö÷pà, ›Œº$fë®îï�äÃ~ÿ\df€³!ÆD%�p¡G_bgË‹÷ö€ø¯Ž¸’ˆúK\lüÌÌÛÚ Z¬Rr¨Îþå$¶rQ}ø0ÂPÀ'Á€X(Ð =”TÏ9.À9P#f ¾Í%cPéŸ Vm¼$ÈQ´(ë´£`•® $Ý÷\5M¤=x‘†grãÿ†ºüÀë< G¡öÅ-VaõúÃá€ÀÁ�À¶? ánlòÏ$8?ƒß½«*€Ãµö´»Åô !åd.Ü �/Þ}:ð* Èæ^�øé–/+~�åZ®¿]€¢ŠPÞêˆiDal=@LU³´ýÐc[µŸX/Ê5�w¼ÿ€R�ÕZs —}æi™PŸ| °a€ð&€PAˆŸ˜rÀ{‡üÁZ÷�1A¦{ùtk#˜¿LTå Øeþ`W–À•@“ÄI?ÞÍ#&%Êߥþ©\ì;AkF.9wHÕA–öHz ïF±E‡KïÒq’Z‹½ €�€€@|ÀpH;€¸w@'èé§‘Èí~�!@WHñO×€?4ôäÓI뮺뮺뮺뮺뮺뮺뮺ëßÿ‘AvnvÑ�M<î € Ž(WÑFä�ÖÞH„Mˆ’[Â`, 9ÀÅj‡Òм*C÷îªå½ˆ@|µõ"ù�À¡ü ©[g›b¼È8í�*æŸïÿú"$J¡×ý´$Ãv)tÔ­þ B"EFÏúA‡m€€�(¨ó§çP‹÷î{fó6fÛÏ¿ÿÿÿ�€‚uz!¬Á©HÝÏm¼ÏfÌûëÿÿðÍÆ5u×]uï3Ÿúž?MI¬¨-’�=£æ Û‘Ù|^V¢ª·.—û€p‡Â@ +ü z ¶–u†lu”?Г�¿^¬°VÂî�û?™Ìûø5H´æS,³îð4¾¨S!RZÿ°È8q§óSD.ðN6&&1žßáØZ‹Ël-ÈÿÃX€€ˆ@ @ n ‡‘‰Ô…F;ƒÐ¥PMZ!aÓ|øé§£IÚ,†GS*€ô,ªö³ ˜ÙCàšôr4Ò뮺뮺뮺뮺뮺뮺뮸[ÿÿ ¦|JÛ¥à‡s�òÕVnV6/ ÜA•pŽCMI<7)ŸC»ÇŸÊ2C ¸•i0Ž´8‡èp Î�»ÒàËøoðÌ PÕŽžZ~ ¼€€@¤TÀú ,´Y(2Dk¯1ºòœ¢úNØ„ÃNä‹#2¬ŸÂ@@ €¶�g¼.ˆ5Bõ}¯1º©ÎâúÆ?í@ˆ÷¦Ç—™·A0n™®ºë®ºëÞmO^ƒžçw+ne^ÿ„lN·@H&áHˆ`]´¢dÿù�3`6¥ ?Ð@ŸÝð€@X „À A?p.2Ð> KÈÄkîÊ»àQ­Q½@†D=عHª–J'°[ëbêEgïOX I¬-‰žI-ÛÂ!H * Q_â‡ÖØɳAÿŠK| Á†hT@5×]u×]u×]u×]u×]u×]u×^û[ØÚÞ4dnÛþ%Oþþ»,Øe�Œ¯×è( w¸ÇüzN7ßÃÚ0ŽËB—/—†<ü@ ($ À  ��©«ªHç �ý  /ÆpYà ?ú²5y^ôH‚" OñŠsŃ'ÿG�ø+¿Ýïñ…á!Œ†ú뮺뮺ëɧ҉ãà=²s�L%ÿ[…y¿JÌo~ ° ²fyê?ûPpS:,šÿåP(¼ÐG…�¸ ù׬ðÝCñ @ " €Cy2Ná(JŽ‚þð.NÐ÷'\–à¬(ÚyÁ’f�CPõˆƒÇÿxÄìƒÊNû9jqÂd›Dºë®ºë®ºë®ºë®ºë®ºë®ºëÜIãÉÀ÷Ý{ vø0£î¬8¿÷µ§L€ L˜)Gœ§lðà—qÃÒì8nP²K�/÷à? P.¡0ä7)--!#€†YûyJZà!þy€0)Ê{Â’Q‰´P*C²ˆxRpñ³°\‹ëÞàb iBÔT6.ºë®ºë®º÷Ù�ÚÖ‚Zõ@FËÅC��&E�ÁÀEE$oix0(Bÿ¿R߯€ÎÙß* hú°IX$Ì@4¨að „`‚0jÀaB#QÇ3uÀ 8¡£ÿBºÀpÏb÷À‘ͬc]¬9…ŸøØd‘®ºë®ºë®ºë®ºë®ºë®ºë®½é4úNñ¡¹%såHŽÿ‹·F¸ÆïŒ LSOñ(z©¹v£Ù…;_{øJ @ PxZÀz“€�y‡¬pÉ M^x/¹€/LïÐ@zÇiƒh³áµ>Ã~1áÁ({ÂìûÿÖð©°©¿�Œ%ÑÄô»T³þ&ìQ½nÕ,5×]u×]u×­ok|ƒp¦1I}ÝÒ‡û¤ ܃1H\VÇÎßßøÏ“]0ÔóO°�öâ�üôÏÊ?�pÇMAP—§€x  @Ѐ@` @h”üÁ%¹­3»RNI€HüÅõ ��$ €€K°÷Ð2‡¿6*tIQ‰  `CìRBïÏD°Â@@=…×]u×]u×]u×]u×]u×]u×]®H§Ï9Ÿd W_ü¬è^‘ñåàÀ Á›vÑ—ìÓP—ÿpk² PìQ-(÷>‹¸—.E/»ïtQ.¾e$bÊ_ Ä­å”ÜFÿà€ðð €@`ê‚Ö0 íû7²q–�'c¡ª­àŠœ{e€{°Áó•O?¿x™“Œ]e°Äó*¬°"¢OÞÿþ õ×]u×]{íoc!„0ó1!EÞV~ÑÀ?^ÚQ•®"Ò\l%w ÃBc^&î0rõ� *‚›,Mú$Hœùq!zÛaº�_ÞçøXð@ N€ @@p€'˜t—ah]Þ0ˆn¸†GÐŒ�žB0`E ê˜2\ƒ I»”p>D2ºLKÒ¥a˜‡8-õeü?)�­¥cþÌ(0 pòá€HA&uDJ!dlŒ¢"HDÈDf :뮺뮺뮺뮺뮺뮺뮽ëÿÐ,‡€% ¶Ne”ÕûáÎÁ�¤ M•#Ø'I&¾ @YØ)I o–ff�L'�Õŧ¿™àHWQ¥(…Fõ}Ã3u±õolø,bf ´ÉN÷øÍ2zu_ÿÁðblEÁI”.–WþÂô–œÇB?0@ \0 5�‰�¹ÀeŒw¯Ì�“°töØ‹¹³„]�•›¸¡] G¯Ö'AÙj=…£Á6"ïC:$¤Ã™ÿ?]u×]{ÿ™á Â1Š•,L›“G­õ€$%@OëÍ^ƒ›ÒÙÆF¿–ÈDµŸX&J*mi¹~l˜ ùÏù'ú ) a�¨0}#3Bl®¨€ö2°EBOiýÚߪÐÃ>‘™¡6WT@µ0ÎÁ�KÔ”8ƒZñ©Ìp$â"+Ç�€í�‚¾sh‚ý QœåwÇÕþBI³–(·}ÿø2þNÊò÷¾€€„BÐ $;Á_Áf–ºÀÉ 8 ?ïÎ5µÀ©u×]u×]u×]u×]u×]u×]u×]uï¯þ�d0�Ó�¹Ìîʆ€¿ªœ×5püÍ‘£-'SUÔ=WŽè ²T½T5D ÿíeŠøÀ a‰ÍȆ[X·gˆÏj©%]Ïåïð}ð,!Ä!{Ÿü÷ဈ¡€Ø�}0T@nÏІXZ:'Èk›ù02éÃÜA%†+ òœÀ!hf TN˜÷?ò`féái,©_êuÕaØ" ”Η„%ïž‚­ô]<‰5!Á6VrÇÏ�Õ DÌ”HZ%?÷%ŽßEÀâ1[ 6€@„Ð�¡jŠmÌàBç9@¶ƒˆf¾ª}ßN·àCIþ�"Ú¯¯ýWØ-íÖš#ÌŒ±‰¾ó¾ëæîvºx[ÿÿ÷ €?—ÂÌ»ÿWÝØw€ éQvéÌjѦ Yé™ צþ€=Î3…)mà{(B ¨¢,7ÇR!¹z¿ó´€@À�¼ GTY%œÄ¸h�kgÝ€7 gTYfœÀ<€éã@ÇOºû;»ËÉðçĶzWÿö >‚1%¾Ôcô* €¾#W,Pµ_ì %ÍÝiçïm¢ ?Rdydÿ¸‚̪.L5w³ÂÇXÒž |    HíàUvÀ†‡þ TÑ-ý`ãökaŒ8¿„3 .ؼREì þHKbJ¬/!¼`§ /áLÎåÿø+è0ñˆñÈ_uNu¿­í\D17h?@ $d #p éü"“Ž80üo¡T å‰<u×]u×]u×]u×]u×]u×]u×]u×]t°º Ë†»+Ùÿ×ÿý÷œpý à@¥\IÃÉ Ï9 3y d�iàæ0\DÅçÀÜAסâ½÷ø@L1 ¡X#-滼ÈT fkõþÑ„l’)îÿ´�V”VOoûÿý†õø-Š:?UÔøä Ykž©/ô�¯˜ÐI;ûÿý„€ @Y!øƒ*Dæêÿ�ðlVDØúm®úɨ?BøgËo_á;„+„€´¹‹ƒÃ0B@âÿ?À0VÊŒT§¸c „/ùÿÿÁYoâÞÛhÝ þ€l ‰Ö*Š’|ó%EHƒú뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺뮺ëÀ;Aš$lÉ“†1_V¼×û®I»ðdÃ7#ëÙxOZnoÑŸ•†„„„†!7µFwïð2·à”žÓóò§,è<Γ•n�xÞI#—þIk%ý :©~™u5»M¶ !a �ð�Á¢“2¿f|ù)ç�¿øà€w|¼!{«ÍÅ�ž ú™á+ЉšøÎX®W>0[ÄòÃüIï\ ÿ˜? ½ÈCCŒm[ºOtgÔmßeQÂ=ÀLصûþß`wÐØžf-R/ýk ÐëÀ7n¾P ÏŠQÌÞÖþçñuïž ÞN>ÎiýÚ—åÆ±ä�‘ð]í>Ül*€~š˜™?œÓÚŸ»ÿ঩:.Co8Öà»Î×Eéã`¡ Ü{ ˜ëä°N[�í>)ü·÷=0þ00òÆóX/”i#.zÓ‡ˆ»á6ƒT¼{Å  dhú\À„€>¸ ¿_ûwî_‡ñ vÿà/)ÛÌÞàÖÁ‡âð%Ú?ÏãïÀk”€ûÉþÀðûÍþ½FuÞùòkŒÖrü’¨¢ÀÖkÎïÌf›Sùb_g÷§‡qFŽÓ£@!“· �ïûþ ç_í¦?ç_þƒB·m5ò ™éµòwµ_=©ÿ_�ñ�´÷LÉí½ðú]¼8ñ!»Kõ¿7Û¼¨1ÎâÀnmVôÐø§-žÏ £jùú"Ê/o„P|OÀ7fF=2Ý:øÖTÍ,ôìÐÃôK†OÑ�Ð�À/IÚ?Ï)øuýŒBaÐbW+I¿PRV»C‰\Ÿ!=øì6’gÔ1ðb(.»»üCiǬ®çÚõƒ˜HÜBÀ füƒ‡37°öoÀá�ÿý†ð0 �´²m£û` !¬à,8 ÁnfÝñÂŒål}tcÃbŒažÿ°à¨ ¡ªRüªB*"°ËóÈrnä=÷!É÷'a6Å‚\,>DÈгyœ p¸œ# q¹Ž"| -ÌpYñˆB ‰@v!Úåú£¬—™?ŽšÈê„ Zk'cü6_‰’p`4gÉMRd²™oóéw%€è€êÓxð�΀:½7Çžc 6CÎò jý©¯Ð ˜Ë=sÖ pq~A`¶‰r p<Ë¿!8I¯88_Â Ï �.�Òx�>4-¹òÀ  ½{~£µ¹­;{à 8¬t½vµ°1‹–»Ã%æïB–�ıÑÐè’ŽBe-ÍïëÌä€ ?VõØe`…ãÅ=?te}�KnD¼ ‚Óöø0/³¤ÀÒùsG/þ$h|O ±J¢_wŸøúÖõŸ“.7%ÏïwÀÝ–ºÿ³ÏBå¶z/•óú¿Q´(ãïÊ}ãŠQ¡”k20ϵÃÇmíšþ]µyU€Ç®´Gözè™;¬Dº?³ˆ\ÕÞŒóËp@ñÀ?܇«ë?öeî—÷ÿrü9�¸”ß…f�]½ÁÖ ?Ã+0?Ó㱟äG¨�xuAüó¯üÄ™OÿvuÝìL¹ßyóÈH"Ñöã°à@·éýóðxxÿBCO€2šé©ô ¿í§wwÿ?àzÉÝÈ׿/�„Â9„†2==Ç›Ÿ“˜÷Œwϳð~ß�êWéùþYTGJSüÅçþâºR1ûÀx¡ýlò_ópÀ ŽO½ ¿¾¿gu½8{ )~LÞ»‚kx�êKÃ'­¯Q®È�M‰†Ü >w×þÿÚ×ý—cÖ•f4c[ …}Qréw.—lS$0àr�µ?Ò-55p–OÙ½Æ «–ÓËa-áNX/p&//ñ¦€:¬á/lt9ë‹XÓ6çZ´ ¡ƒmߟ‚ôáƒv6AQ+ú(%h<wÌaì�ÔPçt/7ùu€f®tb¥¼ü8Š:û{~Èë€äŽ_Ö òÿúDñÖn}�=}‚Ä…B‡ccKŠV�yççáâÚi øjb޼1§†sG‚MOþ"Â˳8=àœý£ðñAžBx<˜Üñ�\ËÃñÃÐv²�É´ðÞ—2–½Ðj^; ê3 èÔCA]'`³©w‡ûSŸ_ÿðûE`Œn Hp\ ¼îíš•p‹µîy¸#:Êm!§ö(Våò2rnÏÃc>ëøi·…°€œ‚CŒv0g¿ø•gc§?¿•¾/IňSî³ûÈeg÷”Æàx‘§øm0ÀiE{‚/Åxúåöì:I§É6z†å£÷«ÿÎpÓ{®‡/øbZsĺÖÃC —âÉ%C„—aÜbyxÁܳãgáÀ¬žA&AfrÅ Ò“()ŸàÒîa\©w1‡8!8\.Ǽí�Ž´£©‡ÀÙÍd95r&®C‘ë�À»îaÃú $O…HO£ÄŽ÷ Ýá3ã°]pÈ]ƒE ]é$»¯– —sÊ*ç!%“sE’Y¡×1î�‰08á¬9LRŸW’yˆ<¾3£É<Á�¥—Œ�æÃ2 L¬ûT8íèçTÙCûb&]ý³ü>;4üi¿$ñX®É—B}„)7OÂÂa8ˆz—të�øùÄã ÿú J,R+ñÀ ®™ãüJBR¹DŽ„gƒ¢2`¥øXECl]ˆ‡U–›ùãö0×à6àÆžatÌo,”~-1ƽ–8×à�ˆ‡;FI%ÄâDžÿÁg·}Ñ+ÑœIʰeOÀ­Aøžs äç †f_j,Løðnú§]ä³î! f½ýÿñ xž÷ ¤$u’gßú $iüsƒ9è?Þ¾¾PWa¼þ÷ ‰½}Ö'Ýâu¢µÃt\Cf!g¬çKÑíÁuØ—>ߺã_ÌéÕ>Ÿ 9á,tŸ½ÁõÆG„VLp“ç€[Ôm?üQ½ïI'óh¶8¦~€ßèýjîþ¦ ¾�ë•?÷WpÊhòTvf÷ÆßÅQ�±°Ã�D¼0ƒcõ–pím>Ù?›¾¿ÿÍüÿ¼¿Æl@,Óu„Ž& MH×_¯ºÍ ëxOa`äé4ïïw¦üÁ.[¢8×þÀDúœï÷¿Çþý 5[¢.÷îñ†xûQaÖ†.HórùÞùé1¹gó‘r3lîàbl¸ºÀÑUç°ñ5•wÉ{ãÄ®84Ø:JwwZ*áþü»~±;   :ýÚoî§€…ZxñùMÆ«Oו}kÃŒ� ºÜ8 �úoÛóW€-É£­qAúÃ?—þ@ q‘ ·ùÒÃâ÷ò7ÓÅáˆ>—�–Ó‡�—æñú`²Æ„@X£CO(ô¼ôSÇž#MýÏŠ%”4·<Ÿ0 ¦_óÁÂx I>yd$pþ¾¶NýÀÐÙ]•À¢A°<År±~E&übWó+†ìA=ø[Ež$•xf М.Ðgìni82i€U{n«/�"7� ›Ç‰�k4Ðd•š�Åœ0$$enõZLGU’ɽÚJ¤¤l H@lh«Xãp)ÐyЏ@)®8Ü i˜«ƒ øƒ0æ1¢9ãÛÒ`�«ìÉà‰ HÍ-*(~·µç¤%pJNrøb‘î×Àé$�á�òÿ°Ù"'´utzD4Éäê€ôZr¢¡�ˆDá³g¹iÆ‚æÆðT¸0ö0¿ˆâx\}xkõ߀"y7ÌÈG!\Ž—#’‡Q–Ò¼y¢±ßÖЕð@—›É¨øÍdæ‹ b?aÁÂüTœHÑ&Š ‘¡“#oÓý�ÀDE™-síoöíŽO7+À�ÿ<1sJ@0QQª¢Œ‘à]V%x i•�¿™ô§–IÕ£7ë-¹Šv´8¸øÐº³¬‰÷Ì\9O%w¯¬Ëà +ã¥Ï�e¯úÅ®XÐë(nƒ™¨G—&hYzG—áõÉ@2>ð}î~+3x�#óKüx|`(ˆšhä€Bnâ¾»œø­ëÿ$çý¯£HîÞ„4—ë yø©±P€B�Fžf\•€gZ-Žçþúâ(ˆ£+Ú¯6ôŠø¥Èø½ Ë5-Ÿý ø+bŸVo½¾@&+¼åíñÞ“{çdwwƒÛ,ÿÇWXi)“&kM½6{éæ{µ†Ù—LËvo;ø,.ÎÅåÿƉúÌêÀ¯Z­ÿÐÛ<Òó^—u`/ØCþÆüû”\ë^çú øÿGã~}:×™mßü_Ãpo0ãäÍ:€ÓþRÌíÇ÷›À-þ>�xÓóíßã¸Nß‘vö×^Ù?o õÔ[kñ]ÿèoú–Ÿ¿ÀÆóXÒÜr~o_¿§¯Ç~Þ=Ñf£WçÑ÷ÿÈýïÐaø¢CH:â[¿÷¶7À$+OÁN "ëûßÿnF€Úb¥îÿP`#W¶éݧ€s»©ßÝäo+Ïf &ò(¾<\ÍøÞȸÛÂRÿ…„„³¯uèÀ8“©ÌùÇõÚòRd=J8˜2µí z�£ù¸fRX­gàØ&|Z�æà˜@a×¶|kÂÈ@Yb‚=¸þRO7$óa÷8p!³ ÁD×ã�6õó×Àê¯0ëüD4+˜¬;;¿òfœæÆ>„®õˆ:Y²y≚s•aËäÿ&iáÂnX(‚1%^!_ñ“<1˲ŠÈ <>S+�k?LùŸSàê|`™…ñD9(Â\ÑGŠ ÖzåÐ�Þ¹'Ü ¸Y€"Ñö£ß›ýSàâO€âV»Ž)ñ‚Âö\¾eÄ'˜Mur~8?\ œ³b÷œzàMû=øzÜbÉžht™æC¹â‚ôœâWÃãøtˆÙŽGˆ“ðŠÇôýá±oÅO2é°þ!›¹› Á7 !<>œOœIÑpú‘ÿ¯À‰­ÿÏ䛨"G•eÒîÂ9”K“’zÁ$Áu;;ïÓ¨.P�<·<‹'™‹ÝÍÀx6ÊIUìÑÿùÑ”=Þß0á”:¦Pø‹@f`9îœÀk€sëÙÃ)Ö/±8 ¸Åž¥t΂OÁ"ûªf’ÿÉ$õx#¢KÂ9s°¬>Âg¿ëCŸa[SC‹ ðGЯᑹ#ÃäEÂ7‡H’#'X¢d”å_÷ÑäÉ)Sþ&_°ðûß|V(¨¢¢�wŒŠy×�y×A :ðõ1Ñ�NμëÉ÷�´ KÅ K/’Ikˆ ¸ƒ3AyÁuè|vÉ âN¼#óð�ÌgÉ$´!êPOq–Jgwl(LDÎKïÎËýà7`²ž§@!à,è$üÊ”‰ï× �pº©ó’Y+ø|">çÂ$q‘8Hã"S ŠlŒ. 7XË¿à7xÊ�â 4Â^ ¯Ã³¹ò*R‰ „áàºxH®)Ë>ø#lq ï3†”Æ2« Q”Çn` ™dŠe䇄„pÐw{%øݯKâR$8ˆÝxH0ûnZÛáë �rI-õA¢¤+—d–IÆ?áø¯_Á$B{9,äŽK»ÂAš¬4¤Àx 'Žˆ†ÿ m6Eæ¸#-Š Ì\•äa*á®lz0ú¾V!!`N\\:ƒË~�a G¾c€3ƒ@!mð B˼@”$0(%þ0ô?Å&^ü7Ò"ä¦ßÈA�ñÂ(–F0ˆ¡À[ §‚mŒë<Àt€Kar~‚D†^ÿ&¨ŽºÌ<;àøhƒ—O~Å™†G6œP–¸ŽÔv„�€$XšÀíÚX’Æ.�û° °~Kÿž6•ŠŒ‚4'ø·>œ†2D2ˆ¨qor‹6»äãX'‚„!(·‚7oþ7ר¯Oê�ªÏêc•€Q–fE¶n¢ñ8×Ûç–þç*„ÿø…÷.õ ¨ ­Pã| &§×€¦§ˆŽ—}Á˜`G 8Y}Ý÷‡Iª‡œÿ „cÀ³ Q\k鿼+=ÿ™Kö(úýÅÕÞ~cõä_ø@> ^�K—âgÄm~6 ¿~àû]¼›ÞÁzÐJ<Çêw—ùÿÐÀpYBåàÒn‡œàºsNï“|l6!ß⨥Îó§àÓ=5æ�\÷1–¨`4ùºÁ€í` �ö}øÈt9u·çü$xÈvBZO<¯×ä¯tŸÿ QráÅÂ�v…(À�ljvÓµíKdKÚî}¡ø#Øz±Â¤Û"Ïè ( kŽã>wᇻƒø‡€:Gºu®Á<ò_ŸS"gà:§Olñ"Ôyè§¼ðTs ½÷Àá~>$¡)F¼}:ÑëY“8½’4Ëi]ýü¢§ðbW/€Ç‰62 »Ž Ñ;ΕÊÿ‘æ ˆpÍsdžˆpB\-‰Î�-gù´Ü•h¿Eù'ÔÞµ¬nýG¼nˆÔÄAÕàÀ ‡³9‘\ Ž�ù É® ,ñMq1 Íù Ä×ó‚è",¤,zÅ0Z‰¿Z5ZÀX›ª¼¹z+ªþcÀtF²¸Re¦‚àŒÖ@<~$O…ùg Ó±F×@MטúÄ›½þ,:œ‡‘‡·að<ˆ…ež ‰®+1'Ž7­¬³Ääàp¿4‚<±HaPº´ ‹*ºO7øÁÞŒ±ì÷V«´…nKI'Ä ¯lõ®èOz' Ó6Œ6€Ô‡�ÂúrŇ„€ïŒ�¢˜ºÍþw™¹ÁB&DüÈUåò)�¾òWßoyé_¹vðYå‰D´•�~¦§ò¢8· ¿ ˆ�#YpØ�ù×ÚkÎ.. ¡(ÕYß�#ðAl{±ð3ì„<± �„C­à…¿À¿¤¼Çlj÷¼LÜ„ƒó¯ºØqw†˜�¸VøvX>dɾ†Câw ÅÏÖð&~>:lì¯×¯øT£¨í½µ?÷ÔÜwøt££Z=´!¸1,Ô2yr_À³˜"ùÆï]tc ÚbDÏtKÚu¼ô,HUoˆ�¶Ã(þκÀÑfG3Ä\Ç}ïé÷�`ñßIwïþáÕÂ8iG¿|LÿoÙá'ù:%ÿšÙÿÃåðHp…C�øq~Í77^DÀþ#Á#„^AG¸22¬  ,å�lÅ?ÌK¿€/0k@p(ŠU¾ÿá٠𠈥Jÿø~ CB …[á΀ͧð3_çM>þX+­¯µŸ?xJ‡Ü5-à+u�™‡Þp½müwÇšA˜WáøvÄbf÷ /"¹˜€V|^XŽ[3Ȩìˆ>O¹|7¦ÍôÓv††ģįø¤z'ÒÞ[*Ö!z¹kFÓ¡—ЀÀ(ŽnWàD_6—Øl)° x!T!Èí?<È1`=i‰äð\ãìX©©ˆû¼2�ذˆsÄBEƒŒiÈG—‡Ç–4B<îÅPÆ^#~æÅ¦Ô%0„ê Õ�س¯� 1EÈsã•Å|8_#qòø¼#p@ðk¡f(¥<ð[Ë"óI|WîrW˜Òùv+$C>߈`†üƒå¸Þ‘ï ÚßÙ/þùÓ¯€ÿÅ7êÁ{×qm˜ãÿú lÏdpR¿?1™ÜÍ©\üüÀs”Q%Ï!_åm‰¬ð€ˆÉ÷ ®ß3ÍøÄЀ%BƆû8ñÆ[Ÿ‰ ³`ðJ{Gì*‡}<=©38€%B•˜9`郔x‘ü3íãÆ Zál‚“ô~ OÀÃ.AžÊE(!à,‚O- *sH 3€õ¬ÁÁæ1Ž "Љ׾2Þ:ë¶øB*P¹pnqØhrìÝDÎ=*ü5£(‚€Îm¢ ®¢ºˆÈüMöŒ{õ<6†›ä,‰ >ˆÏ8/y7þ6W<Ô‹&øÿïÛ-ÀK½@r¯ÙÿÿàU{ß§üBÄ齟ÿáüíNÖÿõøôDü:S„7a�¨±&“2I'ñ0׊ŠÃRºëÉ_úÅCŽxÂb'Fü8¾@FðÝ2(È$JÎ1G‡éµ”‹PF ˜9i¦AYåÈ`#Ï—�ó£ÃðJ ÐÒ˜>©1²$¤ÂËíª½áÀ–P¨ÚÏi�rí2{LÑÕjª²@`]?Á'xõÀ¾cïW0m0?ßað/àüKÐ/‡Ø˜hRÍÀýl©úÃñ%øOÃM䊞Œø_sð–ï†óçŽø©á\¿’ˆŠv‘Îélvƒ‰ÒÔ‰�ŠY3,Vçx6rÎv¦†;í± À•d¹m5�íKrY'’…2EÕ,#‹ç*tŠkº>Škßay8P– ‚š[ ÅwÎtuø—x‡ˆ«T¼KøÄoN¡jØN!Íêë÷ÆYzå¾¢°pGžétÉߘò¯|F…¦p�Ü·Åàùƒµ!Öc¤ñ°D�Œtd~ ¦«wy&—+ íÃâ“ †ž18Ãà›Ã÷Ò˜±ÎUó?ãüB<ƒÃH $xˆp+¿Zœg\Lã3…B7”:%Ř'›Ø@¸£òá§:µNI_´\8LÒÆi D%ІA0D4¦Z¼Þ¼Ö«xÇ…Á:SÑ«í@£­¾Ðà]ügËáºfz0.Ÿá`NñÚxb™� D&y# žHu¢—QM¿©ÎUø=˜õø”ñÀ£€ë°Êÿ€=wæ/ÇüBÃŒoP—�æzð/ ¯€t· ‰S¬ r÷-²I~‚%EX(qß)eá¦X©Ô©ºîJe¿d¥‚P¹ó>3£«JCÔÒö^UüAö^t"×¶}ä•`•_åþ÷ç::¹žžë@!à6 $´ì’Kì ½ò’Y9%’‚]Å<o¼¢7á‘ÁÜëá”=GN2d”†œà:'ˬ‘A?û–ʼWð(9  ™dAm“,‘DË"ý^"6�‚ÁJ#ávÿ*àu›Ã0N„äðLào÷üx àý;õýáñÞIy$—T4©#'#àŽ§N!%ù'€Ý€»Ašð4¤Àx 1=ÿ¹w„£!‡€#^ùþ×m~î'@+¿¬‘Kÿƒèa:B+Ò£tóX&:È4¡ù)¬aòHDŒü&˜qâ!R ƒù[T°¹òó #MpÜ YÈ^uhP%³¹öòø�3Y„úkvŒŒ° ùL²t8 æí<ÀP+íáX8 æ2Ÿ—ƒq¥†h;Ü“ c^#ÍàÓ‘S8@·žŒÂö¾]Wä“—/øØF\Z*4¡?Ìæ˜!w¨J³o|œkI¹Þ à†ÁÀ$ì£M˜^)Öý •÷ØRþ¤ÀÌùÔmþfûüÀü;<�/PÔÀy-Çд¯«~zZmÒÿ2…¯�ä$Ýõž?~ÿ{W…¡UÀ �z�z&Ì©Í÷ûrnå‰ß~¿þû×Éž\*²æS—:臖y—øqs/¿Ãï4›®/_Ö|.#‘0ßm}>X^ÿ¶ÚêÔsÌÄ„gâþŸò†ëø{„EŸR3Oÿíqþ¤¯�Gã¶X0L„P/XA›OAè#33€UêJÁ–,@GÔÕ¬JŽ“âm?¯çáÄk½a`€ Ç‹WH~9úú¸­ø-³ßU×@ TfÁ˜û³ïðÎÏÝŸÒ@`ž<Ápw/¥×ç¶Øu÷§‡xH8Î’ ;�³†¥òÿA”a!älÔ¼ì©^‹ÚÃïŠ ŸÅ{¬#æþ ¥áÅn�ÈzÞÞmðý¬Ã‹áº2»ö¢ÆÌ³sK_Ëå_ùljâ·¼I.Öíút›0ÉÞ£ß2®žGf”ÉC…øÙØÙF°srãÓŠù4«äï‘­WÔPzQ‘BbNéuÁû(�"±HH¾³v4ŠÅf¬Ý]Á…þ]Âêzõå„a˜m¾‘€ÊŸ[²('õݯtÃsúo$j&š;ÌM¨âÙ±¬ ƒdñ.¤ÀDhOnˆ€¯ÿÌV,†¦Dþ¨n'�x«�x¾åæ¿lØŽ³\�Qg†Ù'�LFÊKÀX‡ øKR¬ ÞšÀy¡?I�Ä~(¿‘b!tcà"Ú¾‡¦m0(<‡W^è”O7| ÆÎ&éîÀj|XM8 Ak¶œ%W.è�b6Û<äà Q˸8_E•0±3Zž;$„™k¿@ÂÔÚ{áž-ÿÒ‰¸pòã_½{¦a1á <<Êà´ÞM¡Ùg«¹ð<Êà{ž�‹ž³µžÞÏÄ¿ T.€nFø¨}Ù£#›îë~‰zX, ØE4°5²Qœý¿ÃŒ&ûrñ%=!ÑøvI~@…§ž_¿Ì7>úMŸÕ5±˜9^håØ÷ðPÝÛÿ¾Û­Ë¬ôÜ.Wtú�^×~Œ¯ŸÁ±®ÆMW^‚mÃKRx+À‰º5×—C??¤¾é�æ?øqà'ÅYŽXT¼ ¿úÎÇ«~Vá×€Dgóï¨VC;Æy~gW^ÖÚB™3CÿÅr—Uœî5pNä´°ú× ¸ÄÎ�<ø°syíPñ›ÿçAœê´?óø~¦E’·Á�Ç~ ´2ße¿;U•-ôˆetXÜý-xH*Ùø¤Bwáõ §Âäù %9;å-Œ<¢3øl?hÄ’¯á¦ˆ€œ)0F�åmGÿü5�¾ä` |$âü;9m0É5J¬G" ·ݾîÏž¡˜¦ºpHñðÀça{Âr�«åžsÒ ñ€îÀ露v_�Ó³A{?Âï…!WkÊj£ÞtÙ÷ø ÈŸ-tÅË=„­c\ó^ø$sã¾a›3Œ´›å¡|˜ø ä¹¥wz,ãÅr¯ÃZ0B?$ñ4}2|‚Çañä‘¶¯Îu\2ÿñhh.»ŽØïxMùãÉ#m‚HP4|:¤Å½¦ò$×èpOž1¸ ІA ˜ª\­oü`^EÇpù�â™�lC„ÇÃ�zãL—ð"åš!{”ÉâÅeš&X„Al[‘,tP@øípëÌ 0Rx}'c¢~aÔž=T&(³ÆAya')Sü|ûV8Ê'‚o€íü>Ãú%_Sø$Ë€ßâ¦83LæÀàèÔÛÏ©‡µ­kã^în‹×B±"|¿ž?eà7`—Ÿ/@!à,è òP²¢ BI%¡O©=‚#ýò©xð¹9�'Þœ<0AŸ Ã$_ò¼cñé×î(™%_î\)“≒T,âÖHqñ‹ÁE èpžHÂeàPu9 ¡e8² /Ïýe8Žÿ¥ØÅ GúË“,‹ð%Ì? îÉ2b8n  è*ÿ’Ih¯©(•2úðD"#äÝzçA›44¤Àx 95¶tQ†\|rj¿8aÀ$&¼7Éntƒ&.\ü°± )jü¾xGÕÌò0˜p@dæ\ìæ÷Í€àäŠ'Òþ A­ˆHñëÁ �0ûç�YW2ì'ÿ°Pã)K39h”x^p#üí<Ø–œ9€~^¨ rà?Ê3ìt~–nbË}†z:ß ùùƒ*Ýô„n>‹àž²¶ ™¨u—vØõ�²}>Ó°qöwð'ÉõáWÆ÷ÕÿÝl”ùé·åðŸ| “í¬BvÌÜój´Ðÿ#@Cçë}¶dhüÓIã¬ðEé€Eõ¿½€AëœÙ^¥+ùaâýB†bÅ5,æk3-7�(çè¿dïn{ä;}ýg»þPªn%ùáÚ 1¢ÎÑÆ?" H<€ƒÑÂT@nµz!Ë>­ÅM'ø ѧþ�ëºõV÷i çœ?ß,B&…< [¿± ¿ÿë»ðç Þ²S\wùu–·©ëᔫb¼øcï;ãöÇÍß›×÷9û_ï¬,€ùa# ù§Ëyy²üè³×ºÿÀNßÖ»÷Œ9Eѕɞîëó7kñ'ï…ƒÉzšø/¹?y¦6èåR~,'s«#áÅÔ ef¦MXóŠ?K–‚èì™îÀot«ü1nƒ…þpG8|.“ÈÌJ¶¡›õoúöˆ ÷sq¿„u| úpö @Åʺ|1ršs¿æ\”*†Kjý{Å ûk$^"B"‡}°ð˜‰žd©YçƒÌD„Ÿ>,˜!¼«€�€e›Æb^7Õ”w�xþ5B²¶b´÷»Óëmžå/ Š…ÐÆ õ\òÞ¤¢^¢ÆÚуpl#¯¶Ù‹¬õ€G¼ók÷D!άi&E…&1Mžœ³ñ<*ASO˜¨¥Ÿ/”°Ê²úøq|*M±(f“$i#_„Ùx‰Äp¨E Ã)•xj\«×ã‡á§XÝž\^ê}ü'gÜúáXL,á-õ+zÄÈæÐ°–@~�Ѿ.Fû]ÛH®4àKÿ9xzýþ¿ 62”˜uVÌNóèjHt1cè �Þáó¿L¦'�šïSn�¹øñV¶šÚgMÜx@D×§(¹�|7âg A Ãp8BWàV®®]ÕˆÂ0Hð,õ1ÊZë¼g`mò¿ü>VÙÚe6äï¡'Î�²W�ÿ£?ý`ü¬®žü +õîë7‡ð”*öEúK¿¯åÇ�5£7,xnty©àëâõKÏ×\.ƒŸ5«–0ÏÃé™À˜{æÂk_‚7ñDŒ²aàŠ-À8‘Ê‚e®áàŒ¸ æ 8`\r¬ŸþÆØ$Ÿ¾?XY?‡£-ðH²V˜³àaÀ4¹à¼À?Ùƒq..Þ^NáÇDË/ÿÃz~‹tŒÕ‚üu£–fDÈ�ù¹K�<²’×èe�û…[†ü gæËpzþáiïI$ëÿ†áà„c¡yoün�¤ðCn�Áú³Õ±ùëñ XþZà€ôéÿÏ_°?Â~X#?!ø%?!ø)?'àæOAŸRE(!à,P!É(Vï öã6†ùÂA†tC…Å0ÿÚè°—?Q4,V „dF$0^f¡J}ø½a½-€>õãzz9:Ì¢xWQ— S.„ðÔVÏÆv}©�èÀ1�Ÿjjq<s_Ï5¾aàº"š OáPÄžô†ÈU k (v�ø¾0NXÈ¡¹hçèþ&ó‹XÕ#Óç9/ø„|‘@ŸÆÀÐ}á§+ÀÛžkݸyA‡ÀgPç€<¹fa൮¤ÄFœå—óB_EpʰR|Ѥ—ùzοñKAh]¸¿ xÜâߥ£Þ#/¼8xøF0™£áÀ½>;0€4—ÿÌÉa9Œ ÒñÙ�gÿ2&Ç<ÃŽŒp„8Òâbcd8o‰ŒX˜qbN,@&ý¾ðí´ñ_ Œ䨀 ÒÄ^^$>^$1N(8,8磵‹Ãt¯GÍ�ɉŒ=?á¹,x®…Äâ¢Q=ü=ã�Á”ÚŠ¢ÈOßrœêŠß3+}¿øÌþ=íÍ8¼Œ_“Xí?áÑ‹Å{ã‹Äìþe¨­K›ýíÀ ÆéwüB‘û „p°z ÀRŽ.àÃ|‹ãŒƒöabïµ,u|>ÅÃB‡u+Àg�£9ÿàz�WÔGQ/ØN„”Y±Öƒƒ`®Bò7sâð(¯¹ÝÅ„%%WŒ‰º2UQ<.B/Ï$dP“‘ŽƒMŒðDñÂLö ,@úðÊ ¯ÿüo¯KPaðû/e䈖ŠTUgálf²LÜ#qt~ÍÔOdáóÅh {È gøv$…€§à$A›x4¤Àx 1Ø $‹nƤ®ÞÄÄ‹Že�X¨µå\e¯Â{øÂîðŠ_ð¥0ð쩇Ð�å$ÀÂãÖ[wÿØ<*Z†Â¿Â >x&êv€<㩬ý`5Vù.T›Ï/ ÆÃš!jìˆ ':ÿWÂ�á˜nMqáž–duüÍ•û_ÆžŽü3 8e¤Fx¡ºÏgKÊŒ7`/Lð"¾6s o^½ê@B|*ŽÞ¿½¬ÑïPœ<„Þ`û£Ò˳Íßì¶[Èsgå ÌÁ¬`‚ªX¬:B�z•ùô|ý\�@:ðG¼ �ëg›VÃÕßB ÀA“Bð5°Oô X0“>µ;À�%[V\jÎdߨ�à$Ñåæ0R>Ü'¿ãÀÀø¨@@<•rKÔ_Ëݥŵã%ˆŸÀÿdQ]özeA9²êOLÇØ>œ%›ççþ�S÷æÀiÄ|ïÿ\+¢8Éû»ð5Áë!i-½öÏPù™µÿoøÍí~æðâáHX.ð+˜T:J8‡îcCn‘%ˆÛkF¢Ùáé�ƒ»7×-ììÿ®_°`ñeð Sš@²w”µ×øtHW9oÁÃ@Ô3szOcCÊÀâê ½ ù¢O}]N0¿]Ð]$c5w ±`¶ãteü8ˆ „Æ0G_h ˆv±ëÙ{“ ‰CÞ÷÷%ŒDB!t‡¾Þ 8"œ°‰´ãß÷ ÛL‡!Ø]«if¬I`(EŽkÊúëÁg4N¿n£EÛÀ êy>~Ü”åqb#Wƒ €J™úTG~!,Ò Òú£6ÿ×»khE)â„ Ö¹à•�¸(h²�ÿh¸npЫDØ8_ Ì�°@��¶�‘}þôPK´Ù.ÿ¹}e|~¹ûŒáPº•IH’$ 8Ig3`™¦aN\ùïýg®ñ«¸qÌ"5™~ Ò:Ϭúúr,Ÿçî÷ù–™ñˆ…aqHHµqïh€Š¯y–ŸpÈ]­Ó¸ô¬Y´c³�^°1yr2$aõ°0¿Ãa@½‚;IäQáë|¾!ü؆ÉÇn=‰–Ÿ@qu †œð€ôוó‹ó_®—éŽ/ÓKö†Ci«’/•êà- æíŽ÷_ ¿wð´$�Ù*&ß? ž€~FlK€Irmý58Y€8&Ž=f‹‡8ïþœ-°«Æø¸¸NsÖ˜›ÿ½÷ðG‡=$ÄKÿžðá×°`à‡àüªO`Yv?Ÿö™ÄÓüX3@û+Å>ÿ°:s]ÿ~﬋ðpyd¶xûþÀÙjïwþx¤s£—üÂßxþx ¿MZýÏûþÄÑ«¿ôŒ•ÛˆÁzàTÌÒÏ¢á‰ÏÁÀ ý¯Ðþp >ü%q¬Ú8g<ñ¿Ù"†²íúê~ÿù€Ž¶úª<ñ¿iš:ÅÓõ´}ýã`ý…ZèžàbŒñóoˆ,�dÐÒ*WÕÀèmÖÆ]{oü™ :BegÝý\Á·[a^Þ2–Ó4_¨a€2(“ÚÚ?ÍsQ쨫÷¢¼­y~ú*@ï2°!$[Âo>¶1S…�8¨aÄxË9›™Áï ßùê9؈ 8ÜÒVg—|:thr¼‹�üÔOh@�T¶‘«èlBdðÄžd|À YŠ—­€¶ÖØÄ| €®)=t¿ñ£<«Zæ¹C«÷<0à“ZôfßsC�/æ¡MKþøaÕö|hð*G~�\¦ÃØ3”Ðn)¦Ã®á%ƒÐ@Gé�û!×U˜ç;WÈúž?†SA”Ðómz—ÿP$´w®²´hàúIG£ÜY1¶—¬›àAlW/º&ý`òD’=à0@~°”‰‡ñåøfUVå—ûÈ*°«ëýöŸ¨ÇþS²ÙýçL:Óÿgä?§ä?'àb€øAŸ–E(!à,LaiéWð÷kEf§wJ³|4æf&E4ç€�ÄŸÀG€MßCnÅ·¥„"8Ølh7ÕQ¼ åçY™põñxùKã¶¶¾¿^i\2(oÚ†Ûè9Èÿ±\fÔL4‡ÓÕî¿ß$ß…¼ñ‘Pà‡1Â^Ά޾úå�õa€ûB½HâCcèV>¸éüwàLéÑ¿úððÌÑ`¼h`l)‚á¹=ò÷žî}g÷ãa¿à÷i/÷çAÖx¿ !�¹Å$@�<ØbOɰ®]³_Ò·Ÿ®ÀV‚Ôׯ}^Ú{—øÿ /'#Ûº|Ÿ¯¸ÚuËü…ÅmO×FüÙÏÀðZóßòŸSÏxp˜ˆÔbË®sªuÏù»¿ø²b�‡ ¨øÀ�ˆhÇHq!,¾œvœbCPáàcXá@ôêÝ»”âÊî½ü3ß¾ü/×vþôÚ{½üQ^¼õàû]ÓŸ»¬@�l'â˜Hõù”Q®q÷ð™¨Åõ½÷øˆqà�x*uÝùÀ!z±÷É<°^1@þAÀu8‰`„ñØ! Pê·´ëþÀæÛ©ðô&ÿ€µYÛUÿíJÛwOÌ >âãa— Ûÿ‹sûG>I§ùÃ{üW=ÿnß j¹‡Ë�¶)@„Ï6Bû ÂÓ �îeË€¯lØÕó“ÆtAài�í¤9;Dä‚GwJµ**ÃH`_‡Í‚ü P1icÇß[^bÇøûë>ý¼8�=;œ§GMŸsé÷·1Ÿµt΄%D£¥@�ģׄ{Ÿ$’Î#”4PõöG‡ÛðŒœ¹œœ!ˆ‰yII�/ä’|¾wФ‚_ÈÑ:-Îõĉ(Nwþ&Bâ Fø÷€Ü¿?EÞ¿_Ûaa÷¾$™%8¯Ãq~,Ì.ÐÿŠ$¿œU$§+Ÿ”j[ŠÎUúuùÿMJqðì¿_Ë+R©èp™x4‘{ÀˆóA@[åy# –HqJs¯Ã +ð‡ÉÅ,�ðP”"$  7òCïX%®¦O[?«2eZözî‰(ü~Ö_9âaÁx³u �B¯|/ÞŽ*‡ïÖ¬2üh0€{øeχÉ$¼’K<¨©f¿è¥EZ)QREõ*cydä–BœCÿEO‚'÷Z+úS‹N¾eðpÏŸ·@!à,EqsÊ�B/bú“ªám\LD €7MmÿžTÉ,ÝKÂÅD`�hçïvÇÉ$L6pÒMÿ†âµƒ¨%’…œN,’ÄNE†ýK ’˽ÆzÀ�ÛՕȤ@l'÷À€úwSïüS£\�wáàˆ#ž Â.ÜÁ Ǫ›y«e ë+â|³þ¹GmVwº=ú_¾€ ¶å[on P]« <áÆP$a™|_g÷¸¾¿¿û’ÄmýŸßp‹¥ç¯�¹|¬‡ß©ïÿ$N+á—tH7å ýùh>,™"N+ôãá‹qðâñ‚ò\87ûŒZ½ï¸pšœËÀHõ­ž0/KÉB—¼Y1‹ºf Õçš1e‡ –)SÆ,t8³Æ$|8L±¥²e")eÂFw Â>_Ÿ×‡aÀ¨mSôÉ��Ø4íñ˜Âa»öLœÇ „Ü*ÆkíÅoôq[â@IëÁ7üà#×¶”AAxE7H¹ÀÄŒžfO»Ÿ`Lø=çËPVó£¼è/6ƒÞ|? .ïÒöÞú•5ÑÒ­J™½C‚`/i�íOøMî8{;®Qwzê™Eÿ$±™ÿÞ¼P´j¨3½�A›¼4¤Àx C�ͽ!<£ùIG6”+›­.†0¼ÕBÅÝ¡ÙFǨ8ªÀ(pÁ# !wp 3]€z?ؼ; !‰nŒ=å=w䵿{qyÏ^€Ü}‚6çŒ]rG€¯TPÔ„;–À`ü¹½�x,S­�? ”˜äv¨šŸL´ûÀ ©ôÊ}÷�!`¡ìß!¹”§ 6!ÅQ™¿x¢Þ-a„H©,™ÒXôîIÇ!©�gjß�Àù'ƒ‡™[-Cnc¦ÌÎÈ_ó4Ñ‚08ð'ù.öŸžà¿õë€ |—ú_ÿõxáª5æ¹Cà|O¶‘¢RoÁÛ©Ï÷½>äÅÊ|]îžÓ›gŸj?žAÑ–„öøïÔŽÝòÑ?û`BNŽfj¶„¦‚I“ǘF’ë!´‡q!¤Åϸz+ÿûÂ1ðòi”q�à¶ï>]¥�¶ÿÀN¿ÀôøúAÎJ[™#þ×®LibU׃ÌZ‘Æ9¦›û¢à%j=ü߀¼ëÒ/몼SîW´}ð™élMXµ½¾ÞÛìçx$\Šw­‹€ÞpáWÌ~=›8åÞš>S),óþа2ö)äÇŸC¨·€(ÐôXµÙ¿Îôµ:ïM~ù2°rï7à…l/aùÊlÿ‚ÙÿÍö�°s®ºÿýà y?|vžÓÿ½å~W€%hãùLJ# Þn‰\öR�±óÀÍ€L1ôKzïÝ:ʬÕ»z®¸²ý\ÒÜ×<UÖf\|+5éq!ë=ädfò‰�Ü#®s`âä‡�o¿»![í܈  §Âxô JÿÆýJôöð:‹þ0¿ÉÆÙúKx+¦”Ý' ðž=îþú«Þ/¡rƨ}¨vÄ8È 0“¼�4œšþèÎ"ö²çdxåÆA³cÆ÷´<.¸S áD‰‰yH˜ ‰Õïœòå¥08 ×<1Eùö‚è}É£ï ’§&¸°J{‘Y"Ø @ûà VÕ2°N€É5gÀÅ£„ �5²c_¥Ýð\MÐrÅúü™jÇ;û{•*¤þ"„7NNSñ᣸ýòÇ€[¬õÕ6ü.‰«›’�øvN!’Õ}3Gâ–Q£Šlt|&+ÆɶaÅ¡è&5¥�Pp7Äϱ| ñ2BšOÆ/î/V2{ 9E½5Ìø�„Bú#‚`ÁÀq9ÅdQFÆ Ôâ”ÖX—ƒƒ˜ƒƒ˜�ÁÚä¨L ì|ï *h’nãVÓ5ûÀo_¼÷Û¡ŠRCñõ‰— ¡ ²{¯ÕZ_¶ï�å-ë4ÎïáÂÿ]F�ohwR ž ¸gYM2½õë†$—ò£òÿÿð�/ñ¾—ê¼s?b˜½­¹e§ÿàKg¨;Úðáü¾$X‘-I~�‡2ã¡üm4øGÊs?ÿíÂ{§jcÂüéø~|l-àÕ\�§ßúøðÑ1d…D{´ËNk<,lÖíðôX‰…ª†ô·®m¹¬Þ(Ëj>½`´6í§M�™î&î·Úf?h†DŒ —•£T ¢ÏªÔ5Hû¾œ~P¸Ý]ž“ÿAø>îj^ÀòÆD¥\Ù½¸0à» QZ‹m�@¶§ÇýéÄ’%Uÿþ”Å Ú³'w-6 x}¼ØDH•W¿ýÞ:‡�_uOßþ¨Ø^"{xÏÌ~9|÷øo{îHïŠíá>ná¤iÃMÝê†3E÷Á& 3.jnRA–›†ós ÞL*t�{ £IÌ\�­l˜?wÆ[³lÃÆ\òò…CQОççÉ¿;|ç~%¯äßLMp ³’@Ó¤½p,’ر>mÕ/dK@†…_r«5éï†ü©œðƒ+Y+xö˜ÅR¤/ðrY©7ÿëÿP©âj ÆË’ƒG%i~X µ�î!)^gká® YܽޘI+fv¡È V(%x °Ž÷H5~útÉ'"ïÉ99Ðø*´soeÛ8HÁXKÄî(á±R‚7øg±/ôpàXx+ÍËX@kç€ç,OÇ⦇©´žÅÑ—�Þœç§ÿ¯ …\€ùC@%nŒmò†‚WwáòI,÷ÿÜ$D—É$¼“OG*u*P«åq¶¾©Tœœœœ–G9yÇ?}|Ã)ñôëÎdÿ%®ø ¸bŸû@!à,h…EFfÁ á/û€ˆðŽ–þ¯ùÀ¾I¿¥ÿ$’^I%œ�ðû=üÊ^–I'2üuîÔá}Q±w ¼¸î¬îÿ_À—5YÝþ¼Ïú:Ò¿€;ù÷ü÷eu�âùý—óø y¬rÂrJ`‡Lß¹N%~1ßL’ÄÍd }Æ3 ÏÉ@""c//‚‚e�å-‡eÝŸŸâ–SœW�ô™d‡BäL€GùC�A/ïør„á�±b»µá„2—åwa׆(l/KÝkÄ^Žnþ»±Æ‚H K †ÎO>˜áoãÀÒî€6èagèi>;ø€KtcYú¼õÒ4¾ó¼/8QE¸7ùÇ0GÜ€7}ÿÿ‚(  ¥ã€ˆpÅ“““““““““““““““““““““““““““““““““““““““®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºëßÿ¸Ã¾l1ÙZ5v”Méw§ÑÑÿÿ‡ÁÞvOµ¸Q ,{vó~àY›õÖ`LeUYçù›õÖ›KJîóçøŒ0`ðã^úÿèW-ã¸üŒ�žgïÈ àTbò·èÀ‡L˜K9´9V§SúñÐ4SŒ¶CH3‹ÿ�@& *»Ü·¥ÐýÀ À õÀ'ÌV¹,ýÉq‰ �—{ÿ¡ËEÄá´Qnîø@ 084 � ` 0A윣‚~?ð Þ¥ ±�«{ù4†Êbç3”›˜ }/0Càú'þ`½U8&ÇÚ:Kù4A‘D]{•_ægÎÁl`4â}üa 1oüŒ'æ]å0ªoý€�¼¡>PœC7ÿì€8 °@ 88ð|ð>iå`ÚÛwï€?�óE©b4¾ï޺뮺뮺뮺뮺뮺뮺ë®;ÿÿd~­ÿÿW•‰‹ÉÃ~€‚(DtùÝd¬!±4ðBLÍ4ÚŠtò>ÿ€€ßõû{{x`„âÒ*aЙ¸ó~¢"¤IUzð Å™"#_g÷Áƒð€@lBÅP% ‰7�pÍ3Ñ$öÀ¼/®Õÿð„"w2M ‰s"@À_›Žm�ÇbÜ$G¶‡’Á?#Pc'{¤Y˜Ÿ>$ u×]u×^Ÿ?œÐï‚ôyy�ÿæø.Îb¬êŸw×�:1µŠßYþ¡¨ÓÑ_÷hÓ÷·€)€¡åbp„ë"@ H „À & 1z÷ÿ‡bé&W $û÷� „û�èQ=(/wwÿð4Ì戥‰�—}ûÀ†A9…ä¹+Uþ”O`·ñ‘¨ÅÕº/¨¥ÑW.Ñ?ða¬`@ŽçŠ1ÛP:Œ £¤ƒÿ7¶°*„±®ºë®ºë®ºë®ºë®ºë®ºë®º÷ÚÞÆÁ`.@Ã#geiŠdþ�v׃rÅÄ ÖŒûøäQîo!ùírþM/(a�ºñ,®™#þ`   à ñ º¼„Žr.…D’ø&„ý0Tÿèx‡m^BCD˜|@²$¾ ¡è®ßþŽ_ÿ‚¼ýßþxp€Œ`@F0ø.u×]u×]t¹4úWLœf/—.ÜDÿÒžºX€â VOxž?ûÐ0òâ|×Ñœß`!‘ÑŠÖéh<H«ñK•¢ªw÷„`�Âá€È ÈÉ:†¡): oûÀv—);¦Ðå¸T0˜[�€4ê CV‰0 Þ°¼¨tsr¶ÐrÜD O]u×]u×]u×]u×]u×]u×]{£[ ¿‚G»@˜ÛëãØÅ€®×bŠ>t× Cþø8v1í@¥uzrŒ)øvšgÌ ƒA€ € ¥µ¶vÓŠkeg¨Ø1²ÉÏÖÔí®3¶ÑI=ò±dæ,ÜñýmW]u×]u×]{Òö€–3‘¿P=¨²lh=€° †~4fûÃ\£¨ ޝt¿Ò?á�nÈ@  ã«”ÅmÎÇ8>ð{W$#΀‹<5 �­·…x*ÿÀƸèˆrç@:Ï.ºë®ºë®ºë®ºë®ºë®ºë®º÷ÿL¯²AÇDÆÑæ¨þ« áâRÕè ·¹«½ü£�¤1ìGž]Â…ßE°¤W žLÔyî𤿎Ïý¦j3¾]u×]u×]uïÿìÔfOYž€.Ø“ªG¼±”"’i]{0챘g†¼ÞðaÈᑞ÷]ÀX1È5uëÞT€”b©àòõ×]u×]u×]u×]u×]u×]uס >“Çeë¿÷ƒ Ê¢Š?{ÀZ€ó( Sl0;ÿàôÊ®Òq@AŸ»À8©~6;ÁåÕ¯ñÔƒYA€ÀVÐа@¡(G†N‚˜Ãà›KÀûÍ Ь=(øù0à¸7@¼]u×]u×]uïÛÁ­a�Û�Fhšf½ûÀ0e@H4Ìp  ä¬Ãø™üÙA†“ÁüÆV9þøgcV�Î*Ù .€`ùœ÷ZXf ø`™…Zž¸JBú0÷Øg@GºÐ bÇþðe1jï®�@ ˆà÷Ù=u×]u×]u×]u×]u×]u×]uïfoµ¬!ÒA†ïžr�ýTÇÿ À˹rYâb¨£/ÝðMw䈑]Ç´Ùј mè8‘÷¯R¶¿ï @,$@  t [0Š-_ø VÚí¿ðÀ]LøB�È -˜á©ªBÿÀb›klí§_\¤)K®ºë®ºë®½IáÿŽÆ&í7¿³àrľi`ƒO_¼m…£íýx˜+ ?Š¥þýà�+gSáÅ[ÞðÀÈ1„À ëiµ÷½å€Ú;Ž-⪽àð6Ô0° øÃM}ý½Øí P¥`‰o[X@vú-ùu×]u×]u×]u×]u×]u×]uס·ÿcpqðB ÝœWW£±P#ãÉágžÌû3Dñ2Šˆð8B@°„Ä«ny¬—‹ŠT•%A‡å$”J’Pa×]u×]u×]u×]u×]u×]u×]uǯÃËßþ3UÔ`\ÜFQŸÄo ¡_,™„HÏÂx³gŸ†äÆÛÞjjÿဠïTZLƒzAŸ¬]¬ÄžÐà ýîø–`3¢Ü°õNà€Ѐ”üÚf©à5`’QF�ÐØ­ªx LQð’œÿÁ÷TÁmÍhõ2S¸G–ÇúÛE‹‡¾º±nÍ0¾+Ž…_¬ÃyËßãN˜©¿5­_ía2@S¾ð5%O}à€”@˜àÞ�Û0Ô ß�U†åLPãßï°¤@wÍKÔK°`�q�‚Ùï×þ¡ª°ï€£V"€DÍȥή(Ål]‚ÇHëÞ°&Ü:“À&+ èÞ'‰È $H@H X¼2‡´ $RÕXDJ‡7ž`ö€= ¾º‚x%•Že/‚Ê 6‘3mf¿ÿ6àG¾x ˆ*faBÈÆÍË×Ôó!NX6ùX¿ñ}Ýx.°2ÐAFÑ%º€>£y: !Ù¬ô!_ÎâIV“P°7a� m ;½FÝd-¤ÁÓ(X›ísA”‰êu�ΓîÖ8«]ÿDmì4 ¡>„Ü4Å'ÝÂb¸ñ0ß�®žƒá@ €�v]°7Š@rŒÃׇ¼cÛ¨) `£Š¼C€€ H+¬Oõè åá}³©Š.öìaâÓà `†þ p‰¿ÿà¯Äš™°*ê£Â_ÿ  t �£<¤A3 ÌAJsƒÉ"ŒØ£˜A‡]u×]u×]u×]u×]u×]u×]u×]t±k¯Û�ý¸ü/ˆè55JXqHÎoÏÇ…m-øC¶¿„“ ‘v:´]€pÑOçV‹°<4b)þïÿ Ô¤f"t~NÛþ>‚€ð¦Œ(ó”RgOòÿ%¦ž9rÿ?à¡ò…Õõûÿç‡û,DȆúyÿùÄãn2e×<ÿ<²e×<þïÿ ÔpŠ--ùÿ12 õ£©“þÿ €)K9_Xò$RЭX¾¿ÿèËY‡ÎŽˆ~X\…¹cf¹ú-®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºëÀÎAš!lÆh#x_;_ ejœî¦a󂉯_Ï­Xº‹B?stÝ Ü'^+½Å¼·œÕø·qÕñÿY"à¡ ºƒÌŽ[qEþ[íù0™g†~TXwT5ʘ‡0APãÀð‚�ícx.¯=Ç‘ûßþµ¼:$ŽÄxÿÔ§éýÛ&7Gˆlüôd½J‰&û< ø}^ ­/ùï>ü2¡OWôƒôñ|?oPÒ�ŒIe|ƒá÷´ýËN"]ðø¾å‡šŽõœ_à!ÿŽ—‡}Êsá¤QLpù¯uá´l€ë,û¸.Ð-ÍP©pÝõ¾vú7¶ÎzݧN{h¾Ãropëø?ên»;C›`@%ŒpHÏ÷ 9Äj J5û®¥ù+ià/©û‹þª½þI¬µ¬ÃÌ[£µ@5[ëó5IY¹ËÕû ?gÿZ®õ]òCÈ ®'ޝìþ …Ó.¿ÆÛ>"*±¿Ž¿4¦_-l¡õ};Í +rðUØÚâ%Mÿ1cn"-Ç÷=<[Æ×Àmµ;Z¸aê]Pm–6*6µí�D< påÿ™}ø·Œ¯%˱1nëªâYsjXqÃ#,û2…V¹`½‡ì*i™ ˜>ÜÈ,2ŸS…^’ë©ÄÑSß0€Ö†·©ÿì 8|ëSCH ':¦5–ôãðÃG±ó[S m’X…;,™¹o_ï‚3ó£ðFAšEbðÄCEžŠüάÒ� hÇø¸"îËcO4Œ—ü ñ #Ä~©×"mT‹ÛÏá(‰1A,ú°I­�åEX”þX&nŒ­ûáW´^&HaX_KI~nÉGߨ7vÖá8ã¼ÈFõ\\°pþ5jä©¢óEÃF·<<޾Í{L¨«1`Ù¹íFžÂõÃB+›Ç~@«e7äã¢õEA–?øQ»h ñM5­ýØ.@Æ@#€pc^ C€1gà€Æž£€1**Æë;¬ï¼ìhyÝ�‚1!„½§'çœÄJy€H¤š4øÄe›ó5ëDË„IÄÝðê`FiÅÛ矩8fx¸êÿ¿CŒ<΀"óÐco^7ï½K¼ •ó�¬„æK!x[çø »éIíôˆ%ãš)Øen-ᇆ.Æôð÷âmê ¡W>`l¨ Ë&Ù¶²FÒ¿¾ûüâq€°zÕ³‡Ÿ�Áå]ï—xg—Õ|*€Bó6Ÿó[Ü¥ÿ®Ã=pƒ¡j="y‹ ƒÕ@�”÷{OÀ¨ dã2Ÿ^hatXÝ\jRþùàiæíòáqÍ#ù½0ü16ñö_ÐÀqáÅþ_½p…Xën&±ÔOµð¥ölÞÇ�`ó\ÌA†ïþÝŸ}H ÀN¿¯yå{§–ƒ÷„I²èÃ&îNŒ¡Ì…oñ¾ôpÀYï{aĤŒd~ºÃÜÏÿ]ü˜{ý½<8¹! »#í­r%§»üCkÀÃ(2¥^Ø/‹¾…-8"­’É zî£ ƒ@Á{ÝÔ­©â[߃Rß÷ŽŸaÖ_Z[KÏU½µóÔ8Ob-$A¬Ǿ ;ŒÈO�¯{¼¡ä[s†÷‚øÖ3,znÕWö‹ô_© 9‹ÚK×ðšúo}¤Â/¾{èóeV6ò羯m!£Œ/ðþ¡t4.Ïün[n< ??ƒp-êhðØ‹¥p ¬éËÖ¾›E¢K)íÌ f<„oÕ¯C‹ˆ ­âñ”þךo=ÀuSD06ßÞ+CDO¿�ò7q �I«Mh*#¦Î{ÇÿÞà0Ð'¹˜á ]9š—)-1  :çR]ß\™F^ùOÀ¨–'æ“Àç,¯zh,Yf¤[9ÌÀqE„¯t‚‚E›‘îû(âÏ2¿2ç5Ÿñ�iÍâËó\˜uãY.Ïcð‡sjëÝ© ž½œƺ兀i^�êøsÏïy—ß ý‡ÐÇ&·7¡Àßà~7¸  þ¬a,C3`²ªL^’>öð'M—;¦Ÿø²Åm’šëaR’bÖ‘öïtlœ×M?ð6¤·Èk»ÿNñ/6@@ðÍã‰X·¶à/ôð@˜mæ¥õ ß’$?öøÆN<èàßâ!”<ÖµšwݰûUU\„¿ ÉI;÷oø+ ‘´Ú}ŸðáÿHâ]c‰¿Aĸ0~È?  ‹³š{$~Cñ²&@Ë3“Æ­{hšžGüÈ8 ¢«næÀâHB›Æä÷דÚjy)ïÁ®{]§ë¬,°Â*kcŸ¼§­ébe{ù ê?8×Ä€Э/€Hûxsâ�HsIàÔ(óõŸ<ÏýGØ-ö†¥™v˜LËí0 ™—xVxaÁ{ù3iÀ;N˜\³Ê?v¯ô±µä†"ü�5æüS«ºv8Õøo$®²}ÿøÐM–^»•fHvàm¡þ¼5:@© 6Úb{ß—ÿ =}= %E Ögm‘à“[¡}h ¿ÏZ´!†™c#-º]Ä:]°,$ðÀ´õ@/u=\ÁŸ®EùNd«ôw̱ÅÁ¾| ÈwÌS�Ž|ûÁ’cÚ@ަodM©?†©ˆ‘€ �¸(þïÞ õ²•÷œ|Ù»û ‘ÙM ¹û¶ ½Ä3¿÷¥KãfªØe(38ø€è/µ¤m¿íÞ a­ö“Rmÿ·‡¬<#x¶ Š@æ$”õ@\B–ùø°cD¤ƒ–ÍO� W6œ¿W6tü>õÃ*±óä�{œðÒEëú ´ô§EüWÃNo·_ÇûÞ J…Qý³HnÒÞ7I`ë‹?hE ç|Ð×|ü=rÃ4ïŸÖ&CÜÛm¿4¯Öæèÿÿ‚ðR~ OÁIø)?Gá8·AžcE8!à,IÊð³é!n &+ XB F¨ÁÜÄ©AI'š>b• hÊ&ï òBðÔáK½NhvåIA#¹q*±Ó• 0û‰8u6‰Œwµ#ì€Dx¶EótWïþğؾ4¿÷I ŒA ÿ�± ×yÿãojÚSï?þN¶# ÐàD¦fþa)ÿÈ¿`,¸Ò¨æ.ˆÔû~H›œÅ±°i³j ÅN›�""$ò¢®Æ;y5üàÐcîyIÚ‡\iÏÞ� DH€ØÉÏøÀì·+}_{ÿú(cÏ;:†/KíÊÿ{ãëÿÿÄJ<ó³¨bˆ$Dü¸ëe|ý£Í° í.X6mX€Ø 'ü�nù¾ùÿò?ÖuþÝÏÐMáÂb£ÎNÒ-Çñ„ÅG™"ó¯ ©Ò0+É N$;[¯Œ‘ŒS‡"“ùX„%ÿ< LÓäõ)Xí!Äš`8L²1íb0™dm¿JÀ÷¦`÷Ƀ ‘ŒR2¤Ä°á1°”a1°”8ê&XÃË/‡—ãúÄœë£ûÂV6Å“—œœ ._”Ÿ ï¿Âwëä>ͼŠR ÞK•€.V¬Ç `6?;Ìj¸Ij0ëÏLÄ�'œÆ"1€#¢ß¯ß�Їңûî>ß}ÿ0ë÷ݰ-¥G÷¸D®Ã#‡¤Çø† ®ÿÿ4É_¿ï^:â� Å?\þ þóÉq³ˆü¼ÎXeAêל´/ÕEŠï1}™rÄÎUýF¥ÉRE¹pƒœ(úԲ͂ Û“uíþûï**Êãm—ÆC‚îbFåÕ’6H#-´TUœ�€íâ‹þ‚œH�Ò‰t»þÁ~”-¶š÷ðCŒ5;|cÅ {ä¹­eû ÉÂð”B1åòwÁe»êðPæž‚tÆ ÃIšRh(rÓ˜x£ÅH ™ÌóVT’KÉ$¹¤e¼’É8¥†bKþ÷Ÿ¿‡â·ðNù÷oÎ-ã�&IN)ÇÎ.…,\PC‘e=pܵÿãÄÉ,`þnsßá>.gÁD‹Þð™æ2÷Œ&yŒ½áÁÞY7‘ L…“,ˆRP“,‡ï8keØø�Ý0 Ýu¢bI–CÖC“¸Ë»¹AÁ¨eضqoü¾‘‡}äåá.o…ûò’y¨AS$²O<«á á½~?: jcÇ*U£ÿ?ðCTé¢`7 îž„@!à,B‘RÏF*a|˜R&iÒ¡´ä†b¢ÚO5ù#f•ÝìÓ‹á¤x|`g e7§NÍ%á¤TœŸÊK–r?ãO6‘¾Šß…?œ[ücÿdê!‡µ|<—¾ß’&á�‡} EyòB"I  »³¼Ç<ùÊëŸXzàÜ·Sâô�í°:å8�x%ÒÄ|‘"% 2^ôãønZ=ÃïÅ „†�úOË`¼šùÑÚ'óMgh¾LD§)çðê_Ÿ‡RüáÁßY†âPoLUÃ�L!q„ÂÖËñ’#´î™:Xêg{ì¯7Ûú‚‰Y|² ɲ¤LÕj�àÌlCnøÂcf¾÷¼8;âd"÷܇a-~,˜™+û†eØðÌ»t.äX„!rðAð!ó€!¥Â(”N¨%‡âñÉ9bV?x’D©{ÃH2¼Ý1Icõû†¥¯?G÷¸Mß»_yùÞáó‰ÈØ—óý‹÷þ °Ó1ì¿: EOÀ�ßËùÄ~‚o³¡‡Û}ž-ÝùrËÉ× Ó÷25ï¦iB÷{S ÆåIÊ%ïd†GEz¦[_w?¯æØz_à�—’¸…ÇýÆ­Ë [–ý‹o±’!p6¯¸))QøÎê/œ@m@ DAš‰4¤Àx 1<«ñÅáèïPI 8tþÒ¡xWŽ©F6Šü&Ä‘A‚få“-´2Ò&`nv+8H�„¨’èáP$´ú£“°³®aÀ¢’À©t<’¥‡˜Z,¨«³* ³Á47¾bà/;—üL�Â�è)׿Ç/2(º:øÉ…‡Jü’v/I‘–5uÉ|Â믒ùxˆ˜·Ë–ÅðÓóÛzˆ†¯ôÃÁK›Â¿ pέÀu²ÔÅäk]�Ñ…ÏÆJÌ|h ¾wÆ& &þç¬ ,ž­kö‡›BC€É" Ç÷? f¿àñ;ÎÅ�,ïE U|Å@Üì "à zAÄã÷À‡âÐgMýÆ€*YØ @ÿDô>À¥¥£<´OOßÃ0Û€7in¼½ÿúºÅ‡à€€ã?åq�°k|‡þ'ú~R'·Õ¾†¨€z¿¤�ÎD÷Û|ÊaƬqöWÖãå–`¹ïAÿ>>1ª­ûÿ ö¶('yedÿ¾„S0ÝëŸÌø©&Á€8"—]H•{éAÝNW�ßäc).`òž¦ TÆ™TÛ"E «kv‰ãª_·ð0P×ÉïeNúȦª7;ïð6@ckR|BŸx€T¯âÏ?À MDøDŸxh§Ž³Ï_äŽ *öjŸ\ 7ÿþ ¢È, E€²'ÑSbT‚¯à&Dú"úñàÁR'ßèÔ>à%Üä�>!4Àè†õ±Ë_ÿ¹«Ã �ó€+­},Ÿç�8½Ö%­fàÙM:ïûÐöšÜ²?þÞáz–%µ¬Øi“ɪ™ÿƒ«á@ãn>Víwx/Î/Šgv¾°|�^qòX×FÚ°ÁïüUmŸyáðq™rzEvíz͸v>ÅBŸ`ºMŽZƒ­~¬›¦8‹_„ÇrÚKç¾ êH¶·¿~õê ?ŠÿòW«Ã4çM;žýp§U ˜p-`î—ÚO÷íäDá!ƒº¾Ò¿o # HaÂxb‚Ø],lIðºŸ/�Õ|¶ƒ­Ã/¤�6w]ø"|~8ïéê -Bg§½p"}#°kßRõCØyßs¤³þ5L2Lä`'n÷aMn‡{^‹ÆòõÀ‰sÍ^÷`}¥çAþ÷…V0Ó<Þx8ò…‚ÌPf%N½ùJA`|¨&R~c@ë/D¨Ú]ñà&�f"Ã$³< ­Ìa<9xðºkúìp—„†<^thÚô8T¤`RÙXÈKÐø‡™^K Ð÷</�‚% ”&`�K =€+uK¼;#ÓU´™óg+ëºIcòRÛ\8‰w%-µÆ,Ìc ±ŠÐeüuËôNO¨ð(9LXóý{ãÁ'VŸ|ø:l³¦²ŽO0?>M4QÊ9Ì@u{Ãu¦òZüçïÂ…Í”^ð!];§ÎŠÌÚ~ÿÿè�6(RÚ"Ê‚Œ Y"¸éh^oZ\ÁLI`u7¤ž \@Á½´)ë�[çªçUÐàøÈ�paµ²ëbAI°¤gAD¬JzB6Ý\í@ äQEµ2Ïî>§ ëÓãUÍR˜¾>ó!#}¿™ï¸þ¿Ì¦p†ÊÖÓÑl¯Œ£¢o¬ðÅÂ# uß4ïù˜Úmi½Üõ€ÆM#Þ{È7Î!‚mwðŸ·Èp¾�C³˜u�_Ê}u¤ñfσ$暀 ñ<èT¿=å€$|ÄÉ~{Å€&ž¦Æ.0AÂ궉» #o¬Èbå-1àtšLñPI+ˆ¼L •'£’˜,žMd=é0Yf°8O¸˜�³QR�Y—šjBœž[Ã��Ãà,$oðu17±V>A®AxMÐ'"µ›á‹AÑ€Œ.µ€@ǹ-±âñ"lý4¤:bü­½½ð y-÷»Ã¦À¢}nnëA&9ˆÅĤ8‹°ºðù{ëvö×àcÂäœDX�[°IDrÏÿêß5ŽÞxD¦³ÎÌ„S¡Þÿðª°èܭ[„(»ä:¬àndâåU: p°ÊÞx=,Dd×®ƒƒÉS:é½ô}ooŒ)¼ðz*C5ë ày“³mãÛ¶Þ|áPÓ5Å øÆ_÷?k7Æ›áúÝjžñxh”íZÔ7þÀ³ÌÀ½ùÿ€޹ϫ׉ˆu _ŠZݤýÖðytkš~~(×fû°l-i;IúÛÁi®2�ÛÂ,Ñ®wÿ¯Ž‚ïO•ø3ôªómÀBõ�f&0%¯<° òñØEŠ²Ë·P#¯°õîZ 2Èï.»;Âa”aæp2MÏh؇ÿo²½È0áÃþ I# GIŸ¹~ñbG„Cì/fÿ^ÐüÀ÷�ø µÈþ¡šÆxÚ�|)p„ˆÐÑÈ´úMÖÛLÕX7 JÌgç=¹¾8ØŠFU2}àêvä!ŸžÆp° ÿf‚ü~å…ܵƒÅ0¬ÇÖxlä9ùðx;’Ì-LðØq~{<ÈËu6ê˼|D0ðn X²ÍÁÙEä¸7Ëœ±xdá¯4ˆ0À+M¢À©ÿæÜ.°ñÝcðè3¾«“�Kÿ¬þËS£px!€ƒ¡Šh «(ÈÚ¦€(ÉooÛÒÿâ!‡ìÐNdHoÎ�­Ú!´û:ºÀiE˜}Ø­kMßÿÜåþde“2É™y[–L˼3Ã.`Æ8bJ×ù;²õW¬ø“K WX—ÜóÜ<)¤ ‹”:�p®þøUK`u–€ß•Æ@º8Š(yÆÕ¡#LýF 2‚=àz©·wAªlÌÿöƒüR?_6›ûCk|(U_+].Ç�Õ†–DT�ŽÑ…|\‹˜ïŸÃú ‹ ÷?÷²ŠA°þþÁÀ�­ÞÈÈ û=tt¨kOE³ëÕÒOE€Hk߸Uÿæ}2×ë¿€è˜]ôÿùG}?ÓÜD4ˆÊéŠn£ôƒ‘ßÙówØü&Pª„è@Ð+éþüp#ˆ�€VÃë~*“ÑcEò mKg„I%ÿ†  ´Y áxþÀã˜%Lví›­ÿè×óò‹¢ç:ÍÞ³þè:LDiÊ #Â\]§ [(TlH�#ƒ"aǬ7�¹S:‹ýî\W7³¡¯–ÅbKÙÜ´˜ˆÐÚ÷óõô6ӥ͵tÆõau÷­Gû¹÷ã Š�2T¯ ••x<×L! ™q„Ë•Ê&}°:ÿœ`^Od­áÉ�—¤ÿ„À˜s!%çø‡Àé0DèŸË!þñ�tü²C„É(#aDþra eÅ€ ¬ú¯ÿà$ùü%>4‡~XGÍÎp<ÏT§õßúøÑùßðë ó¡oÃþO$¿ÇÁÿ'5Wÿõü20yîUêðÞ¿«ÃzúY…Ç¥6ê¼[Ç}ã¤LX’⥀â§,Üß(¸m‰]s9o$°”;¹S'99T${ká7ŸÓ–ø\�uÊE е‚D\š¦t ³«î­$]Àþ¿¹´—~ð@¤ªu ¾=Pä3ðâ{_ŒÕPÏéÞtt%ßî©ÖF #=;x°|º€pzio::öϼ‘1•:@m@üžÆtÄ“OÉ”o<Ò„nô\>%�‡wÕí èƒÓ¤bUÁŽÕüº ŽI%žümŸã†¨QEÜ+ˆ''$²Cl i¿á˜ewƒ0aeC¯)μ2¿’R¡Q¥Ê¾ê—矄#üòôñC¥É)ëƒ0aÿ“$°àïñ�6d‘áÉYWÀdê¹ã ‰š&Y5*)¢É–Hq‚+&Râºa|9ĀɅÑtÅêÒÆ†ú8OÀv)½P/âc(z‡cõœéÿGðèCüå‘nû×ÿßÀ}Ýw¾w|—7'' ~á™�À|âü x>Ÿ‚j:/€Ú€ žÈ@!à,¾I£xì‘1Ô)0©‚wtÈL�ŒL1É�x|÷•öþªç¸ D•¡-/þ°Ó#D” ]z�P·,¿ÞA2߈bBÂaï~xàp 0ÊÐûÐ7sÿ� 0a.#ïÌýw?øðë‰9ÀDSá«qð½/…†ƒ° |2“ÆŽ¬ìtýÃÐÿ¹[þtW¸{‚D†’ýpc äC¿?8䣿'�äâaÂòLFï7æ�#ŸÃƒzŒ†ãõ Šßm悉4ý˜p™lÍáúOexqÒÔ`[¾J‡õÆLÆ)Q@àG„g‚B²üa11Äh)˜Ã…ùCKPßµþûn,Aý¸v\Ÿ‡\W4>“Û< Ý $Ose -û‘özØj_œ×?tÞ×g÷Ø ‘$E‡ømÛaŽ~1!ÿ ð|ýtc 5{âÈÆ íQ�U”åOÿçèíøtwåyз™ ?_çSÿ¯×9—÷â™ù B_¨±^ïË™GÇ}¨°©qñÖMŽêP¼Vñ“ÇA œ{Ťòü£^ôÃ"Ãk™§@8›Õ|áüŒßå+ºà€Þ¨é-’&Ž‚Q,Åðu’éÔ’Éb¤BOª�Eøì�ŒT‰WMʬºùƒ6nŠD€Ú€äAšÍ4¤Àx )ØïPA 8‹öˆ¥‡À%jN�A0,XŒTºÝņÙdoŒˆ þsÌ0c»*C€Ú83œÄ=+Ä,—lpf(¥tˆÇ Þv po/�@EùØ"xož$1ƒ�mJÀ¸RÛ‚�  ³(¹Ío«Ì/¾- Y7Þü fOv™ÄeFiêÀ¸`1 $„w=É¿ ´…2”ÿ�%׿gïÞ :tlž,o`f R¬Þ* 8ÝD‰»oñy\Y¬ÜëüÖÖó¿\uŸ�“®^×iÈ6Ü߀öNŽõË#kl~/ÕEHY8Òö�ä‡|+V½þÉÆWKUýÿ=Bý½ 0ž€�®$áç–œ‡ÛS%LbSÆ–{õáy„xÑ ¢ËñJQÚk'L³üJrB-`q†8jKÄØN�?!àtKƒ€[2d0 ¬OæŒX "Ï’F»˜pF»’›s�"Ô.—‡Ü¢ð‡à‰ÔÒ`_]dä´Á \þÌ `§�¬#R6 5†jD@a3ÁÆEÂaö‚F¬NMk •^GYÓ©ï�û<-°¶Æ–ž`‚š&Ó0­<Àâ[‰ªÆ,Á“ µŽðÕAË:鸛Ö*Òx”LonA*c/¸Ú¾ëßNÜcà9a¦%?kàM€C &_+Ý¿±*y¬ýïÙÐ}øÆê€³O»®ýßÍÝêö¼£—=5÷|TuLÜc\Î2Àn�‹ÝdÙàŒ,òÔ‘ëÿ�ËvÚ|qÒ3 Ý÷ïÞagŸhY_¼Ns4ÿô*ÃŒz|¿Ù:|=ozªG]M•aŒ`G¾V‚"Hï'ï¨#ÊßìtÀÑä�³ÄO½GÁ”­ÃÈ=?°Z êÜ–%�‡Á@Þw¨Ö&àÇYxiG§` ¾’�þ+ëᎅ¿ƒŸ~û[¿?à�èT¶nþ¿Œ@ûôâ·�š=]t«À­)ô%Éhe®¾÷Þ>G5;ÿù€ds[ -è1CéÒÛž¬K2ÏßðÈæ¶[ÕŽXm}‚2éæH ët_|ÀhØžO1Pó^Äúyµßèžÿ¼7,Îјª¿.¡I—ÿ:{´JYæŸËÀz×3?ª{û €Z{�æóПŸõß|ÃðÔHJ²Ÿ®P�¹©ŠYêñpDPÃûWÒøæ”7q·þ¯‹´ËWÏIÝD †×ÿ�ó"¬»ÀêrÖË¿<8à$Y[Ì-«Ã»»÷’ �àMn²Â:¾>ŽÞ {Ji‰T[4•|åƒ-ZFpü7s¯ \*À ӛ퀎„OÅùǺ ï5UUuÀ8K…üëÀ8íÂñüÿ \4ž Ë)ãˆ&€Ã¦éà±�ôþQHÍOŠ<;éÿ�U>3Ð,%TñQ� ½–c·ªIØh4�™Ð8Fˆ£ðÆM�4ÁÖܵâ&€ã˜%YØâqf矂Xìp.¶nyúeĆ�Ç”ü+L.¼ÌwóøŽóŸÊAHÓ@t—^uáô]œN‘Ò{ä8D5Ñ>’ú�†‰`Q–@:‡ä}’v«Ùƒl¾f .€<,žùœc‘x! '¾`Çl‡ÄþÏÁIø)?'àîLAžëE(!à,¾HÈÞraŽ”bás$ɬÉqñüw$\Tç*p—X¿´$8#€.Í œjA=�Äò‰FíßÊÉgÿ¾ $~›{€ƒõ¥MÿÞÒKSÔG¨ÎÞ價C;Ë$‚�,Ÿ!*ç#-áôS_Ü|×0¹ âzåQ*\ à‡÷¢Bξ€˜÷OŽçÂÛO\ú¸’î1öǺ|w>Üž·ÍØ?rk§æàèÞ¢"Oïùh¿ diC,hB.�PÕxå€ôœ1lçà1|Rá¡T‹p *J“ ãÏ`öû»>î7ÂÅ4zçÕ¿àñO�/~oü˜ˆ““ûÐÿ±\]޵æ�±C$¢—}ïLTY—¼9ÀVí?*Ä»ÔX†ë›À18kòUb]뇼#qœdŒ>“÷Ó7‘q•*AD·¸½ &aDO½pá0Œta0Œ Fuðá1qAuïw %áÔ»0˜¸ ’÷î~qW‰¨‹ 0ú#¸Èð(„¦7�®|½0Ýóÿ´SÔj·ƒú¿NïS Ý¿øñK‡b×nЉт~â~`¨-Î?¼@�~¡KÑýâ'TJ«w’š€]•/·üÑÚ?þ‡La¤z�<~Ixqwÿ5òÍ‹ ሦ¸±^ï»’¨¦§®£¯?Îb¨þZ�…B| ' ® »6¤…#θèûsüË>GÓ¯üH9îüB_é{_xXXã:û©Ä#¯Šö¾½¨:-w$LÖ­ô\Ô^Ÿ tÄ“O˜o<Ña¾ï$²añE‰8 |œÛüõö—¾/^Z}’ýúü’Ig¯ð‹‹»Ë%Šï”’É Š@3‡Ã�ôØ;‡!é´p:'Ês #ÃVk¯M�L2$; f*¯rŒLž?àkÇ¿üSOA‚±Àèî?_úÿü=£º?-÷ÿù2Kù9ã ’X(šC¤Ð€P%™ãé€á11„ÄÄ{ü²F,�â— ’È%±? <ÒÂ%�¬àB‡,ý×O«÷˜r\úµ:}×Ñãÿâ…ÄCŽ¨È™Ž ›þ@4íÕ?:‚O…ÐíÕ?:¼�ëc¯�xôÔêwüýÿ²ÔÒvþ®ú‹^ýܲ‹Š÷Qo/á/‘4@di ËøeýS¾°”yü¯Ý`þ'9Ò§ßðB0ûGÓ¦ïÔèêH™(§G@6 bŸ @!à,&x¯<èìÑ9(BT‡ÄóAû•I,�F>î•̉Œ¨æymœp(Cß(J§™ÚÁ ÞñÎ%SÌíd’\”S¤“×É …³0Ÿ‘‘!�£¸ü˜Ì £÷¶ïÉ’Sÿ¹~¯ØK_pœ8òÏLÓ�G¼8O$@SÆ1:`…�¼¸tÅ |7Ì8!W._ñB"n£€uwðeò]SüFh8Gãü‘**ÁÇMÜôÀ½-ã Gû¸i€^òÔü¼7 Ã0œxÓa‘“4›—Š7ÿ·!�ÔÐÞ›Ãï„ÁÀÉcY‘×ÎkM/hŠ��ïqîÒ�#�òT¯%B¨w8�ÖÒ‡æÞˆ‹ÿh8_ú˜?œpÕçÜ"Æü:°™Tóö!w™"…\ ²í?€¸9“´¿J¿±�’%›¬W~ÒIütO𠵑ÄÍßÙÆ²=rõœÿõ÷Þ]ûýh7Ðo pQdjú{þo1_ÛþÐ,p±u{Òî@«èÔúïÝx>Ôþ[þ€Xà±ù)�˰ubA,x!Þ”…¯w]î¬7ë}…ŸHpëi~Þ²¡w!î¶‹öôÇWÈÔ `�I  �êæt­íÿÿÿ¸ÁV¨žC$�Q—–_ÿ/ø$j³–[óʇHqY£ñŸ÷báÊlÿº÷‰0’˜5¶×‰•x¯Ï8f nè7˜O`ƒWGñð›Ã (ñÛÄGŠ}'žŠžGBÿÿÕðô4,r)Át§jyàá< ¯í©!ê<ÁŒßÇwEžÝ_Ë<q•µžy•%,ð´mz4î¯åŸÊÉežh’Ež_¿”.‡Ñf°m5²1ïœßm¯ŽM‚%V Ÿ0m¦�±Û8ä�÷�_œ¼pù ²õÏð�ËÔ1ŠÂÌh”‘Bû%�ñtÃòd±%�F'µ�Š€JÖcÄc%¬ì¨=ÌfQ�t²\èôC°:ªù¸×‹rÙ; Ê "ÿ‰�”BŠsü;]0Æü‡ž\·A 5zÔ€=\! ”>ÀN't¡× =¦O>çðɰ2xD»–£ñ^xœ—–x"€a'iëã¸+ÏÅÚ. \±a|@#ªŠÅmgƶ, ÁŽTƒÙLâC}AÔ7:†a4¦<F¹ ¥áû: óDX’4@ ¯4ó�Uó„¬×“  ûßO+�8LÓ`ëL­|ý‰bÀàoOšï –Žêß-ta~"MAz?Ê Uvý}=p}¸€a•¹ù»Ý•Af&ö-¨Pþô„>o œðÈÏÅeÉÖ=1>/èMJ!X¢]€:‘�V|v”‰L·½Ë_ñ陦eI/2bîÝI-êF.6,<ÞwK“~Ÿg߆c8ïnukÞxQân·uzì­¸#x=)ö†°#ÿïô8òì×�Yw:iÀ'OG¬žÿoạ žET£¸ñ T�hB5¾ûų ÛóHƒ6·ØÁpXÌ'oÎF H!‰ ­MÜ{¢ð÷ ë3è Æx¿(È»ò H2êJ þ"ÖJ¢c—ppžW…—0ôƒ:'¥·éÒK0™»�“§˜XF¸ü¬0—ç˜X— ŽãQ¡vU -b’É»-MP”*Ö{,¸ ây4=JFüœ…‹@z Ôä5øD¶u{sߊšÊm †^��@teð¯Lg%ø©x|z\·®¨ÙOïƒ=áqÈ<€÷§Iðb.Áä\í?_/•³Úýÿ V €Ø]Ú‘zÀójÕærPvm¤_Ý ¹ »ðMf­^?’BØ›L¿¸9g�6Ü%+ÉQôÿžü¼Õ�úïösNñLšì°65䵇Ãx*ñ(˜^z|&ðx¡ºùß�E®^m>x- ã¡êåÿºi´æÂ¸d8ÀKih=_•º«O/­<¼gF XY„­ú‹{[硺�ï|ßx~ bZ±ßÈ Á®ß¯ @’�1F f O|~<;ˆ†�¤…fÔð´ë 4éáwn>šô}SœýOŽ$>óÛ2›�¥bÿ‚k,94´†ës_öð9ÛAÈéo|&HVœp 9Ó�Ùàlspr_ß €iŠÓ�œ6¼g>ÂZA-,ªÜƒ=þs}³Ä·hÿ�&âxNXaÆ½à ÆŒ"¿8Êo›¿zeÃùXì²9pÏD¿ø”M`T©ëÿP¶oA…�5bð¨`}¼…"Ÿ-3ÀéD¤ªÿú¾ý�Á€ÇLÖ¤ýà’"DT¿þ§ãïl c3Îÿ`JqÓ#Ÿ_¤^0õyþBFÜåPóˆp¯^Y€/rá¼oÂä§ÄDÎãj/G$Çö¸Mw»VÑ|dž?µÀ®îïÿ”…‡þC¤B‘')mœÐÝ<à8ÖÜW ‹N­‚zLñð …J+;mèÁRBK;ËöR‰Ÿcàdv»íÿ¿à d�.ùþïÉŒ‰9Ÿþ‹�ÍïÄÂq„Âf^ðà]/W 0p¯¤ñ(Zb9l(/ÚÏ‚ý¬øÀ¼�„¡0»EÅÒdTZd¢Ð9ù¡+Ã�› ej¼a2Ñ;M‡ �ʽ²¬æ&a,d°ì˜Š°Ž �àO-b—·Ÿñ–ÑÉŽ ‘ÞKò°â™¶)˜]¤¢Ðämh"ð×%qÙKð:<"8À¿q˜?L�A§ãÏ’OÃ�vF�ã"ƒ0âO/\ßmÕXÀg‹×%€,‹?® ¦D0�eŒˆ &”TÈæXL¼7/àƒmyã %Øô¥ó�qć TÙys‰Âä¥þâcpù03Ù‚yk0 4þÞÙoâÇj0wo¿û3p¡ÄH�®Ø=*#< Þ©‹€'bŽŸþ¤ÆçžT Ü‹¸úîŒ0íì|ñðöQ"CH‰59·î½eïj£¯°kÄõuk­ÿxùuOjëŸïáÁžîÿûùú×–QÞ,^Á[–R¼6¸!}Ü .`2ø Õt«‚îéV¢›d™á½Û=Ãbj™£i¸µ@ˆß"&;Q6/PZ¼lÐÿ°uåØL2ð i±í{ -QSÎó¯épz‘YÞÞ�Øíâ˜@ƒŸdåßo½ÿ²;'wÿŸ“)Ìpú§KóÝ\Æš_œ8'9ìþ0N§ã5~EÃ�rW #Yëø?„`G 1’…ÛÞè½~ðrßÀó®"XêcrûŒ“¤ŽŒ9‰âœ!ÁÌ%×+ɉ x] Pî¶Õ‰Ý‡Üâ[Þðõ0dSw?ŸÂªáøR ,ÁÖ’g•}þXÖ0'·)¦¦W¦*‡ñ7 †iyü`þ&ä‡C—øDÇþ¹Þ%ÂÂaöŸ‚_3‚%÷×Þx=£§ÛzºÀ$ç;×ÿµác¾Ó³kÄ ðã ììõà[†Eà'C SÐëB=¹|ëøÓ·™”þ´ììͯÉ5Oê(¯»ËÉrŽ÷.8¸��ÐÍGòT,Shl[Ë{³’&N2qd"†º˜�P°�jtð$¾>á=ü"X"ß::„1³Í©�(:¼º–¾œnµ ±¼¾ gàe–àõ¹`zÜ·Éj¶WÀm@ßP@!à,rÌáìÞ!}L’iø]/ùI5 J°P.žU¡£ª—•!òI,ä(˜*é‡YEÛÉÉ,˜ud —Òäò¨“Ì„¿!OaälXÿL…)φ¶føSòÄÔå ½À L²@PzDóòÉ;–y¡Ç&Ë‘âÀ"¦»°Móÿ¼U÷ãæÿäË$:;¿ "À<èê¿�§_Â^>ßÇÿÔÒŽòs’Y9%’@�ððúy$‰¢�‘1 ¨ÊA›U4¤Àx ‹¡Ç-Ig�2ìŒZf’*ûAÃÔ$. (Æ >ùநñ÷ʦÐh™Ê3Ì€GÙx4drŒóBüîŒ% "œHcð$:F£¿¨æ¤g�ÿóhp?–vY¶vXp">ËÁ¡´>–úZ×Ï*Á*–ù€»ÿ3£ùð,‘\£0ó�ÄÔ?°Û€meÏ[Uw¸¿ú>a]L0aÀ8’¼Ëö—Ù™Ã7ô:Ÿ]ð�DÒê¿ÈÜÛØð@Àq�£ù„°RŒóÀZ±*?˜HÀ$¥ç`”&r²:2ý\h+?Â9 ]��öw6ƒ8?—æˆ4Y¾v%†`*‘¨3™è¬róo‰WÀ-Ë0ðï]ú 5ˆû;Àâ¤ùçœ6µ­/ã I„ËÇ@ =bÄü“N!½Î%™ÈQž`,HÈÖH÷0ûáÐD8Ç«úè7õ — ÷ùÒûŠ3OÚ�[Q²Q¿^øê†&0$Ûw ™­ƒ`“òP{•÷“†PØÖÑÉ}Yš{™q§á¡­”T–;¿[Ä‘ýˆ]æM…X‹œËÎ?àfâu+C/ð8æîi¿»Zì‚Au´­Â™oÿ¿? ýnBú(ög¼Ú»<œ|-1Vÿbÿëü,LU½ˆ_ý'†:ê @ðß®À�\±ޏüP6:±oc ,­[\ »ŽÅð4üV}:Ï»~ƒôbº¹%ß}øÊí “¼XÚ0ô2')¥×·AÕÂP°]àòæ9¢;·g3ˆ%©ë¨6ÃVîoÍÞ _õ#}^w�}4]\Þô�wÐaÅ_‰0}$øç³6úÿÅå�€YY!þcÃ�‰`{ЖòYë~ýàù49­_Ü´ŸQþÀçqMbÚOžçšûÝ‚¤É>†ÞÐpž¤¦ÆX¬›œ»³"[ í‘*÷^Ÿrèþ‡¼@x1uµjØ8¼»[éyƒÄ�]g‚3qïy߈1½¬ìb",.å<ù�6õ’ ³OèÕ~ ðœ–FÄX§füäp5¹)-y7qÈwpBÃ�e»*(-ÄŸŽÐ £Ï¢®T"+~`‘5v¥ƒˆˆv³„Bµ–1ÃÁuôMÂ`‡jK’„;;GvOøp�—!À>\‡Arä8 —ˆžÂˈò ÷¢pã¹áØaZCu¬°7ù/TO/,ð9ÚÌ ºÄZ£±~Áá”H]qUÇwÿ#'Œ¦47‘k‘1¹ñ~A n ¿ ;‹P¥ëAʼneaæxàV£ÆSžü#6{Z´¸ªÿÀ©°S¬ ùIù­Û¡‰(b¿·žü÷� ’¨�¿íæ_Á…ñÑfêQ,¯Ë…©ï€ ­½^Â3ÐpeŽ-ܹޫ¾�0oö#‰¯î&‹m‰ê÷‡ îBÉ‚×б€cpŠ·ŸxKÈÑ�åöÛÆŽ÷Z}í¶ïLÀ§þ!¥ú߬ßàa~yz;äXÑ0ÉžsÙçÌ�µ`(93�«�äȤFuúßü {–ô>vÚx ת @vÂÌ‚v–ŸÜ¬87ä»N <`Í­ýócVxxÀ�¸ð&%LÙæŒ¥Ÿ–ãÆ HvP»[úcíQåÌVr,¢X,¸�qÖ3YEƲP.–²¨BãX¡@°�H(3 ÃA s—_Äáe¹‚|%oÌ8=Ìø%oÌb#à�.)xÙÄÎÑNé cKK …A.æ)4YƤ1‰ ØXåÕ)AƒqŽ?¬I‘£Ž¦1±°EO/]Ó�‚ŒÉ–z]BUM/Ï 5ŠEÒóÑ‹k}¿8*Çø ^œøzø¬fñÄ[‰¡ª•Qþ!D2¦¿akWÙÛí*%$Ÿÿÿ�›lWþ~¶€üJˆ¤©ÿü <Ø��%…RXÕÍÑ´ï›Éù~s {X(þ{<6NÿX rÙà¸N‹�è b©À0rK'fŽÁ åB—n΀t<’ÃP!°3¿€’Ð�P�|ÿ8e\Ð@[ÜÞv"Ð,Û{çëož~ÂÄÉdð�m¹çúÄCMÍ4$x¾?\?�}¯yûbK ±~øbeú<€hezG�Þ €ªF€@š\x46„Ás†K�°9<*”><ÏdüÕ€ÎhÏ'ê²nx-!¤ž¿D¦ào$4GóÿÉ<@îß+3Ѱ?Ì=3—ð@~ OÁIø)?'áXxAŸsE(!à,¸qÀc£€Úÿ Äþ!Âqc@³Óƒûõx ®_ ·$#ñhh-vJr×”¸�åG}ï”|#ãr´%‚IR¦H„ÿ£ÆÌƒ¬‚ƾ7ÂìE éG,L´}ï aø¬>3„‚†x` ~¯¾é°$H$í“Ênn¥ÿØê[rŸ7nÝÿ#ÜàÒͪµï-KgE>‘í䌧¯þiüoù •À‡qž>mœfo†õvt‘¹ËƒŒ5Ÿ|5Ÿ<'ˆp`1£„W¹õ@†ì};êÛü㣠*»sóÿ`­6¿Ÿ˜m5]¿|þbn±ð´Ú¾~t&2$9|¶ï$2,¿ }R»ßð¿çïNü?ŠQ3ßnôóÌÌéçã�&âÃ"^×…¼.âô̲>ÑúÙáxrënx#b{.{vÏ ˜9ÒÞ³°àS F‰0I"ý÷-Å…Üó’çùåAÈ“ó—ä|Œl›óÑ@89Õ›ð8Ù‚ÌÁ“28pà‰’ò�ÂZƒ©ðo¹�Òîe}ÌcE °qüº%ã�‹—•2 ¼2ÔÏº=ׄè�qK%ŠÔÀÂej‚FhÀc©¼K+$óMLH��Æߨr%¼u0âÜ–9M00wÍf]àrÚà_#Ãð–%pÐ-~±Ã€MÓb³›îÃ@ïÿŽrôÈN¡Y²<…Ø|èø>œ�{^0˜F(‹ÛÂë(h_x&Ú™à‡kûY‚¯ÔÍDCi6à—4nn©SÑ`£Ô;gÿ¾xçvxy=!îw�ŒE%KÿºðèÌIá¤/Ý›­¿Âq÷›‡rX’\+Ïúÿð`y-p¼Ïßëû+’3˜MÃŒµB[‚†ž¤±V %\¨«(¯ MšP�¢„†ßQpÅä€k”Œ²þr =𦺠¶.›âD P؉Þü \ùo§å/*3ø'Ä|ÄÊéY‡ŸÃî°°:<§a8hPß(3ȧöF¶`z¥üB­C|·aûð9·-Ü’Õ¯ÍØ\ž*x„oͧ;–EV[¸\„ÆP™ÒB¨!|¨ ×9èFYùÔ ïGÀ®ðý¬ÌÕŸ’tÀUEÂ`ŒÎƼ#÷Ñð¶h«„ew�ÇðĨº–§È7’x$NHÔʘÞ6‚4Ôg¥P�òÂ’‹Ç¼$”$‰ÃÂ\ ´,B�BEn>ÛBw#Ô�v ôOf«órq3—ð[ù�øCüïü„yÀÐü„)xW/$8'ä—Ð7&·€îû>݆‘^·£[Ï™ÿ]‡I’$÷ÿ H)' Žƒ)Ú�ëùó•ÿÅ8�‰ËƒŽef}[úòd‰B“€pš–1L) ÷�óðà]=6%à>m2ÎìãòxÈ\̘™€à]+Ëñ•-·Ÿ‡$gåø(•�y8ȧtù˜Ý>-¬2ÀûLT j ÷> ÷>0/ÔX'`ÆLGf%QM,8Y%Œç–0ÈeLøqK‹ñ´5»< ’.H!¿·†¾(AÆ€µ»±Åït—€«¸þc€%eôúÔ™bGü8‡wè/˜#$ Jx�íþ…0é’H™ÿ³‡Ñ$<‹8OÂŒò1Uì�¹A¼t°òXŽöLÂB­îׂZÿ¼7o^¼8"µè:¿°øi ͈ˆ|àþœ��Ë™.eèòÞä#Ä1¶¼~Ds/$Lt¨òÔùÒjôŸ”@!à,K/$Óîr ~\\üö~‚éã|LG O4˜|’K8¯ðì97Ža˜»oÝ N2K$â?/¼;yKaػî\%”žq*'~÷·ü'½»þ)dTã&IPƒ‚&k*ïq„Ôðä«ä\a>EñÁÂe’0™x H–ô9ÁA3ÍÏ$Æ]ðà_¢�X%úóˆW@oÖÇåä=p"k;5øêvaÓDm ˜¼!q/Á D�¦ªEO’Ia¤€wÎõ­CÊú® >Ôþr/øz+ÍGWÉ,œ’�Œÿ»��ø&$ñ…‹è ˜ A›™4¤Àx …¡Èaà~[�4‹I¿åù‡+¦3÷~^ÙV÷Æ�Ãw8æ›ü¤ðâ =cñ<€ô˜ Üð®Â»�"Õ<"x/Ÿ<Ç¡õž „JØ9‚-Ͼqh~½õ°DÃ) & <Ñä·|4ô:‡ã*s?°C”±p9}C‘´³_ÄÔû( ‚ÌO3†žï„Þ}•~åw¾``½œË'Î,r–ù€=l ÙxŸ õ÷†ï\½4ßÃK„”v­Ýÿ‹=§Wº;ÝC{#IíÁÅzÓ_áp­YŒLjþÕOÑ[ÛaÜØH¸ØCÛšO ؀郠5ºŽÂ@áLÅh;ÔµPt3³ ÚºùàÄ¥œ€èú39ÐoÖø°i‹ ÁF~\šÿ#je¡­ ÄÚÆ+†˜u>Á€ ²/8Ã%Økøl3Só$aìmŸa„eŒ=÷ À"Ïã˜'®>ù…9˜O¦wùĈÄûXÏ0 ,jjTÛ¾Ëù„B ½š@E¹(ïä—̨¼Òȱ¯7¾ÀJ4Öy+:€ H7›v‹{àGã,,µ9bkɸ•ÞØ=†Rþþ%YRÄ|¿­ÓïÿFcå_î©ð!};è*‚Ë{¦wÏtkÿ¥íþ ¹]™~òJö]0îÍiÙJ uÓ¸ ŠW›??À”‘tù€ôIW—úÿîÓå×ÿvyÄãíÉËOÏäÉ¤çŸØy Cwt_„3ÀBÀ¯r5Ç ÎHíey¢4ûÝpÙÐvð¶ „8‹O%yï€îšúGÿÚð¥…+KŸŒ "£Ñj*ÓßxUZäFÿéqƒˆð­�÷ÿ›Â~dË[ƒï.…‚ìÄÝöû_9Q…œànЖ™¾ð8³Bœú­û`�ëóÈ/,Ô=z°WÈ*ô_­¿á×üñmо$ÀÁ +—ÆíwØÿkÚì`"ïÈ`³Ë\‰ ¸°¥ß�ʼn;…Ð'[Ç[ÂÔ9­v9{sV¼æ€9ì¿¢^ë°ð#ÐÃŹnÀ@GâêõØ]éÍ+ÿyÍ­aÚ–ŽOç†ÈKN@ëË “bùaŸ%ÊQáÆ� Û–ú–�,-¿†rûá%…«ñ€`_Ì»ÏG«?P5Öĵ Ãˆ‰ 8½îƒ½ÿ…«û-î�ï}óá—�˜Üz¶K ȨÒõ»ÝEŒ^O|�È\S§õmCƒÊHyÁ!é/v™ž¿zþV`:6Iîw�{ˆÖ›À-¨r.�zsüp̼Pâ; 1±9pIÕÒ’K'€_˜3kéòcNƒÅ]ùØ`bdÍlUjÇAÆb®üñˆ°»¯ÀÇ·’õëCØ:íƒ@w!¯È5).@�+Ã?¬›€x¤¸D°�Ybbà/ÝðÖD›ñU(r ˆ!M˜3€±M~aÀÄ-0N±[ó‰a »!<› I-R'bñ&Çñ›È¨oàØìk‘ïžx]¹ï ;sÏs Ö ˆu ”ˆ°/"ÕØ8K…\aiK—à˜&W;ó à_‰R�‚Èã5óü¸ Þ5#{‹!Âùÿ`JXU¸‹„ø—fúUôâ(Kpækçá ±­À4ÆA~œ2‰D=â-´:25ÉøÖ›7<ÿ ˆ�b~¦ÍÏ?ÓòCJxwÀ9Ê[çë„Ïm?€žë�?`dIaWJ<·àO#S�âPØ!°3°xhÓ¿*@­ Ã9É6$>cþÀ¤ðª+l‡üÚÕ¿~`r u|?…’êŽ�!pŠåÜŠ)—˜�Ù´×Ù ‰q^œar¸™üLH0%»åÛa+�ÄðìQZ '1ë,®²\¸Ç  œzžÀY.WË9ßÒæß1õ¯ÛÄÄF‡z~ÇQlùÄðÈ¡%�©ø�›=÷/ÿOøï=Ü¿üñc¸�"D20}ê#ï«ý1h0Sw€;·ûŽõgi}€Î÷`à]$ü•6x$³™U�üa1�™»‡-°üx£hãÒ`ã ±(9؃–Ä5ˆ8{ŠÈ¼¯$W¦Aª­-¸´¯$aަ'W™x-ÛýSÃd·LGM㺜@•LÇÌAA¦ßƒ%adDU:˜ ¥ý„—ðÒLw¸ËjÄ&ÄÉ8–_½Æ# ’qHGÎw¼m2EÂØ85–Õ`á©Á’œSÄ];0í£b –Þ1…Ø™BíK0í¦iRAÊ€x$- ´ú¶ü8]‰‹ ¸è¼Òš'•`í.èîî˜pN„ÂèLpŒó¶Håøè»¥¢…‘wKõ‰†ÅÇq5Üo™^SŠFÏù§øM5;çŠþ{ž›¾·ÿõÀ Öû÷ÿÄNð°’÷1ÁÄ =ÿ󠽿”«ï€íe£Áë|R§Ý‡BéÖhˆ�WÀØ{ÜGIƒ‰•»Ÿÿòˆ­J}Ê}¸ÂîîáÄU9÷£ôû‹\&ï©È+fó]ôR¥…½UÃ\{ܲøoðÞ…ÈDþ~$@‘TnzÉ/”m?ÛÞRÿà¼&4ÿ–½ü§ÿôPó :& W{Â2sÕï¨ #êp8‰t·eÁþ׳£¢³C‘0G"+ŠmÂò`¬D¡ ‘ôì6hÍ¿ 0‡Ñçy ß±wËÀlÀùŸÖtÀ’KÉ>bDÄp¶¥¹0ù$– È?ï”!F‹âð­d‹ã×;–NJÅÑŽ H0*éÂX}#*ççG `–‰”´üê¨â×ÿ‡°á2K„¿†[½Hï×ÀGßûõÿL’Á@C˜Nx0™æ8¥á›wã žaK»Þðá2Èr¿ðÚÚf÷ž¸ØìO‚˜;P§�½;eá¹x�Õ¯Îá-KhÅht’K=ÿx81 /“–{ÿ�ôòz+`Œ'û†Ó%DJ•L+%IÂz“œÐôðþäN~�ïd ø}’"')$¾Ib*tâÝó糟Ø@!à, ,fºU d†..f6ËÂõ'‹’T=¶@ûrß•wØRk‰™ƒ+Þ„ah‰Q†.ü-’&äÃâæˆ Üu²w/‡ám™ÒÿÑüøñð +P„Ý·�«^‰¸8†²V—÷@Ÿr[|=“•sœè¢¿˜ßÑ_Ìoù ˆC`ä­�$nŽœar)8™ù$AÀ¦Ê ƒð?ç <A„a,à ‚°¼"ÃÄÀp½¤oÚß€ÍæHßµ¼8LD¡À‡Ÿÿ� ;ð�–‹B—> ±WõŸ£­çh–ÿŠ~LD¡�­ßý‰,|� ‡¼�…Ä·w?\I|8$�…çŒã©•rñã¡FsÄ ƒE-/¸p')gØ—¸À½ô)™�Ýàl òE\¥_q„Éfopà_x¡,Zdjm2‚™ÆB�ââ‚âÇR£.£¦Z¡~WÊðàT†æ–HÁœÂ„�{»„Ö]frcp”.\6( ö EÛ¸À½!ý?¾R?éÞ)„Ãð%ÎYþ“"P(B|¥,*À0{¹�¸U͘oöL[Yö¯“—o”‘<¢òä%熄–ÐKS¿yC˜pP}<¡P1Pa�Á\fN‹p¶hZâärç$L‘4›!ï;¿±]öšãd 4·’’Ò¶È­²ûX ˜ àA›Ý4¤Àx ÿ˜A=¸FªP7xí O%û‚&nX3Æ+bSØDŸ¿ŒÌ¥‹>%þ& X�¯ö6j[岌gG£ó«ß" R�h4m.‹A¯-õb)fÐMý† @ü¥7þÉ¥ËðÞ „„ r’kÔή¢ÊÑS��(…3óþ€ãB–ÈôY'‰³ÖDD–ÈiIåvXa*+áøŠÕ O/ü$Æð™”ûø°R–ØY`¬¼‹'X°"§-°°ñ.buŸ>ÿa·šøÿÜÀw*î¸ßqÅÞÒ†=½&ø'zþÑÀð£§òl2Qx¸yŸ“]žìÌ”ŠígÊÀ¨‚C ‚ýIü ÜèýÿƒVå¼(å %¨#ÜC$°üóÚ�†ÎB¾p�p8JIa{ü÷—øläóØ&¡÷(ðéÀ3Î…VþóÜ9PhPr3KÊ$ø`¿µdˆ¿xØAkÿåï_ATÄòOméÃÍîÈßžP )T½$´‚‚þÙò0È2øñè×/‘à rO_øÿ*Rô}†x²U|~Ÿþç©zŸÈÕ�ì µÿÿ/~qä4��J2fæY‘þk«àâë� ïŸ~ÅÀÑá”»Œß®C:A†ä…[¤"Ùû€]§„¹RkÝàŽŽÝåöà•ˆ¸ûƦžåI¯w‚~òûl$ yǸÀÚ¥à ÂíÖ“¿ÿ�µ5‘ÉÏïÿ1  @À‡ÚÍ·ï†âÀ�>˜þüòwöðËÀÃéVÃY‰"Uýð_O¢áÀKV§º€«0ÞT$Ék€§�©K?[S÷ü¥öú±¬zfƳ·šûâJ>ÅôŽNm¹›( <jÛžä8_ô“ ˆ!ÕÙ³ Ù-ý§ý½Ç¯ Ní•ôã¼a[³dý„ë/^`óæ:˾×j /Î !x0C0Q·EE€x­ù‹yï6ÙVךÛ$õïkÿûêe2Ñ@ilˆüö1;þ¿Pq2Xü6àÝ3›þþúHï7sŸ€#á›U¼ì�P »óÀ ;°bdïï jÈê€*ïÏ�!yïà r/1\ꇔ–s)ÊÍùÌ'ծĀÜð�r&]Zèq@½ioÂb:Ë/<ÙÉüÝnaà V•¬À¸VÇÎì(+~`‹JVüÆ"hлK&èœ'¼¤HÉf€´@[8BHÛˆŒØyíD6é¬ð,F‚àte¹�™ÜÈËs!1ÜÃhª®ðæâ`ã¼—§4ÒpÄ8†x�äJçk� JBX€š-ÅUàJsÀsœ. %åÕ‰Öær둸MóÛA‡ÝñÇTv“Þ< œëa\óÿщµñ”„v”©"ïoS޶Ï$ÐÈmÞPgúÏ? 7á&Žkïÿgÿa££/n÷Šá(:ÝË€œøœ{ž€�Ó^E•Ú<û…)MB&™ê¯?ÚèZM'Ø P/ùå�ÿ``dÏU6µÐ[d“ìT ø£ÄL#÷¹<11Póà‘æ» †])À¢òtò>ÿÜp®çÚ…úåëü6h6hÊ;qßÿê 'š}ÿÿ�R84‹­Ï�FHçG_þÜNŠ–ùÿ^È�iŸ{YC × Áñ‘A;sá½ø]�!R¹àj̇‘³áDCäòr]ôÆŒ‡§ÉÞžêp˜úaûÉîÄ£ü-'Òµh0€<Kù¿p€,’¯ÆN¯÷þ¶ �×jÞìhOú†~Ëd}öö%Á¯€¨ôf°±Í·¬ Ž”7ŒcÍ·?•åá†ßÛøW/¡!÷¥Óž4½N¾Wµ²ú~ï8�ãpÉèØc ÌŒ°Eû(Ì‚uŽ6»°ÁŒ�;Üïóá×p­ÐàŽ�à•„¢Ê zž·YœÏÔªy·í߉�™0¼¦Áà^hw�'ë®#¡žß+"gûþ‘nSið!‚D˜_�ÊýõÄ9 |¬‰·ïÿÿžÏçQ÷ç‡o­í·aÃÒ†”¾l8˜œ§¼…x¾yûj¾lÀ âùçüž ±U}{S\#ëÑàqšá#|xÆ$ ’Éö šK˜«øjL¸Kª·×ÇÇØ‡¤’À_‘¥ñá€^’à€/áO?Ä…\1�þƯø‹B-œ„ÈÒøN¶Üóün ‘üÂffçŸéù!¤sú-_mt´Õ×úÿ„ïÏ`TI¡UpÁ˜”œ¯3Ø�yíà�MLÏó¨ãO¿� J' ó¨á'ß¼.Â9•8Z"¼$K Õm[-Í’1À«j ú,øøàd†@0ï3Ïñ!”عèþ5@f`öp$BI_ãZlÜóü.• 4øÖ™�Ï?ÁAø)? AŸûE(!à, C‡›ÌëÀÝ€.vYòEÿÔH±$qžxX»ù¥Ÿa©<â>0H!Š™%�é5ðºÉââ¥CÉ5$©oB4TSÑ œZQ�ôùpäY¨r,×ár6MÂ2ž ÁƒÑü&_þ÷0›CFiÚ6‡ ÓÆøˆ¸(FÚ¶÷zR¦´r®þxKù‡Xž›Ä‚ñ  ¾¦ocðUœ`vNÿ0èô÷•ÏýÖëE‡¼9àÓÍ’×ÿ¬„ô_­þ65kÊa‘äw<¯�B¤­¼Ï7Ä¿z <±Æ«ëgîà0äˆøØðΩÐ�MD(°]‘Œœ«ÿù,‘!á€5ÖÀ÷Ø 3ìñ÷×€K®�k¸ R @~GËç}5ÿÿ…o½sú°QËîømzçÿþð7O¯Ëï–"»6 üÓO«ÿÿð?&M:§û¼8L@�¡¡¥ïoøn_�‰œê¥QJ3þ0TŒâÂé�&øÛ'šç}�2 »F¯<�læY2´½»ËØYTé;ssvÛzr qœ&%½ñÔÎ0¥Î|/DŸ”.Êë2ÿpQ¸–²j²5�Gñ–²”|ߕᱺ ´GRà.³iøÅ}Lƒ¦ÚdŽúiàÜÚÄ ûYÁÆë²Å•a-Ç/Á"äÈ^q, ¶ç0�\•»šZ§†ÉºbXËã è.â¥äˇÄHMI‡1yο†þÅÄ�Ëîý`�ÿuòq! .ðÛý�Ôć€â¦K`îý¿r)J¸¤Èv™ã"žuàýL4’ÈpºHX[¤S õtAŠŽ/Aûs±¶ å±]Ø�¸°à)Ó2 çxpé˜G.^ü4ØF[~VôC ‡ ²Å‰ÅÓÑV2*šyÆ”.Ö1-˜ ë!¤JðÒAâë xj¹—~@4ˆõR†ªŒ»ð8LL`]ÝáÆãÿ­ÅÁèøÉK~ö&�Ëóž?ëe§Ûÿ€-òÙzëçŠAp¯Q–÷"ÀÙïÿ8¿�£ü}õ’ðèìÕˆ 8*�œþë�ï¶Ñµœ¬orÖ§ßbY­ûò CqÑÿ&ç:å.jþ,bàpMNoÈ)�XâpูÉùmC#,Ê|ðIª¼“æU'ÏÁ¢mºw�ˆq@uÜ6³ 4U,X ;9x¥¬öÉ,¤5]v~d0jÖ"gÇ0%3g $Îë*]¿ÇW¥d›÷@ïTè<+„dcKä„!x„·xë[°�¨…ä\^„A+1 x™%�)U¿Î޵N˜[41<'"ã+eÂòpŒ1 ÛE`SJÊÎ1÷’IìW|Ì!žtÀ-’ig˜#w’"'’y&]ò×ñøž#ŽžY%Þðù$–`îþ†t ¶±Óåt;_ëGN„0C…ÅŸ`4àb’��Ì$ ›íÿÈÇÙ¾!Ì+íÿÈÁ÷•â²ýÕ°Ø[Áþ,À~ý¦¾©ÿÿ—ü²dû“ÿÿ¯$¸¶z˜n}Å“$°á5<`W¨«‰‡.(T=È¡y—¼¬ ™æ\9r ‡£/2ïDæ Åþ&y£C9æ‡ydÉ–@Û‹òá6ÓðŸ[Å4a‡à˜y¥ ‰‡I$³ŠÁÁɇáë°*ŠÉáéüœ»£¥E ôYœ¸òK!Ngüpa"<>ç䈉Ë"ï¤{ r^fž@!à,dš/ŽÔd/ÂXfX(ëÒèA“ j"XþV¼LCÁ¦.ºùUø]HOžXÈŽKˆÃ«G€� ´ð8eÝÊD £R�øX³Ú‘@Ÿ“¨/Õ3ïjÚYÿ\9À.~TϽ¨­¹{âôBÈ)"ñß6—’q+ñ¤~#dßßp¸V tmG�Ì€‡þQñ²öoÇ&�6�à—Ç÷¸°ž çþïZë¿ï_€'™zuýxp!¨º8…ý%ÎôÎ-LU»þ ªªïø²d‰8§ÿ½/ÏÃ…æˆ+Çè0™£aÀ¯ÅÆ<i ±S*‘�y$â¢QcîÆæyti ˆp#ÍÄ”g|LÏå÷œZþû(T°/wê ü—ù.&‰AfLÆÚL4™ƒ©É�xÀ¿&6< ÚþAÒ4˜õùÊŸç„>û …x™ã¢Š̸À¯ÈAgÁ/ˆïWSÌEf»Ûð  ðXh Ð$ÀÔÄ9309£,� ôB*gÀÕoØe`¥Ì>îø ˆ`®m Zæ¿pÝõö°7©cº @Jä` õbÙ(ØÇþÇÌ'щ¤}ïAÿppºÎ 7ŠÀѦ#„R@ €€– ¡¦øZ+¦îBf®xm½�'€ í ýuÒÓ?0ëÞ±]Wýx=‹Ëÿ5ó8×Ó:¯Œ±–Êáçùf2âa§“ {¼;I”{×øIá0J¶XU+Ôƒ% äñøNĉTñé�($ –Szþ«­V€‚G8At” ébËóq˜Å¬ pTˆðk–ñÙý~�|Ð ƒä@»?þ€À•]Á..LOýà€€Ú4 €O à}Äh€Hô Fß•&Œú/�ëˆà h7§ú°Á¬žÉS`?˜€Ñ¤¾>bïàÆ€6‰fm7¡�(ï%xîàëw`£Ê3ü6cå0cgÿÿ~I!jÙÇÕÿ <x �„;ëöÿü0ýõû{ÿ :ébÖ¿ÿåï*Ô]ËýµG�¢ÂžûÀ) tL‘³ZµÃÒ …I…H²%”§|  øp}ˆÀ®ýÀÌÍ$m’I„€¥�€°œ`DV48“VÙ׺¶@Fé£Ä‚?†+‰"jÁ‹¢êØ�¤„M | £ 9Ëÿø+ú¤K‰&aò�‡€`§‚€ï¯Û÷øaûëööÿ :ëµÊµJ®1Ü'pÓ ˜˜}à#ÊŸLÀFú™Þp@Â\Ô"±‘1c/ c$0`Hð,B/ÏOþ»!°p@ U(;L Á/ÑV¸šGø &[ÔeƒP&AÞäd` ¸Ñ�]i¦ðmõVƒPÿ¤ÏoPͦT2[ðÄè¡‹ÂB>ÛRÿI§°[�×}ßÿÿ8âFnH¿ÇÁ„"±�;�ð5¼\ 7O’óÀ¤�íâÅøT°k®ºë®ºë®ºë®ºë®ºë®ºë®½ìÌ}ŽÂ„�š»¦�“A+ °8Ò“ÎO÷›£ÿ»‚G»@ó…72C‹ùcLÝÁ˜Vþ1«ÌÏà 6À@ 8 f%`ex¢—"\?€¸´ªUcb¹¶õÃØµê�È€g,qƒ%Dè:€\I*�H%%;`ÿ® £[ª©‚0ÀÕª]u×]u×]øR�Lô¦;]W;ŸÿûÀcNˆ`Þ[RYÒ?�,<ënQ‚_÷9ºïþÏÅÌ‚ûj$Û½àöC ϚبÓ�&Ÿÿn "ဠeÀÌ¢Eï2¢�à+Ó®îðÀþ¢`ÅbØ 4Ð F‹þrfÇÀ6W§Mwx`_Üjf/ƒ�u×]u×]u×]u×]u×]u×]u×­oöø˜B#î¼óÍ,aoÿG»yø~G8`­ÖçŠ9ñLò·?>¸#K@8ø ‚÷‚ êñ4Ó!pp Àà€à~XŒ#¤�, Àbà€nC7õŒ½Ìp³æ …ïü_¸¼@¼‚«ûÇmÅ×]u×]u×^õJ|8rÝöÀ�¸1cäšF»ûÀ–HÞ¸Ó&ÅýØlpÇ}ÿ½Ÿ l()ÞÁ(@! ØOH@a ?ý 0à Änö@{ÒZc D–€Ø`ƒõ’]x,ÿÿÿØ«ŸóÿžæÛs]u×]u×]u×]u×]u×]u×]{ÿBö” ×h{ì©<µ�ÌG Í^ðmðJál‘Ü€þ‰3Ê$$w anõ€³�¤“?ýÇß ¦¹Œ>Šç£×]u×]u×]u×]u×]u×]u׌ô¦–>¹I€xí�D^ š%X @×¾„ ÚlŠ4ÿ÷š-͘՗Lý¦)JÎ�>¯ sc›yïÀ,l À €,uù�~ð�vqt ëͪZ½î€¤ð¬û[~ð,ÙCH7 5¶µ«öõ×]u×]u×^þßf6À¸Ùà`åô›æ7ý0—QG§a€$tƒ«^wÒ~ŽÝ«5à`Z¤prEc^/ÿåÆ¿à0@‚ðS+¬1ؽè:/8$¤/~‚™.  )ÛÞƒ¢\ (Yh,^ý—`ª“þÁs]u×]u×]u×]u×]u×]u×]zžŸT¤9ØEY ¼£ÛÀ�¦ªVU¹÷þcGï�—Zæ 2öeÉÕûÀ\N¯€Øád× Ò|€°@ÅÀ‚UU© iÀÈÿ؈³œ £C¿®A ªªõ†¼@ú£ÏpÂ6x/ú ?ÿôÏ…_¯ùÿšë®ºë®ºëÞÀÛKÈ7ÀÀ[r€åŽ1b%ÜàÁ¦°„V _ðÚŽ¼ÏÍ©ÂOø8à j¡?Ôz‚�f µº›$« g�ñËŸ;ð@x0€@€©€ÑŸ•ú¯úØR`Ü`©)Q�.àº[ðP³¡X £7•J¬m…&P§¤ÄƱ¹@¾y|8yЂ,.ºë®ºë®ºë®ºë®ºë®ºë®º÷Ù�ØšÂ&ýmÂðžÅQ�9„ÿÀt÷Ãs[˜ÿßG•Ûà´­Œq£j¢È�@¿�¬™±ü˜ T¹·ÿþ^� pZ€ß©!ô„�¾ ålôIåÐýáè fi»ÿÀ2ø¦A Ú%B .•™èY~P~¨/·f¼Ž†+«®ºë®ºë¯Ršg¥1Óï/†ª�È¢Vÿö`M€ì£m&+—¯ƒ×òl•>ð%«�ÕÕÒbG‰N r'ß}5®Á‚º`ï�pÚx*…’›ç��ýÝÇû°Cþ †§…+«ÙÐVéx,XÛ¶UaŠb’%-^ð @ ,I0}=geV8‰žì’1ï=g‡±˜8›Ïz뮺뮺ë¯ÿ°æ>§…'«Ñ´øKŽ,‰�#Ð~ÆpÌHµ:ð€$ˆL”J\ô¬Åó祛QsÑ´Åñóೆ*þ°þ°ð¾“‘?±Oóo ÷K=뮺뮺뮺뮺뮺뮺ë¯! })ì}¯w:€xå¸âØILjûf-‘Æ�½P$:ÃL ¿@9�¥Rˆ_U�¿ÏN¾ßûÀx˜@ €xè%Â�{8ê^x)à 2 Ó÷¯Î�5ÁHB+ê~ð9º u€¡ÿ×çI=u×]u×]uïÿ³`]ùPh �ÒßÞ‚!Ûð/½¬?Þ=öøA5¤aÇÊ�6è.M!@ëÊŽMYí{‚à0@1–A{vô„’p)ì³÷èEÂÒeÁB�¸M7Dòq`<1ÃI·ö+"±®k®ºë®ºë®ºë®ºë®ºë®ºë¯GJ'®qÚçݽŒ;W.ÿ½x �šñãÊ„û> 0°Ô&&m¡Ÿ²hÿ´ dC•}ïÿá @!,@ò\B²*¯Æ½2¦X¡<Àû.û�à‡ueZBJD°t�(Âë7œ ±c¯¯ôþÁoàI-m8ùq¬ÐŸ¿xTØDŒþ#SMI bÃøŒ"¢ ïYKcC]u×]u×]{ÚÛùø'@-¸£ÑxöŠ›!®l©ƒ#" oî[þë¢Ãþþ�Ô DJ>à‡ø9@X"T–lüj•* €@(‚@P3à´fá2•PÿµáY0dzLA`Œ6¤O8DØÄ7ùˆåVßö¶“ QI9€°€ ¹F‘l €€KèÅ.ºë®ºë®ºë®ºë®ºë®ºë®»\g<ÏÕk2!ó‘~.ÿ„ËŒt*G7¼ùn"n?5÷ü@%‡hšñPN ?M!¢‘ò‘U?¡.±xƒk_Çða„á…BðÃzõ /OØ4Vÿ:®¶Å};qÿ~‚n�Mô[ß@ã KÂ>cÛšº™gõáàD @Ò¡Àã‚ÈÛèaþjnÑî�ùa‹ånÈAQ=fæ»ÿ÷E׿ÿ7‡|ï˜ 8™ÎF�£a¹€öV0h„Pùþ „(�€¨c Ë ÃéÂ2"F¾y†Ë ÃéÃ:*BJŠt×ýÞ+T᤬ôF‹ìðòŒ®†0Ù@Nœ€%3�û¬ó�oäMþƒðûñb˜.*´ÐæÌ´?sƒCq®M?þá0(H@\ àbA‡iéÉ(*A€˜<²—\@]×`l�0V‹Ý"°i6ÿÀJ>§ÕÂäÝ�WSaØïNó{³Á’e5ga?ÿ¸ ƒ]†Q1Ø&üÿ˜¤É1ï«á€•€gz>2ìÁ¡ïOø BHzöùLà@üí±ÁÄ+ø\É /'Êž½ÿýເ=8œÎÏ,Úérò ú(  Í½à€F4 FsñŸ€Ü1ˆª±¬’a°‘øY¸0†UX[¦kä[¹'Í(°¶ óDÙ§ÿ€…Øÿ±\�ë`¶›Ô(Ìß°;È �úð¢Îº³À;`GZp7}|{WŒ[oÎÜ9[ÿ€g€àø@@@0뀨€f@ÆD‚V ^ÑüÄ�X� r×’)!…€3 v8�FdƒW·ˆ£Q‚+ $ÖäŽH ºá™¤_ÿ‚¼‹È�ÿôp€¸9ç„ÀâÛ,ߌj£ßïþºë®ºë®ºë®ºë®ºë®ºë®ºë®—W¬Ð,�e0J$͈Û_ú¸Y¶Pzê–þßá …P@Œ÷xGCLž(’oëé–åžð`…Ȳ_ʺ›�ú @ÛpË0@Þ,æ¬wO \V·©JXzCIcn¡ñä)r¿Øå…~ à $ªg˾|,Ð2{�AvLÏš;¯´0Ue�åÜnÿè5À6B~�ÆÕ.~2¯SalÌ1þ¬`=L Û‚ ©‡½,×P@Ôð€v1%L˘ÀËëŸ~R2”͘ÀEt·ß×Üô‚Æ�)�ÛËnù‰b�c§‘eïíÿÿy/Ñ(†É J!@ 0A4 %‡ þ‘À€ ‚?ÁMB@ŠŸQý4 PXÚŸù¿ÛÕô c¶_0"Êû³ ï#í€Ù&cÿxaÎÁ�¤ðn�©‚Ê1ãîÁŒ]L`„Fü=UÇýº›ðµ^L ÿì^€@ .’:÷¯�ìJ¶­®å›:Ý øÑ-%ø$  €€`8'¦W!Í *À V¿à܉™m‡|€Û ¯,xßþƒU¾DžhôÍËÙ‘4ü ˆå¶!>«t ìm2çûþž&5 €¹ %!p–à"‚#È =÷_�Æ£@±¡ì€ˆûíüïÿè5t!FⵃCòc€v �»h7Fàfüvÿîýð €�ð€àŒ ‚Ø-`d¤¿· i€àpßø8Ì yAœ¡n i�dŸôõ×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×^ äAš$l¾ì�ä0ÀYêG€îÈ-9J¨7üN5š!¯o ¸Ì‚&ž9ñþQ(ã­ErØdÞ’v6Kå4A8 pÈÏ4 ÁÙ3æa¨üqsµñƒXJC$š�¯¬~"¹€~&)J¨B¾dH¥Ø,„c]¸Bü§±`$šxóóÿ�‡ ÿ¹kðîvlíþïð„v† Ö­Æ/â&Î Òž ùª4 8‰p�ªyc8a¼Ü¦[mÐþö‰×ózÓÉ›+âaò4é>œ=1Ô¯vÿ²Ô0àGøÚ¹ÒÌó8…ž,_ü6iDƒJ?„Çý3刂$sÒñ›ž ˆ‰q¹}¡3ìŠÚ± 2�D»§÷‚E ‘3ôbÇå¿È3(œÕ÷�yÄLºç�9©÷‘;gi}Ë ‘ñh�È‘”¹˜�˜GÍ2ÓWÒjý¿>#ƒ5–¡ð 0É|È‘Kq5N¡ð #5¸ ‰¥‹8‰p¨ƒÃ2�QŸ•êüÏã¼ ¦8æþìØSNyâHiž_0 ±‹)V,àQLœóÆ�;bÄ0�‡‡¢ò°Û�NS2üõé<Æý>ÂFx»ã–—Ë â0ëäÎ#êiü“v¨`}¶ÇÕƒ†ôì0j£6o¿¹ƒôq€’x_lð7÷À ‘"í³Ã+²*„�k¢ž×P(ÿ¿XϾ߸4Ö³µgïûAÐ+wÚÅX¶öÙžcõzc?²DDŠÒ騳IzºïÿY\ŠúW…šOÖýïüù,@yðOÙGp‡Þüðç´Ñ§ò÷u?缞\L�ÿù|#Ðf@¦Y"ÿê�i1~sòó97èÄmY|Bÿê²’ëK:g„·ÑÄš³ »\ Ë�M?ð# ñV^| 7ÿýû÷ÃŒA éW×¶½$÷𠓆wÒÐÄD˜À,{�î€ W%k° z�`˜Ê¥;ݺ$óêH¨ðÄ$YJ}§-¯žþUà¶OÈvëü>à&ƒ4.%G–ûðY2GiÞíâ@â<"ï.%Bë:þü&$y.ÿ¿rÈcÈcú â± ,³�7×À‡Ô~´�=)Ïà F†Î»óÁ‹ ÔÃÐâ �›Ö$;¬Æ+(�»%wœ3í!}-ÒÓ•)A³¹wü:C¹*FbêµÙ‹rž$,ýŸ/ÿš.âj¦ÀÖw˜îuºlz8U˜m0 €àYJ|OÀ²_z.ÙhÄ…Ï+¾iuçž_¹�£‰éG,ðàÿs€ö-+YŒ|Õ"QV“"@âXtŽäbA‘ÈtŽä,ˆ­ùh¸Y ‘’=ç «ór÷ºða8€¼±Ã¦;�ð}�GfbÇM|ÿ£*LÔ^ƒÃK K;&cÀ ¥@ò%*Öx€L¹dÏvA)˜ë¿0GGÓ=ó¼Z(@£‚íj²U¬�“ÙØÀO¾Ib,5ÊBï¸� qzäXS�»Çž€íõøîü÷ü7 ºݲÀ?=Y¨üdwoþ…d¼OëÇ7ít.b¦”æ‚Êø%„¢?#à!ˆjoª÷û!Nèlôï±�ëÁ 5È:¸<ç|j[À�¨+@üûW4ó¼˜ÙÐåñ0�Õ³¿JE!4I9À!%öºþœ�€ ¬õ«gú¤) ¢H§˜ˆ ou~œÏàÃ…þz°»È­óÖ›¾´Çˆ0 ÙçЬûÿÝéy×ë[ѯú?_j¹È`A”“ûYùç}¯€¥é ž=òéöÿ!¡b޺ȕý}®%‹dÇÐ�ÿ£ÁÂø€¾ 3…˜Å$k.t_~eYÅA7˜,÷Îy4aV³ÀGÜjŸ¼ï;@ w›?¢(.£äQ{µF�yOgÓ2]º‡ˆˆhB1àü}íç¹ÅìªÖÞ: ¿Y–µÿöþÃj,€”,‚ÅÚ…€HøyÔŸ^ñR N1°R¤Áo•*R¤Ìœ‰àæwv@Ũ—v™�É cÛ�1â�×|Aù写5Æ’hû´Á-™Ü‚%fšn–1:ƒDR¸zŽG 9ÅtÝ~`ªüÇ_ˆ'NnŒù­M¬çŽms|ClC†BÈs9@ÈuÏ“wo6›�AEôij߀dG k�zU®x|+y+Þ¹åG½:Öx|üÔ·®x“ööP»¸$‡mr-PØœA èÀ-€ñŠ÷­¡èøõ¯\‚ M·Äˆøþ³yîÏЗä&w>çSñ‡°XmÃ"rPÿÉpÒOÈÇQ…i»€›€^Qjá´ž¤Þû2uÑîû7B:+MêY¬�. �wˆ÷;÷ÄŸÏòœ nd㢳Á1Ue´›úO±ÿT ,i"ù^rѺÏ,�°-±þY6öŸ�½³”ÿÚÐÊþB5™à ûÛÏ"l¯¯oyÀæ²þù„Åû7;Ù°ÀÊsy–ѯHïøyÏî'w<~ P‹.ÐÂðøÊKk¢^è—#±N/9?¥ÛþÖa!µ¹=‡™aT�q½®¹I¹…¬Hvþ»óTo‡ÿ$9šÂO‡ÿŒ†õÐ pºiÄßuóÀb+ìÖ[ß§.6±~èý/ïÿÿð;’d- 2�ÿóì«,¶'›ü°9±¾ZP€¢íRs"‰ÍåÌ ¿âTä”Ü}èà›=X­¼0Œ}®�ždú¥ÿ0ú›óˆÞh£ÐögwôN¢›†fxö�µ/âa!.r›DÀ~šazÈõW?ýQ«0“½¿«Oÿðp¤´ aeÜg?I€ (ƒÜÊœöûÓ=­¯ÇÔÁü8~¤…Y ó×Ü,‘ÖõÙð»%“5<‚¼JGçÿjx; ¸˜±çÿ²Æ‰ ±›éþ¾ÄÕ&G|È+àsðšCðèE¡ðÐNA®IqèXh€4|fÓÏì„ Â¨8‰t±ºù„€<¡mô ¹ÏTƃ5uá†(êˆàà�\¥W~ á¼zX!Ü:KÄŸ %€Áµ¹}޶Aèo¦$å>4Š�ÙÿH&Ô_6ÑñýM !³í@è}7ûðû‹ãýìÐó ¦µ°X�¯(HxóìµFá]?6$4b ˆ]¡@¡‡5ׄH+ç|Òœv<ó¾Á\¯�ì”8$*޵ZÔ@3>Ò`j6ÂùOX#Ýè,E5Ü^©ÑÐà- <É;ê-p %¸û=Bª_àFÊ�|wóÛ4oµ?� @© K𢊣§Ûü¦%óºm"Éöü>~ OÁIø)?gèü%ÉAžBx<y =îp޹ ¨ú•H†áƒàAÜó?;§e¼‚c!j[Ïï´n@±#§�ã‚è+ Ch.òÉÙoEÏñÄ Ž‹@kT^Vô¿Mï¹™k‡X™Ä–<<õ>1À#ïIð!r"WWÃAQ¼†Îª¼ÎÕe‹ãá¸Â+ÌíVX¾>PºÒäc¤�ÿe! w,a…ãÂ�+>yÇö¸H7kó€ dº|À>�ËsÿÛó ×gþňœèð}S=þýàjGr|wûÿ‡E@�±„ˆ�‡ä“$) Ãq�Y¡Iá¼Or¯¼ï†Æ Ülpd  ²¦ ¦�ò¦hº�×Üø:߯ù#` PL²4ÈY_ów‰Œ !‰¿ù¢¶ÿ›Ÿàƒ%sàÂW> “ÄDÆä則&¼v”|X]ë²±³´Ã’4‰/Æ[¼¼8$šˆ¸˜À¿$DL8T% àzð ʘÅÁÀ®©Œ\põõ ­f.íf_k3ÑÈ žÛôãÔlŸòûîrÔ0ho°Üˆgã?Îï[ÕÞ™$€Ùhù�IRÏxåë=ä’¥žðÙ,,5EôûÃÉ¢àT–2–³ýwà%Œ"™>Ÿ2¥Œ,#)Ö/ÓÂÿ<ÒðØÑ!x‘0Ò>có�¨!ñÚ/Á@3Î;E±3‹@n Nçì~Ëî|ý­âÆÔHˆ/ ,óÜ7Ò߃l9oݸíq‡BäiˆâOãJyÃ-†òÄׂ˜´íŒ»¿ñ{£�DnH§÷–žQ^öAqÓ šÞSˆq"Aßü«pȇ-ð¥º1 ÜS]î„ …¢ûà:/ÏϪ¶‹áàº&¤„h‰Ô,HYž `ˆl¼-㣩ÆD.ìÎ\ôté…‰< �”-‹–C!‘®L×ÁØÏ‘Õé³êžatÀ’KB ™""|¬ù’Uß’.*->>ïÆÜ’öaÙ‚x¼ï$’ÃaS¯ÍϦ<€Ûà âW’Y'å� »¤dÇJòd”çW1Âzñ³ý7áÂd—EÅ“$°á11�^¢®,Íï”R/˜'™½õEðPµEðó2ùxp%žY£ žhp‰a ™Dœ÷8¿Ÿû/À ßÏÿ‹&Y)ÿ"±¡!‡I$³ùÁ— >tuøÂòƒä’Yʸy_Äå“’Y'#þ ^0êrß°°¯ýPø²ÿ‡‰4ôDêHˆœáù …iÞ�Ù€Mžc@!à+Õp–¦‰¡EEì�„7¸¸V:4½äâ!”Ÿ…é“ÂBàÆðÛRÇLÙ¥‹GòùsaT³‹_À&=sã—áÔ?•põóÿ©À =_'ÿ4/‘(Ht+Œ­ï_�œpƒæ”óþ'‚G†ø)΢2ãß÷Ç硌ûò¹ò¿r xÍP(£ß^A=‰ 8R€þ¤¤¤?´· ý‹ý¿íjT3D/wý¨°žFñÝ~(oæÿ¿ÿ„Žï�ÿÞxp™"O(~Q ¼{,´ Œ&H“ÿyyìÜÝá‹p õ,'LѺÆCa|œð�!@rù2ÁËi3<ñ“ËáÅ3 B†OÌBÉš¼ÿ<óÂðuÂðN^' E¾áÀ½|Ìü{²ßyBxý1az¯Êrü…úÞù|a=Ã�r|T"Ðòfv¿™èÉàKí.ÿ ë� &ܯÌ&DÌ཮ñï¾ë3“Ÿï™÷ƒ%ž¿[óûW¬wïT¨Ëvq§²oï—à å·J{þ‡� l FÖÈ“ÚBÉhA ƒêä%<ôlñx€–=‹O½͵Œ�¦z‹¸Ú¶°²ô0×K£–'â.xpGÛ˜0Îë¿0àD?ÜÀÀoµÏPq—ËÀ‘ $J#¹"-ÈtŽä8„[› ¦ßÀ€\’©ç¼‘:{Æ…\ñå,è<÷†ê¢·ø}«™ñÇb»‘þáÞ<è”ÚºÖxà&\óYèŵž"Ž%-oóÇLùòYâªM–xeAv_ ºSsÀVÑ1Â11�©ë–»¥bX€ë…Yzìxå!,{†ùÂè{èö7|ïÂ(@b÷uíüMdb°—¶�qeîPFRDZð%XJ_ÕÏÅ€ñ)š{î_,Œ)×6½ ¿ÿü ºä¯QßëèæëúYÍqk"4:ðU¸¢˜ÅSøPîל�ÑDºyçûž('§aGˆµìG úTWRG¦û*\¿:“~¯.\0¬‰ÃxaºÐÛ~÷€õ³q}^0òðõ›ê÷I(ºY¹o´~(¾Ëu@Á«aK¸é`¹O¾¸ŒYH�Uæ+if=äb‹_Ö¿|þ‹ê zÖ¿+þx‡!0³f}tãeò½Ûüî¼àz=Ûý7îÞü%Ѫþ. ^€‘ë�5¶Ú¡@÷&æð9‚ÎÒ#0D ¬1“‡�:¡-p²ÇšžèüQ�¶oŒÐ¡§T”ÍŸ€¦�yê~œk6oÜÁÇôv¹]ă~_U¢Z�ã§àpy9l|>1—uÀï�ƒÑžªC: ÿ¶Ùà‹FjŠd`™¥«|=¶ZÕ´N&@óe#ÖýÃkó<¸JÜE|o«æ ðãŒ9€ˆ\Æ�þ€_uNß/õX pIi½^Ѩdàþ l•ª¹x…òK?�ïô»|AôRZ­¾dêêŸó‚Œ¹[¹H÷ÀâïôéM�Ǿʑ}!.¿ÆÒÿ¾f.)½%ü€'¾e#kÏ/ºüaa`?ÉlÏýÀ‡½‰pÃbïIˆ0"¹ÿm·ý€õ_ýÿo#cM@Ûá5öëÞzb5ŠùÏ>I"Tçß,ùânµû»ž!.?ò=ñn„Ø�¶þûëì2‰BwÛ¸`ü@v‡º"’_÷ᇀ[Bm‘ðû 8vöD¨UðN3�â[Óó±Fþv “4³Gº”=)¿?øò3Vy}b€íHÃ<ÿönyî çkæ4#ôïÅ€——ÌTE‰ ¹É|‰µçX6 ¯˜à>Ãïß��¿0xäïå ¤:fj Ÿ_}àùÑ‹ !4FƵ¶çŸà3 *k¨Um·<ÿOÔ4ùjfy}‡\Ïy¡¨UÓn$þs±ÀkMÍù�xs“!žx€MÐ~IË̲½Ï?BÀ@õõûý>àœüŸ€ˆ€£Až†E(!à+Ð)=ÿÀ#ßO¼’*.a�ø]Tb ¼¿8È Æ¡ÉÍCOA‰è”¼<Š^‚WÏ"—É5¸=ܶBM/ ôˆ ¸92@rs\p¹ s HìVø.Žù„.¨‚!Ï 0ó¸í/ŸYdmsÏYZ¾pkoàK.yâK+WÎ W±žu ��lÈÞ¿ÁŠø»L:ÙCŒ xÔ°üõ&ú¯s€!E3Õýìv>ê�E= mýü×îåÂvo–»Ã×€ÖêßW"ÛÛm‹ós¿‡ç‘‘‰ bl‚(¡¡qìü¼ñø' €';n_[7~ðýè‰ÍgËëuÔN°Ú"œ ß°y[þoÍéÿo m‡|ðá1'ÿ€þ¾ß'R²üñ„Ä1'ÿ¨xþ¶eýxŸy¡‰áQ+Ÿ¾ñÔ‚T¡q`‹±| ¿;æî¨ TdIJÜô�Ü€p».oø ŠÂ¦‹!˜mxÀŸ� 7ò;›MñtÛŒ6vˆ Ÿ¼ZäÎB³¢YûK\9áxÁ> 8/ »ôÇò·sŽ ¯‹?Ùæó'(Óò¯Œ™åáÀ¾ì@Yc�Ý&w¯0j¿‰ax”ƒCÒ—ç<,ܬ£¹:qdm¬DáÅÉ�Á©çžâ³ ·2«¹†ÊÊKšùçåzŠs®ñÉæÖ`à{;q€<‹]¬Á>2·æ0 œ¡qfüÒ�n°(1C[ü‡‰P�åë‘"XõäÁÌ,}R­O¸BL8ðK²Âð›À~ ¿²?š¡¡®I‡‹Tûîžø½™µ� V»ÔóûÚнb+*a‘À7Õy�`�yR„ÓMÿ|?ÜB‘›mŸþpðHÜ\4Ÿ€.évï�dün˜ p["?�ä®À÷Ú‰ÈþárW pŒ �Ä}¨tÛS¦Äó[¯jòD$ÒÔé…äK…ü@R \žÔ?–×úΜ¯%ÎŽ€lÀž¥t½Ë<©áÙ|dÞy‚§ÎIæîIÄ®›�ßáÙž1þƒ¿ Âa$$@X#€vš¬>mx|àl¯e¬€ùýïÃs¹mçÚ€¯yýïĆ�½XwÀö×ò5¦n÷8xaðk†¡Ú«�±ÿ<xÞ7�ù~�ÑŸÇ|ðá2KL’‘ÏáÂb"c ˆœ‚ʽ°Sb�G$®?LÏÀp%Ÿš0™æ‡òÜš–,™d …]Ýÿ‡üáÒHBÃ-ïøKÉãá¤]þaøSþ!ÉÉ,“ßð��ø†b¶èØH ‘àØ×ÃÄšz!ÑÔ‘:ªt@¼—�Šü‹¾f,ž§@!à+Ô“ÀD«�£ï\3~ $—ȹIÞ4ÖIæä–Iθ%Õ�äÊxÎ;Ã+÷à!¯($F;Àš§öÀ�ö¯þ0 ?½ÈŸÚç+ø�+}\"¦üþÌ_+ø�Ê…Ú¢!ðÐBà…LªL0-»Ùõõ윱s:Û~}åè]Tĉ  J½ïãþ¿,tõüüÏøާ¯OûÛÄÉ)¼a2Kê*È=î jzF÷Á3�~uéá²gš0™æ‡ –C>ã –B·xr@¸EÓü’K …àG˜ø5˜õÿeß�%$’Î%*ü%y�ˆ$²rK$â?•K18"ùÏ~‚a��Ûmð2J¿»ÿ€š•Rwÿðù""uDN”‚»è/¥ý5€Ø€ )Aš¬4¤Àx ÷áHqÀ!›EøÜÍÉxƒ®ßS€H2íܤðÂ…îl�+MÀ¤yY¹À·<2z.`$<2#ôÅpwzŸ‰¶~B³äCñ„_ü° 4ç-W•n?È*ŸP°€UÍ£²g4 >¼WÙá�rÆ7®ú‡!¾YabÎ ê0ׇA‘ôPÔÖOÃ-y„_ûíqŒBùƒ@ÜK¾p ”¼Â“սո؈�ùå '4»½À%¯ÒPZ�p²�ûް¿ÀAE%š«|ßw‡l˜ë>ðtQÍ«“ÏÚözõŸÿn$þö”0à+¼£À_rÓ2W@³Ê�ü3R$ƒÞ¸é¡l20M³ï|! DÔ8 ÃŒ^¨ @é¶ÂD?u»•_‚Æ ïcÅ¥âàÀ*©¹à.ü2€U$g<µÏ>Á¢ ¾šgí 2Ê},5Îpeãg�Bl°h¡.$?`ÁÍüöà ®í`B×äQþz GP “±Fîxÿ‘wÊBÜ�€€Öé} QM9àv&³Àt³¤Ì[ºì¡¶oñ„-ìÞZ‡´Taø#C4ú».ӽk>åf¾•ÑåjísOƒ«á ãOg�W×#ÿ`!Þ¢{§ˆðìÇ„gŽÃ¬»ëîíÀäï+%ëë?¿V5TŸÿkOpa<!I÷ì}`8pÇØ`ÞÀ•/šR«ìmíÿÊýôs)&XþMIz½±¶ÏßÅ~ú1”‹'ž¾×ë}?õ¿<¿¿ATxk/žßïVÖPäº-o{ýu­Ï /êÍĤ>ü‰d”jsýpØ5:9>Cq€ Å,¾}~(ŸNsÚÒKf«Ïþá–vo{Në;CgøùCQÿ"0œ4„Î z˹ºþ°B;íÄqúÆBtëo¤ 'T‰ó¾·üåS®m@¬7t�÷î·ü¾ 1ÄPYüg¥ãÿ¹PÔùÝ„¢^hxA_̓^‚_Šý¤oÿ_AâVZÓÒ7ÿ7ÝKÄ ïèU˜7·…ø”­“û}ሂÄõ'ô¯hŸå÷ú› ™¡W¬ jÑ?è¿ír>§õmÛ@Œ� s�œü«,¥ § Ù`vÙŽ‰Z›¥:ý�9™l¨÷§ÏOê¬t–lÍ×uŽó3Ûåeµ>™ü¬t–ocú<Ðá]Ê#(,òú+Åèäh)ë�®y¸–Ö»‡ ¦QçÑëž¼·øƒò £3À¥Jª‰Íå°“­+^ë …œÖrÄûœºð-`u˜"ÞmÓNM[ÿ&ñ·KT"F³ yfÈÅüvæ°Êv&ä$(%˜û† ÅÒë1@;œ1ê×Y‰?užÀ•^T¬óÜ©XóÚƒ)¦ßëk©˜˜0&“;±d”mñ‡ÑsIJ<÷†ÑÐY $âÿ€§Rq¹ô pí€â`NK ç’óÅY6Yàq‰%Œ�é� £yâª&ÖxQúEPi™x_~DŠ: 7]lôx¹ b: _[=…rxÙà:¥\Û<÷ýųs<Ãkq"Bè{÷k¼Ñœ?ز“š³ËQ6ÎóÐ3#nEf×ÿâ˜7ý-úÀf­Šå’7´*³Avh¹vöÿ^A¶nÔÿڨ矠7·ý§Çý¾ö«(ÿˆxvr“e{ $þ+<8‰Ûä¾Wê‹;,aÿvQ€­¶2ZQùãÒ!€‰„HÌ9s6/ÔpêæJ•qàEùkv/Ú‡_ª_`aÂþ ^`“šzK؉—Ô¬gñ  -ó"oVìFznDÔÚ_Vï}V æH‰ûîë¦ä_ŠC”ÿ½úƒ> ƒrM3£–üYm3·@é(Ä>«ÚÿKÖöZÜ¿*Â�¿¯H[®/ܳ\ŽüŒ/ýáf‘ØO²V°²ë�[¿™QápvJh·¾$ˆXíf@}(‘·®,¢íùŒ\‘�w°3$KM >bûÏMû%; /¦²qåΦÍͰ¦K‹2þÊ1!Ûè`p*l*3àvpw¼ûˆ–3¯~xh,ºÂ»‘«’]Ê#ì,Š81û€Íñæ o½™ÊˆžÓ̰¯“€<Íç½P,ÖcSc·/cû /Kß8™ö¦`³ÌF´\&.ÿ &—1k%oü8F˘nÂÈÏé=t�[µyî�Wu*€,ÜÀZS-{\ð`#ÄX{¡¹”ë¿1–y¸XUo=´óÞªT­ç¡À|$�ï|Øe1YÍþ"p LI!&­s2‰óÿ>¾œiî?¶oÏì8�° XÄS_æªy9®wÿÌ"µEG=ºkÁ–]6GëŸüÇnÊG=½~~ 9=¦§Ÿ�ù£Aç·‚š‡™Fæ-õFöÕÀ�|¦nÁíÐû¹Ô—Ĉ’Ä8pW—éIûâ_Ñïhh¾]e£žàIÚ°}²øÄAO!Ê­QÎÛãÈÒ@!'�‚¥]·?ò=êâÝJ*g—Ï€GŠlÚ�ÇO¥¥¡¤³p(ëgïܯêRi¿×-¥¶—m®;ííh—ÿÀÇÿ=~àßø¿›?…B¨›«ôZßW¬?vp» 2 RׂJJ3Çü‚Ƈ…Kç¡g±>ÿGw.vÆgð}úLJ 5‚ô"°¤¿êQ‘œ5à‹Œ©iŽ~|þx2·Kr£Ð¬w­ˆÏ¹£¯t `À“D‡bLAfhNrìñGä}{h„r<ËÖo@—³ùzh„=‹‘zÀà•Âádèþ(ÛÆ?EF ¶£G¿ž �¬Ôö$:+Y©ìc�=Þ_ÅÊ¡²“wo”D[*¯ÇÒA‡ÿ?ð\Íþpô‘¿ÃUE¸Ü�F ,!ÔÁPgéãúÔÇâŽ-üp{øÇ!…†{Í�ÂÈ<Ã*fCzbÁ-u9ßIÏä¥\ør«Ÿ ØÁ9|`^œ$(¡¦VoŒÔ‰¹k¿€$0ä‡Í@VEñr/�áùL²>àïcéæ !!°—Ùîìw"„x^ÚÄš"Aàí �‡²þaÄ5¸uPµêå>¿Hò¼÷ýíâJ¿*Iý÷h¡‘D|oóxöÿ–UûïáÁ{>\UõÿÇSƒ\°ªûg†o€­jƒ£ :÷œð³wÏ9ü.®bp¨˜×|>¹`€G¯@8rùeâƒIpÓ\“¶šðœ\ܸ`wYÙÈ\¿)ÖG}¿ÍßÈ*‘sç fŒ–& kß>ÜÜí(4òYpð�–A¼ø¹óÕ@àécžNü>–0};ø^Û_ã_–e„¢ IùûX ˆžét¼‘Ïÿ‚G†ã<wÃqж£9øO…ñYI–ÌÔ…Èï„f’íÇYY–ø\�Å (œ\£à8Ó–Ü2+)3Q£ÁÒ4Ï^HÙ­Õ¯rQJ%AÖ<}rï¢ýÑ_ëÿ¢ýÑ÷Ü,å øXÝõûó¬…é/üÕÞc�è¾¾¹èÿ«žüíÇßýÿ†WÁ*PrðE²Ž–ßõéSávKèG•†‚œð½Öhƒ09‰gÀq4ijá}¦"Cª÷ÿ€©eÞ>RÊ�ïLJ ˆ!Cmãf;¶’¿üa1™å`8æB®,» 3Ôu«óá>‰œ¢àùÌ6' &¬b53…ñXOxk ¹3¯Lb”§úŸÁéü6å¾0'Ëy—áÁûù˜!³Ö[ò„ÜMëux¹B¡îC|8ù~2ØÁŽ ÓûÞuŒ ÷¼˜©a±t.Q.f öÅŠ6$P]i˜¸½ªi˜¨Œ‰†Â»1WwŒ&6c2þáXª#“´ú+†Çñ7!Ås�„ð:ñ a5�׉„ÄØiŸ¾ÿ”46–‚Z ã œX€Lˆ\z/©Çô¿ ’ÐhŽ @p(ZÑZ¿ WYôµ\À’ÊÇmAè%$D³‹**Á?±x"ðÖmü/ß V[‘ œZðó´ù"d†‚· @xUà›óºpWDã…s ‹b!‘Á#ÂõcU^ï:àÛùr¾ÿÿ€X™érŸwÿáà¶*2YàˆKÜé…ÈGá(æ&â6Ë,ƒä¢0õD9¨|É— îü!ˆ¹dá¶u âX®rŒÐÍ ×�KvøOuÛså߀؀žë@!à+Ùy$—ȈGÌDÁ}/Èçg$óU:„µÜå(•~Šž&IT¶I8uÀÿôw ÜSûýÀ 5Ÿbo Àchd·…M°x^Z�%ž°‡Œ¹ø ÝŸj|™%  ø ù øîtaÓ$±„É,8LDLcœƒÊëɈ‰D= QQllÍãæRy D$²`'ÉžhÀ¿s*S&& óÅr>?ðÙ2Èq]áÓÆ,‡—ðõû¨+áë÷Ã„Ë h.À|N¢mËÙøýËò?$’Ã!FN8�û„_1j’q|«¾Ùá.Ù±$–IÅ?ä ˜$ð~r¿ñ|X¿§©Ó$TNªtt@¼‹�â½N�0 ´Ašð4¤Àx ü)0[jï€ ÒŽ4NéXU3Ýõþ¡¡& 'Ëuò¼�Eîó‘z  ¶e¯"«@Bx€™8´·ùƒ¦v¬Â‹à”nÁÇ–(ù}ˆ‚2¬¹AçÁÀwjÏ!p”CÀµÒ�0 :A�³°k/< ó°A€'ø|¾凜ï5Kò·¹`oúìwÛâH„d`�³°ž(œe~B¢À°!>}�aÒ ãã¿ÄÕÊ¿ .HG˸O œ°ß”œðááθ¾#à,*IU–®< :ÄQóÁàb›uo.×…òÿº†7�ùõM^ÀH~�5Z‘—äx’ɰ6pê÷„ð%e—t'ÜÅûKã!‰ðÀuÃÍÌž§§€š±»"~A#c�8»áß®�JÙ›?óðeKf@•\¥¦Ž¨WžÊ»[zmƒD …_H€Ò¾ve VçgÄyAÓyØx"Ëâ¿èä3<Ë�õ׉} à +JOú¾$È05T 9†þ¡·äaytÝù’í ÿ@ 4¼K‰àv¶†Ü»+~ÈT—iwúÊJ=§ÿ—Ùx\ËÑ¡D¯�æ<‡(Ÿ@Ó[¥“üøð`éè­2Ü–Oð:_á AƒŒš‹ –€$·&üà%2¾”¨$Ͼÿ8(;P%̧õê_-ü*À�Ån€’êg·‰?àÆóöá)É.C’Ï_13¬|ÿ†§M>ü¾$r5PU>S”{$>ñz¼8­æ?\º[’`R²ÅØs`-r¾ãCŸÿð¾¡�…Äbâœè›<¤[¿öÿ ˆÅÆZŽtÏI“ÞùÿÜ2ŠßI'Aý[ßõ•ßóúÉa¤Kí/CüËÈI‘c±{’ÖûþíüÂ_£ZO¿÷oñ&¿ K¹‡ûï—»»wDº#v7×ì˵Ì|=á:ƒ0•yo¯ÿÁm\[ÍwÂ{Zéÿù€Û/̺ÉàkAé�y­½{^º�üļ:Í'Wò ¶‡ö·¯rAØë-^ ±L@ Š�sO6E¯’OŒÁÀQ‰ì)óÁ€ ŠSª4óJ½ô‰çÁà³Ø[<³úû ¤cÝžTà´Ï®| ¸+kÇ€MÈK ®_|ñ ‚\)wäØ]«Þë 67Æ 5!ñü`³R„‰�DàHõž�F× HòÏH!’æœ,ƒ^îÙ·.VE³àƦÓ0HžPgþÙ﬉¸ÞD ëmžôü&éŒóÓ[¼÷ñ|óߢ¨,’S§:p 'óÑ^ÓK×®ú€DCYÇÄÀ�ëúï‹À†æüÃVx`MÃÌW)h¡ÈÚå/ÔO(Y_O¼«êÖxT�T”žMk9<és¡tÆ�µG¼g~Ek�Þ{�¿=§ êšfÖ"¸W^ Ðɚ̩e®ö €‘Íf$ˆ„L×pß ÙV“Î�^�Y£BFÓ@îÒÓqï°»ÀâÌ*ï00—>ZéU#k< âL¥k<)#£Ôo?±/<1ë ]‡›f܉oMñ�~x¡0ð[u»âm­g£€$N¹lôtŽö˜ír¹³ÀtZ׳Ïxr«7ž{Æ×bD…Ó•ôïs�ð©™/7“÷ß/ðÁH �~‘bå{|ƒVF+>“f©BàÒ/ÑÓ¶‚\h¤ñtß+È 6Áþö»Áà _w¿Þõè ùŸÖŸß/øU^é§»åâß&xW¬Ž1ÒQŒ{LŸX:‘øâ!ÑeÚ‹­š|ûãS0‚•B¨¿ÄH•VpX5Djßø &ïÜâDm4¿¿%ƒ•Ñ L¿�‡Œ‚¨Å»‡ÂpoÖ=Ã�ß#é¾Ç²� �«A¼“%¿äP8¯F½�« ÞI/ýú—Õ¾,ñ!% çÝ�Nî¶£}·>È �iÜ7J¾j�>+‘lck:K·°ÇõÙ�¾ï}¦6[QoËö¦V²ö•>ÿ_¾ ¹Ör�‘í¬öãØ9…˜2܇šåðÚ™ýùÿ€1%[ÛP¹ê­sÀ€u �~è| n-íëž3„ÃKz1¸Ø Úðñbê—�J�Ë<ðYŸY�§Ç&!/ƒ¥¡Û‚[jc ›[ÃØphÛÇ{o³nðàÑ·ŽöÞ³À¢`Q ‹="6¹µñg¤C%ÍAgV!v˜]m¼ÁÓ¦`Y@XX@˜ÖÝžö!\Ø ÙïbÌ`Ak¿ß¨,ܬGS8’4Ì€Ô@ÓG˜p""æn§?˜(IBY†¬ð ˜6v ÷�Yسߢ[ "wÚMú܈%HBª#ÒYï‡"ž=îÏ�Q^{ä9J,j=sÆVx û€D²³÷Ü+Ç %ÂÉ¢ÏVõ8È�øUáI°'� —®öHº×�p�žw² M•j¼7Î úãýë‘ü|q`%ääaýí¸XKù°x³Â[\Ïkë%Þ¼R’«iùð€×ÆåõÏ“OÚÀkÂ’Ó).ŸpÌIÁo^ikœçü#¤ÀBY¦lýf"„7N µ˜¾M¸•ÑMå1õ¨�vÞÐPdKþM°È½gâ¿D"kÛlô÷ûþÛÃÇßßž7‰†Ÿ=og™‡¯Zÿÿ7cÅë0€ûðÞóS"vß_ð,ÐÜ «;`ï{0< E}›ìÄ x'À,š‡£îþ®‡ŠOϤÂ]qr'Hé1Rà=1a÷æOÑßðã4‹0 H˜Þµ æÄØàµÇ›ýü¤ð4ä6 kÃÛ½à}>¶ŸÿÒ³ì‚K‘^¬lz}ìžÀ?¢ÈÃπ‚ÎQ(ì¾}Âר7"I4ßÙö€ ŽHí?þá"ü�ܤ’*oìû^øa1Ú4œ3ö˜_ÝM\Ó66V¿²‰cü�ŠÙ¢ä\œH yÌÀò }L&K)†þw Okÿ€ÂyºŸ€¨ÎK?÷ü$’?/•"g쩌Òã~î`h“B©ÞT$ýõbᔼoE¢1ÿ}Gžªæ62UŽˆýÿÿ]í>ZuƒÏ»ýþ^?ùáu…iùµ´™ü`VLÔóøP�ªDädrÖûs GG@ì rjo„@ƒþ!`-ȃxäÀ0¼7óÿ±O ¾ÅÌ:'òµ*À| âÛ< ?˪6 §,D Ì[=ÃÃ:Ü×.æÊrìÉ »Ú >g¦Ú’áÅ ¡�À ç“`uvƒ<�pê< ó @ ^•¡ß�þ$*ÁŠ/@×X|A4 š\Ì®È- Ö¶ÜóüqU'¨m›çólÃÏôýCHþFÇ k™–~¸ÞŸöD¨eÔ.Ý®‚3è@ ´7Ïø�¢|ó ,%ö(˜yÌ. ø–,\JÔ{àá¬@ÔP ‹lðHe_62¹iÓð@{|Ã@å÷Ì,�ƒ)ËÄ…Wp,g.~Cz6-�ÚEÈGÀ�*WÄçø  \.B>ªrâ¾ÿ ªÂ×UÀteE›M<ûÃý dž'Áü<~ OÁIø)?1ø)?@eAŸE(!à+Åq0ã1°#ëß6Óÿ€ ý«E·y|±§I!¹ÜC�¹\„Dd ð«€Å|û{¥o˜ cSÑ΀êå�.£ÀuuCåü.’@^áW çIù¹|Ùzz’Ë~S�””°p‚]̯)ÇȤ¹_Vc+îþð�Ãã|ÿÀyCùøß?²M,8‡8òw1Å�4ÄaW65* œpDi.�ĹaÄV_�Ä^Xq—󓃷OQÑF¾f[>#!`tiv¾q dç„:ö,“©à¥I'„ŠkžKÞÁí®“É#b!·Î_ÿîÅ2eC£"8q„¿ ß &_¬¾ó’ÛþöŽtËΉm¿¶œ˜0 °,È Ä<_‘¬Pü­ÿ:Ò·Åêr#œëà�Ó·üƒîtÓø%6YX Á+Å!¨Þo::"z¡¤ÒxiìÀWÈñüó“‘yïÏø]Ó‰¢¢‡Ý"mÞ‚Æ@ÿ8a·ö¿ÿñÁËò_æî>²»ª·ïŸ’ýÿ{š „&,,ðßGÍ/·‹ÃˆjÚGz^D€#o«Ý›ïKÈïÆ $LX]`g—úGSeÑ2bq—Áš¥ÜQ-ÿƒ*¥ÜAŠv×jðÑMNP/…w+*ä.JðÂR…ž”f»Ö€IlEïÎRW0ë/æ©c¼ðâSÇžü ²qç¿ìœxX@…ÊCMÑ,Å€‰åœ^éó8½høf³½oðá6¹†Šnr–\—s2]ÌÄ⦠À‰¾XL4’^ü²6~ïzjBQ!ÇèIºt£&y|.Kæ„& ! 3úàÁФٱp. ÌÉgÞˆ×Ï“ÜðÃ’îëû ØÁ9çã‚B^cTÙˆ €,Inyí²Kˆµóç®bB䆿aéµ›(ü ÓÔƒ^.Æ“„‚ƒ˜ô  ä¼HF°Êµoô š—ÝÿZ×Rû¿áÈ—ŸŽÒÆ³ØÆ Ê¢ÓË=³% õ˜ò'ïߥ†Üž67ð¯xï|ÿ�ìkßž#Õ*‹�|XYBë²:&õ� n�ﵘ;§#=œa>É:cB"qad»À{#œ—^ðSŠY.Ÿ�.e’|.a¥ÈÍùA p²ÕHZÓzêÞä`d=k9C*!ðnßàà +·Ènßà€Ë]¸Æã0š¡* (;2ZÓ)½4F·¡¼z|á¨äŸÃcK—é¼&å0­ßšx‹Æýye·kwá'–—òÀY ÊOïv[íÉó¸té5î£@“ó=OËà‘Íþp~@«ø—§ÒÁ�‹$Ý` An^‹ÝÖè'vÍ7üü8'á¡øMcšùÑÖe®{üÚeÍ=¿ü.蘑1'‰ƒ¾@o?À—»îB”C?fàUW>ìH¦¸oxãYôïÆWj÷Š|�—Á �Y~•Bý¨DL%°h¼'ÛWÑ<¡{khˆq¸Âà#‰,Å€"Íþªî¡#ð&óô LÏò«º„¹¼2¹±þÀ¦ÎÿóGµO‡ Ã‚Õzëó:Äð¿LBI}â|§J *—! lèzÂV ¨Ä'�î–t¿&hN—®H˜È‡+dEl�KØ£­WÅêEß’ib½ñ¶@õ¹hÀÓ¤q›>г¢…ቈ=š“}_ äÍ¡‹=_³›…\(Ôé{(?*eçÞiXh3Ì ê�Fxÿ’Á Ï0 UŒñÿG �…¦ø„¶ÁòT¬ùôÁÅ"±ßÝAüÄGŸ-t¼w$M¨S�›¸Hn\wá–gkø;<çYz&ØçÆA ü½âòƆ<)¸e$3ÿ?¯ƒÓø'‚&u5N¢uÃd(øæÀÃx¸H)ï¹~Ä„Xóu^�·àÍÊV×¾øÂ> äêXѾIÊŒÓ ùø\ŸÆ ¿˜É$Ìôl!ƒËùŽ)ˆ:LÁç¤ÌóÃa^.xJ20™ãÒ˜W‹¸J¡°Š¥8¥,ò‡üb¢7xqÙA0�ƒ~î:˜‹_Kükðo\êÿÿÇá)>à9”L©ŒÚ?¾ÿQüæG÷¼ƒ±¾ Gë’råß(¹f‡©H=øÄüÅ¡ÆøÅ—L$øÀÈ¡+2#@OwPcçƒ~Ðaøuc¤j6W ®�Θ],E1qžÃ¬¼j*vŽ�NKr—�•/ b¡øcŽˆ¸«ï+k€Ø€ÑŸ/@!à+ÅT¼“O(S7ÎÛü“ÍÆú¼:?$²ä©a—¨z.ÿø•ð�ùÊp`“GÝ¿òd”2à}€ÖpÂ’¾¸I²£%|kAØáÂd–0™%=ÿÃìpÝäÄD”l‹ÆÄ§_�5A†í"üe€D¡±¿ð;Ê)‘x'¢¢Ü˜™£ʔɉˆ†É–HÂe¸p'üäù‚?·àG¸õßä’Xdlp˜¯¹3 (éQ‰,žÀh;�ïðBñÚÿêIÄ�SðÜ~Íψ$óH{€Ù€ ÓA›44¤Àx ÷„¡Haáq&[+bµì•û�|¶ p�D�Õ±"ÀIú¾Aâ¼H¥òBC¯|úÄÕÏeg|–Ëø‹"¤FÙEÈEààš7ÎG^߬û€Ó¶):¼ÁµÎXé}égØ;Äî M›ò-‰ª?.(±‚B ðÒRž™@N¡1`ÌYíÓñh&.¸ Ñ÷‰„„œf¸DdsÏcãj^ˆq9²¢}óà«y´ó+l‹ž´o›à€©$žc{d.ÿ8@@a •O¥Ò½ò‡¿H Àl×o§È_&'Àxi*¥Ó`…w®Óé‹/‘ ³¼„¤ÒWk'¾p1³Í_ò˜|¶6³`:W¼ÄЯZ�®ÅDoµ†ÒDAÜø^ Ü"²Æ6jTϯˆ&D uóŸRÀK÷-f¡;ëÃ=J{/ü%@àMç�foЋëÁl l*ä9CCHŸ`ˆÖ­ üc h…‘èÏÂß?ň(Bß1€Cwø�òyB¯Ü ÌQûPôòðËÐeè>‰÷Ì­~�ýV7쉣` £&ù€L>K2¤Oö+û"eÓœ@aXܤºÄ�s;QÔfÙvp ¯RÐò@v!UÜsÂùv×»àò@9âäøÓ�…ã¤�ðup€ƒÞmñ’€XRÓCLÛ5é £ ³Ãû·B1Õ¾ŽÅ“w½½µ@ uoHà,ónyW{Vøeª·h"¢Šóoþ›©$bÀðH{T¼<ýÙtƒ“Ï·Úî·Óþ0ÜéÏŸ¿!CÉO tâdz5  ýÉ€Oä¶Y_ÿúÙ�©5wÇ;þÖ¯©OúŸ¾~t‚Ô'ú'ó·ç±ÖÛߟàj‚,úMüÀZlt’mûßÿî[¿– ¶Ë*L�ê?ðýÛü�˜ið Ÿ´Ÿ�œEÇ�ÝÈßõÿº2ds×ÿ—Â?1ƒïÀ“Ñ–ðGÚvcóx@Åež…ˆfoÈ£L³ÔO0Y ¾ÞwÒw§¢ ’HŒß˜€¡WIžõFO1b¾“=ãQ Y)x¶¯•‹Ê¸8á3q„äÛSç‹ò¿0èJª£Þ¨>€êFLvWT߇woÿ ¶l@†ì±Ö¹áIá.}ÏF¶³Â³ÇZça*6 PºÏdÙg†Ta—!ŸÊ…Z @•‰Žà? ‡Zx·ªŽ�Åö²õÈ@|$[—÷E`N´�ïòù3Àt%·_gG‡œ,ùŸ6¸�"Bì}H›òÂoÇ+¯�¬AçEò+{t™f+òÛkag‘×:=ô…£gkò^í^A´‘£�ûÿ¿ïÂôŒ™|¯ò}ÛÿÁœ?äùùâׄ!´Æò›KŸù@CÌØXzÈ0¨–þ†%]`ÌÒ½Þí$‰/ýÀ…ƒ_ñ[r{è0É™·{´„‰Iþü¬ Ç„NMþ 9Œ†ÞnðŽø%'}fkÀ »¤xªë|ÙXNY3[ÿèK«¦ä]ªëlÙÐDúEÊ–xyÁ¥HÏ/†A‚C ‹J‰õ:Ëà-ckwÓo· ?µÖ}ß¾¶6[z-ùî¯ ®éþÝ.·�Œ¾u™@l¶ô{qì/ýáf�o•Î�7ÿljYë�ìjŽ„HH¾¸|R8³·®xpø�‘ï|BŽ,êõž2„CŒ¿ øø:n—%ÛâÔ$áÑï�ÒÛŠŸúf±|þûQàBÀ…˜úïÛÙùz­WŒ»3ÝK—½º¼5f”“‰œ/øGa3¡æÈˆv=FpGÍ(YA)AçAƒ’�aa"sÂi±dR6$MxšlxILµ�¨,ƒXDp >˜x'I¥8`„ÁÏÌ(Œß˜³77æÀÒ‘¬ÃDðá? 7!•�¬Œ+#‡ŒK’/Aè!ÁDJÖzˆ˜�²ÿàOÛ þ�TDA.c„ƒ}¥žõHBk1À±]&³Þ2œüµ�ü÷=ùê"p²jZ^пô–õñ* Âeß™”‚2Iyïƒù˜×æe FI¯=Øo˜>€ödLkgXͦCÜ [Sb!1°­J¿~xJiÉçÓ{ðHÈÙ~_÷€ó±Ç÷Œ0ÑIçÓ  uµ?î ãÂì3Y3³¹}ô�ŒjÌ;Ý<€¸€H�uÏ÷À>Aþ[±ß!`6;ïïŠ: B:óÝ©7¿z<%¶±Ñnßžò³Î>ðᵉ†�Í—y•ÿ|ÒÉšy‰ŸËª$_1r·?ë9yÞð€B¿hP¿M;ëLÒP^ZáWGqñÖÊ<H ë‚GÏYyŒ£Y›ÿ˜(e #léâ—ÿYÞñâCŒ¼š›¦'¿u»µ(ä¨Láø¸ju@‹ß9ü¤ð$ä& kÀOVÿ�ã¤f/¸üg¾ð?º¾Ûû°üâËê^�Ÿ½þ]Õ§|ø4áç80e •<ö~\Ĥ>G#¿ÿ“ƒ@#‹<•¼S¯(ûQ�x–+]ÿ²ù;„†Ð§kfˆ|qvbzâü2½M~/è8õøs}¹½¹&‚!‡„¾\±”nQŽ{%™�:‰÷@Œ„RÀ*¤Üð,d/^Î{|þø‘˜ ¡ü~Ý÷ËââÄPu€Ò˜äGï`ª¦Ò5CGOë¹ú÷J¹ÿÅ'ìÎï~¸­÷·ŸáÜÂCO„o‡Zœ#CÚÏ>ÿàù:o<ûÿ`@J…PÃ?9{óû ÖËѼÖ/@ç$až`hãÎ/;ãá¡% ÃŒ;Lµr’˜Wrhé Å\•»�ºã,‚�¬ÝŰšžñ¥R£ß*ßk� m™ïøL7±®�ž@H¤(X@87Ú©¬7±Œ‡œ$rÀDS…¬>c!f³ Iaó Ì4GX_P€°Y=ø •éùˆL$J¾·ù®�ÞV¹àPe•kž4~&x€Ê€ ¤†tÀ8§ÏĨ9 \øÐ“5�Ãd í }È.YÃch¢úŠsøEaxø#°€çêó½ëÑL\£�øNxrF_y:£¤v2ÕB÷aÈ—;bÈ{cÑ,Æ�ù"J8ý1eìa¢ô&aujìxíÎ ÷1(tBÉù„A)V¹ûøºÓ1¡|‘Ã$(îK0¨WÆ81x8:ùÉ]Å®et .a¡%ÎRÀ&K¹‡(•Ï�) ‚äÿ÷ñKµ¾yµf=fEú[ÀCo¸áž›ãecÔR–/8X@rCħôP W˜¬úy-C„šçk›Ãa..&85Ý`C´8ÒÿÞQÑ�¶A,1mF ÄÇØêׯ¡mëð_¼CVOÇl¼wõðü¿7ClL4ùnü.u•]QßýïàÊu¿ûÂ䵚" !q­2¨QãÿøŽ»çÿöÊhH¼ÁÀ�º;ÿy2˜°‚L-çÈ]ñAØLXŒl(nö?+Å Ÿ¹¾(­Çw ìLHˆ€à�_¯¶-~Éð–sð�±ì‘ÍêqpË6€'¯y:õà¶PC&åß�×~`¨¦rŸΛýÿmáÒDFó±.Fýä¼ä€:‡Rvüüˆ3Ì4H4 ïÀ€z3DˆŽ��ÿˆ)(*ááÒüðüT8 #ýw4*ÔxÜô¦vº¤–h‡ ‹ž¼wÜ„’X"vaœI:…ÓKÚè�× <�ÈÛ£‰€ÀY;j=ÆQtï¾yø7¾¸ø\„O„e¸Zâ§Î<òq�È÷z¿ g¿à6 ÈŸqt¼’KFJ¡\Üþ “ÍÆí^&IOþUpu¡ªEÁ¨^žNd]A'Ì:n¥y÷µÿŠ‚&À9Å“$§!Ãîµá˲rž¾—oã ’Xp#‘9QÌѤÄDÃpB�Óç y±Oø²>IDyümÈ3&&"0^} )dÄÄCdË$a5$8L²ŸÀ"{§ÃßðC³´¿$’äªTa·,œ/½"bÀ7øUþDäš|ñbO˜‘‰B3â k÷Á÷yè ˆ!Ÿs@!à+É-(°Ž2§‚'wNamªvIœ%î&Í)ÿ΄Ž2$;,™§ãÃ)A§ zÞ á‡ø-üú_Å<4=€]\ÀëOœwáè¯Þ7šŠPÈFžþp;?ü´K÷j`Cïoþ{ò•Ú˜p.FOÆ}Æ(p[߸zÌ­¹‡¬Ïs”|?ea¨§›Ù@Åi“Üx”ÑŠåŸRe� p—(�Ž6$0všfÄi˜j@›ž¦" gÆ„¸ž!ˆM‘LÃa<—ÆHËû¨poùB^öH9ŒRðéGÃ�{~ã$÷‰Õ”\3'œ530n ÚeÎ%Wóä^JLÆ…xÆ®<-5³Å:3+î9sðÐ^øK�„¦+<mx®ç;ü˜F!ˆ²išE3 “ e{¿ü7~ü;/àÂbd 7¿�_LÂpõ¸¬§£ýÿá°œ@¯ U$˜Π;œìï·àÇO¶ñù9ag<¡>Ëà )\Àu¼Tˆsÿ„ö>ô­äJ¥F Ú ®Hq†�ª ­õ=€÷ÂI„Af#ÂÌdÝjû©ýâýÃú+ù@„jþŒzï?`[†DÞyû¾‚씎R „¢ÑÍ-‘I×:”’Õ¸Yed joár‹ìK©Z3÷…"Qæ·.mØáï|Ä KA›x4¤Àx üÂd–cê>ÄÐeÆ�åþ`ñ3:¾ûƒb $Uw}|!ß@RuÛ;ÿ¹Ctü»éYaדµ4Ù;ó(óí/�ä&7ZÆwüE�“°‡•6óÁ`Ä{ç>6H”øÀ©Ûo<2ˆbÄ_†TDJxâáÁU ÀA+R‚ð„üõ'Ó"úñ¨ è'£ùÃÖÿBG¹†[¢>±W׉ƒ\7Œó¦ïФúEp>º9(5­y@RÊäUàîhè∙ï˜p)^_‚ !o˜p)O/åø‚ˆË  ‰¬loFpq‚*fýY:°Î|óœ=/7’ï­ø¤[�Sð`Ò^%Ä»çîÇ8€û¥Íþq¡ßÞÀ÷ú_ÝYÿ°bæ½$ÉÏ<*dÂE™ßà*'i%.û¾$I‘Ï<—Ë·ïÀ<(ri©wÝðw`@ða±A±8-~&¿r!¿÷ÐlŸ_� í…Ý,RŒÑ-ù¼——ô¿ìÁEÓ½³Þ€ 4IUóoXJÑ_ô¿Ê 5çïlþl*@똡¿ÂÈ$�3z°a䯯¿Ø×^ryëíw&úà ӖŸ~ý*€jÿ6¤…ì˜í‘’e›¿þÀç+ütÌ[œúÏ(¢Ä-2Ïo§ýJ .Šúj³·ìò©µyþ gtš¬íÁ.4×Oîß#^|2ÛüþdŠø¦ïç¡ü¾‰ü�¸iú½—wÚî›éÿ2çLÿ`ú ÂX»rºo%9¯°¯‘ôÆÙá—»æüÞÂõM¾¼n{ŽYãb °4Õž”’ûÓ¯m¹0AQ³ûsß¶¦í^Ó’Uý†¶/£ˆðÚ…ëŽ׈G€ˆð$,÷w6‹=_Afð¸$´¥ž Ó«MÃÈ rm!lðp†¿²óÞ(^1ßßàp�…+­—žñž|ÿõéiî ÊøtR;tzí¶ëÇ…£ØùÉÑ븷yî$�gêŽóaÉãNH™àZÌK\yð>`�ì¼÷”í#Ïx<[0F[ç熗b…Ä઺/dzÐ_q\®œBzÔ›ˆ ®“V¢n ‚¸“Wxl_ÖàA "ˆ$+›¹‹!HDÛ®cW,*Lo¦Á~ï–é0c§'YÖù>`5JéX!¶ÈTx@À ‘Ïë=+12�ƒô!kž¾ïùîKo«y»&B(~¯/Uê÷ü Öü_¥‘¼ZÄ C­à�bû Ùüu>”fNóF114&ô¹ÅÄ¿ì1‘ÖHÚ+÷½ôIRïzÿËdüqª6¶ÿߪ_ýØl W£Âd¬s °½võ²Ü†ôþ` ð*²Í½ýðvä¿Z"cµkúì¼–b~{ம܋õ\GaÞÿŸc‰ ¥üF©q ö1”¬ðâød)îY‚Ë}tE�Aåe”MíÖ¢¼L¾k÷À±¢ßÑo‡usµNÑ?íÍûº`Ëç&d€ ?è÷ÏØ8_ûÂÎ<¸¼ö’÷Š@´H¨õÀÃÊXÏí란ü­š>ÿ¸P¥†.Ì·žñœ"KÞ½ÅÐvø<Ksï¿}Šyç¼5Kn*K‡JÚhóÞ §”9Ÿ×<X³»ö÷*ooÇŒ]™îÀNÈ·Ùï ,@\@t.ñÓtVÓè9H/ �<År �Yc"y ŒMù¢‚Èc—ïÿEâ G%„µžA\ÛÈ­'Ÿz„&O1«•,ÚpÂ3f£Þ-!²’Ä(8쩃ê“dHI¬Ã€:Je;šk=@/LG0Òà€D4ÊØR+·KtG/‚Í“\„¤ì©t6q7ä:Ež@Û†.7(É ¯I±Þ¥ õÏ;^ƒØwêP¯|ñ«¸ ²¨ºÁô’` ÷w±+­^FüÑŠ§ÑÀ™€p ÍùŽHÄà‚O5˜h µÿŸ¦@ˆN¤sß.åàìwc̤Urh²ˆÄCàsÓç½sÁÁE‰QõÞù·YðYïE¨ûϼ »ÿÜ@îÇ ïãÃ…£m"þïÓ  {#†:'€6spÿŽÎZqBürÙcüÃ;A´FÈÑ…¹ªÿ¿L<¶³Ê@!ÌËÃ�¯çwø›Xͳ»/`š2»dÁ@½†öV¿|Ý4d‹ûûˆŸ ³SÓߌf8Ò±ŸøD8Ì*\$^€£‡»`F±VîÛ-ÎÐàѬUÎ[¥ð½IF»€Zë?I<,RK4ž¸!ëGKóýlÉŒ”œæ°ÿ/põIÿ� 3+·)·ý¾©k­è?Ú#á»òv©ßö–j(ßüÀÌ”9RÛïPÃêK £~ZÓÉÚèçÿ¥õØ”$0„­Mf—¿×uô ³“0c}­¿ëñ5ÓžÑo½ï�íðB˜q€·êcàÞé§i‚¤™ÿäÀ?¶­!þýîàÀûf‡XýÛIábDaÖGÖ}cösæ�*üüˆG¿^7ížš×~#Xy€Ó= Õ·çöD…“K¿ýÑ‘¿ý °õ¹ªÚúݪ$-k:¿õBQI_ðÇþ$4£U¾ áÀ~zéÜÛ¿áð×ÓŸlß¿(D*à–³/¨`ê\~†% ÁJÊ�¤�ƒ¦8çéð�ÞOˆ9& ˜yÆþœ Èo§• ˆ„ÜØ‰û¿¢XfㄇS–Vr篰åù °ê9uÇãÍeñœÚ ‹–ÂOF �S³ÇÃ*aJ®;°)iQÎ%ó?g†¥`À{Üè7ó*ùŽÏ?ç� P!°å²æÑîì~-Í¢:5¯ùçú|ŠD3ߤÕÖgÏ÷„݈iÆb[èA¶„Qy3Až`r%ËoŸ¹ºxò7nÃ<ÀxYóÛ<ÉišžVp‰�a0ù cí^=Æ#$:'–#—=g|ç¬Xây€tŽ]qóxLËã‚#&´pÂk«0ø 77ˆ=ñ$D!©vÁP§,Žÿ¦9yþ b¾XÐ[ž#ùÇ£{F~â¡–ô*k²­[b7¾ônßÅ ÷¹™î²í’~?'à¤üŸ�üŸ„ üAŸ–E(!à+ÂXˆŒHq€UÔÍ3?RɼB߀ }_jÿïZWî{XEËî×C­j÷?ë(A wÂf å‘€f9¾"d1=µèn2'îwûñ#Üü×Èor,NoŠG³ä”¡TÖVÔ¼< ÿ,¶¸ðWùb;óÌ¡ÀÅ–8g�v8Lø]'"ä ¯‘¬s¹q$ œK€ñdåO?ÎÀ`<o¤ž~@½P…^8_ø/Q/à18g~¨ÍÆýúaT×8�Cr‡ZÃ&ÎX.·;th,ìp©žd[æ8\Ï…Ò !9aWÄJæew. Ð3 lK|ÁÉ«ë�Ê{8u"ß0&LªyÅœò-Ć‚~0ß„ö4Â/x0˜n$}ôð/(×?'ËìG€|¼Uõ�Ì� Âa‡†õo [^ãè#üÖéyè#ÆÃ†/=~ë#¾©÷€Uä�Íå{zìþ#¬X ªüÛäI$’|~üìy $øýùØ0å ÷ �>4·ƒT'·C�`tά‚8“œœ|Ôï×ÿùÁÍNæ¿þ×ä 0ˆ0_õÚÿŸÀ æ§Cé¼7Ðo¡)ÇÆ5€)©Õ> ~}6ýÝAãpä{ý¯ *sæÿàGÆŽoÍáµò�…›xÝzÓ)ÖXf�ß4>¬ÃöõÏàâFÕW¿654«ÿ½p1…; =¼7&Åß çK—ÛóÏxx.‘ÒkÏb—<õ,6)®/À…� “¶› [/ùïkê~ÛØNÙzç¼47 Ò,.ÀPrÒÈq&‚á*g$N„´|žAd“É0q)voηÁ^H’y ¿2$W8ê —¿<‚&¹�\y �$ßu%—²*ݨó�Æ®cà �ƈ³ Hjå 9 @Ö‰dKƒ“Á‚W"!Yg¡æ+�+i–zRœ,씕‘%¯Ø  A r*E˜‚']&{ÁV‘6 ,ê’g¼iH&œ, ®4ú¼õ˜òSßýL²ja<"MOž#fk0&‚4š·¼7h~.GÆLîõ”ŠOÅô7#Üþ[¸ø#‹ð²ëcÅ1qÃ^îøq v:à €ˆi†eÑáüä²Î¥ÿè=ðIçïí�ÄgQ{Þ�+%­08íö6}Lç�iUž0i)!´Ñ½Å…ÚÓˆ 𗂈÷�Pù–µ'¶ïøõ™5«^Ûÿ Ï Ê1W‡»Ò”$*s� ¶U  º-ùïÀT‹W=á¡ €ÌIEÞYL¼X€�¸0\�Èd¹(ŠäC5ÈcÂMI*:O01æ%@ŠìK\ì‰Ì \X–¹ä5ÌiKB‚Â\»IWlƒ„µôZƒN…”ÍL`ÄO1áÂcY‡5Ì4 ’\4,"ä²Xˆq å®@j¦²Ï¡ìW 4Q“.z–_êSöÎÔ™ÿ1 D%\Ä=Ežðñæ$$_‘®c„F:r, íÏÀ<î%—ÔÅI^›9¨Õ&³ Љ5îž&üÃP„Âë0ÚJ@Æ;µÏ”ßﰆ±°»Âç‰F¹Uwæ@‹å�ë¡¢ûôü€b Ó¹Ýùá#°¾Y7> ïdÿ°bJg‰) Š÷{´XNÅæ ÍÅyÞíž�ŸŠÆœ¯í¥×ü6ÄÃB z—à‡3èß¼ì;àƒŠ¶eö_Þ~0a"Ý‹ÁLµà‰·üдbX‰Æ¦íà¶³‰îýõ�Ëw{¿u 7)àðuTõu~l\j[Žþ‰3Ã)ê…ÕêáuàI[3��NÄ>ÍÈúþ‹Êá•vä!8€ã€>jFQÉÞ22À!exØò_]¶:¶Øø$¶-“õ§'¸_&dàûßãµo�ù@-`Äáÿ–‹�1•^ÿ¬ Q…+åûç?b”ª¯ÿ|tdž…±ÿp3VŸHxº.¾}ÿ>™wµùŒ°¨Þ–¤gÖ„i!¡jLËÏ0H¼[똷“1væ 羸‹ºy ¤�ÐÄwÔél­µ©„”5ã€H€c‘Ë£–Êrʧ,:C–#–BÎXuŽ\ÁîpF¥£»‰p= ë@Š[¾`Ð>¾xŸ³Àz—ôð˜éPo«æ|óɯ/OÎÇþ– QÓ²u ¤�D±¢a„|kÞ£Éóð ª[øRF$Á³–԰露©ËÈîaæRæ9eS—0¡W¸3QMùy;´!D“Ÿ@°Jƒ9â~ÏdçЖ•üå_3ç�Ì8&†EA Ô3‹::pz�²:ùŽþ¡4¯AüÄÖŸµt¼o$¼+›ŸÄyº,XAóä„:L’ž³£dO K€Çvç^ýç¹2Jq\àEñÚ.ÏÃ…æŒ&INUø~\ž¦†¢‡¼j—Ç„ù"pÊHûÿ ·1�ár|‘0ØþY¤�F¨¦ Â÷ø"W¼þ,C¾&&!à�&&"&Y# –Nr¨þ?á°�\æçPLù‡O³|nuü’K=ü¶eœ9ñ(1ÅY'÷„¬2§„¬2§< Pø/ñô ã¹%äš{ïÂoñ/˜/§k$ó{ï±LŸ·@!à+É$M*amD\ d„bá(‡>I_ c./ ^J×u˜h/'Œ…Á;y|2A2@X§Z� êCCb=°2¤Åü®a¢bc²eš4)–#�SþPa°�$’µ÷¾¶ÂR;þá±>ȵÉ,TŽðÊȇXX¦‹ ¥å[B´aÅÈÀ›òÎ_µ‹ Pá~£VЉ|47š23xg±Z� ê%2Œ*#¢†)�¢a8XPL Ët£«ãxù¢cI‰ŽVPw�í3ë¼6L“µe|bÊzÿáªÛ 7|6‚! Ž×M“à§ÇóDÏÀcèöø½? ¨mVÁ öZMz>�Tà¥ñz|^¾X焜hqk &Zw”b&ƒŽÜµà ©O�¹üêK>œé šp×,ÑÀp)Óª§ôYŸÂ 1ªŸüxaÒIÂÞ®[^ï b#caHAË™/ e¿ ܒĸëZŸ®×}…¼—=®ü’h*îÜå°{NXÓ–còëuÂXùá&ß/’¢0A›¹4¤Àx ûð’ÃDw®²Ž¢¸Éà¯á×P‘P9 uúûèçêY)¸××¶©™ìS¹H$5¹C¡éG#_><ņ¦»†ó×Ĉf¥À|„W9{¥ŸŠÕöpâø­8ûAܯ°J”±mƒK_s©ŠÎ4>\¹¥„ ÓÐQ½oϰS ùC ·˜ |¿¹wWyð£¶ÌµQôüœ²º„¿šÉèïØe`�á5î\äòºÀ å¾I»Ùð›Z—WÏïWJIþ³ðí­M-†PMŽGO±ðôÓ|þMûÈ 0 Ø£ÀZ•ÏrXiñ³ZúqŽûÿ°ûWß=‚@I¼üP(«wZ»Ù…ZnÕ}¥w´ÕÇÕk¡2>TxoL¾°³(òØþ¨›Ø"ÌNy‹üãüb~í�eÆ# ÓÖ˜Åùçß/°"P ?´¬i?QPeS˜jÌÁ[L­¥·‘ñ¶'n,ˆRFÁ*¬Žæ_q¨–„·¾ -‰XnÂP±Ò®óøËî²+^ÊᨂbìsW(IMï�ä9a§ŒOùÃpþ˜~ �Ç­ô÷mœ-H9b\�”4æØËì©×€›ûAW†¬ÌvŸm?ñ¶}Jœˆ~ߤQ‡G? Rõ•NÕ}ckªŸ½3®¦h䳆ê¡)«ý´2< ¯µÕH¾ýð‹&kÌx ‡¢Üñ(noñ³ñÚÌeAv÷�mm€<é…YúÞUÿmN÷ÃvC[]ÌNæÂž­Æ&ÿ²„¯<[ ™›: r°Qž¹q+&neâÉ–h‡c(ÿi©/°ë¹E œ;¼Ž‘¿ß§„Ú_„NXº»*A:™‘�‡›´›� !Û2'Ãþìô§ò—¢ÃØu€‘ë_d¤»†–ø¥þCû?ëå.‹ÂÃxw "¯}ý1´ÿ ]ö<¯ì^O ^‚©£¥Ø]!dÿUN†™¥€ã%»2®O”XÚë÷mÛ퉤<ß¶)ó«ì*Ã/~ÚL}F¥êØÇ,O™q#çžÞK±Ôs©o€zÍÀe¿Øeüú g_«ù+":Ãýþb ÊA›Ú=!à+×hHaà*nú"žÊ¾­Á%/ÄJ8�Vªg>ýpŠÀÀÖ Qöïò_µï .7ÑÔLoÀìæÓ*ò%l÷•†£üfÓ<„åZþñpã ²� tq�ú¡Ä%Qв?ËŽÑ“ióÂL±(>|8†Í�!Ý$_�­Q·ùþùêSõ¾_ÞD…Dx–Af¹ëX0³bžkJìÛÞø`P"Ôüò‘üwÿpîoÁÄ·‘™nîP™þa^•=qn ¿gƒLÍ€+Yú2/åòÄ×¼¯[X›?ì!ÉÝ+5®£ùÑÒdÍ0_SaËáX$~^#Ÿ =Ó[…$á FfIm *Áx¿òÓˆ#:°´Üóðª•òhÞÏ—ÂÐ…ÜjG‡KØÃèZiw,™çñ¬}ùïžu×zdÇ‹za¿ÇÞ$0‚±š� Öû,ÂVæ¿>"â43p—˜Daà kÕÏn cÂñt™÷À  X®ããƒXØK—IŸq0â0½[ÖÖ')Í­áý”À#Ó“ÿð!ß@³¨Öjòû·@ z¿¯ÿðÇ¿¯Ú$Jÿ§m[á”7T¯÷¿ŸQ—ÄPØJ~ÝD RBÞýò 92÷íPI¡ í†+|í9Ÿÿʈmo¸�Àç_ þ£bĆb½‰F%Ÿ ‘^†"áêþ 2‚[Öý)ûÞ&‚@:9½½?Ïü×™íò¿{Èû‰ <0 ­æi/ÇšÓr?óþBØy¥=Ü~_Äh>Œ9Ά}Û¼šÀVæ:'KÿêËÃl lb9¤øï÷ÿ £'ß{p@ôûÿ߀”G5'�ÏýàªGdwoøGÓï½ø9D‡XVɲÊD¯‰'ƒüÔ÷ : ‰ÖzÍ}y85T`| Súöx0x}ÈKïí84ª0>‰O^õÀÆQ!t‘çg¿$dôˆå“ŽÂ¶„³å3Æ7t˜~±Þõâ 4#ºL<¸;×lð!aS|pàÑï;ÛÇ�|{Ó¼5H`òûAb6tüœ@¾'ˆù\€6'ÐX�ŽrŠ BÈODóî4ªT{þ%ª…‰ù7Æ‹í6 žÙ5Y w1‚GA6ÅFmqæ™ZÁØê1|’.Ó�L x³Þt‚yu˜&¿0Ò!Z 4½ý¬�3HáÈd¹ Öz†K�+vYê¢& $;þ¯vŒR5w0"‚¢%"#Y‡˜ƒ}6ñB¦õ¯DwL×qƒúòÃŒéD+ Y!(Ó_À·Ä˹TE¸Q›ŒÊAiyï�Æz6�ïÞ°ÊÀ&N à3‚D7'*ïÌÏ rë×c¬ðC)9W~` Pù•éÿ ²mg†Xq“àý�M>;x·ºŠÀèU¤Þ@(vÂe¹Nô¨ÇõŽïÍžvþ2ŸgG‡Œ,Þ:}áÃv_ÿ1ÀlÀtF VÏôÿöè/5z‘ÞðB°it‹ÿh)™l»“ïÜT6„|~àá;’`lçÿýUKb�ï8@ðë/t‰:Ê Oà—1â¦öØjÝAY.I¿�ÃS�…cBÐ""«?Ü ùÙj $DT�îÀ]‘hr‚ÓÁØ]áÞAòé’öõÀ&í%ŒÕ€pÿ[¿Ÿ M‰Ë,™/ïð.ºm/ϪÒúöo}³ÔÁ¤¯}áï¥$óŒ8¸@H2™×Ïúe‰à…ão:K¿ì×Õûc#NêÚ[Qü½°¶}zK¾ »0Ê{¿}Ë{=×¶�á» e8P¶ò ÿ�™o\ >)õï\ìÚ‘¤®t<…$Ž-ÿ÷Ïaw›Íð{ÁÓ}qt»zŒV^��ŒºÜ].;F‰>; ¸zOX°!t;k`aÏÿªù»öÃÀeyúþ׆¨€»/åAlGáy  °£7w�"m/=ààó¸ÂÕˆÆM/=ỤÊe¢r_õZ_yfKæGßOüpö„­áÍ�Ï;=côiÚø—»ò3/�œ�¤¼À™ šq�»ÿ?ä©ÄGÝíà~ÆÃNY}ÿÿ­qLþ0€ªNš«ü—ˇ€ÂÎLAž`-�\yѳäŠd Ï0À/‡©FÎÏŠ>p2é´˜ž�€óÃ5y�í7=ŸÚ�ñ ŽH‡c‘R·‚ c˜#,µ±Àa¬#ß1c ³2˱T2'ÙÀL@é,ëæÕt¾s×ìðDIÖúƒ WIùÇ[矰¨­´¾ÿঠ`,ûD|5‹Þ€<â—KÉñŒfÛíkÈ„;ÜE=|oåòÄø€ª*z,Óù‚Q€I_ßÏ‚²òÃìº�M�UTüŽHg°û‚0óá-&QÑ€ááš•ù$çñ–þ8""vÁ!Í„ÞY2Ë܃¶ÖÄy<!–Y2ËÄ…\­ù:Ø‚Aì4 c ”C4Àzw²x&®äç_Ï<@„Ê['�ð–¹"~Oìü¡”GQ™a¶{K:üèè:ˆÌ™ÏæöÍrÒ$Ýñ|¯øxüŸ‚“ðR~àA›ý>L‡€¯Âa׆gõûðˆGXd~Àáõ ~=Š08 ­ c@urø:�ÕË÷…Yćœ 2W~:ðV"æáþ{‰d2su¢^»áÔˆ®INï!Ó;b7àU›$cŸ—š—wÛüÀȤ8l�tð¼.ï>äqÝÀSº9á°Ÿ \«æ_eæ¿�åá�0 ‰´™~É©™_Kޱb4ç~"¹ž5pû€Àw€8Xö�ó­U†lL;2E{æÚ”¯&$:𪛟™Ç'^€ [sðEYlç‘ÉO[ð0 á€Ëv™•<lSè‘gõóÈ*Øí3%<[ú$uóÀ¢®^8XxUÞ|P$ð:Yt¡Ä¹c‰øq.XâF¿/Ž„‚.¡15k†FZÿCc„;3JN¦O¼¡“`‡í/K&Ý੪µGŸØw ‰2ȇVõz÷-LÝÕÅ€�¡u¾FŸÿ¿àp]êÌIïçĉÅ® w {|9^XˆU«eø”—ÔÛEî}Ø#�`iB¬¹üЬPÀâ…V ‡ˆscw0ðR׉úÿ„†Gœ~Þêýhnoê>Ïù à"4N ᡃQï_øþó'hp‰nºÂÂm9Ÿj1ç&6 µ�ûž?ë!Ú6ôÍk#ù¿À·sHÏúáz¢°°¬W^I¾ÿÝ|S¦ÁvE4ßÝxsBÊ'03,Íâ¸ö&¿¼4ë×­¬¢F¥„ÞÒ×?vÀ` æ%˜Ê< únéCJg¢X�³Ï)›êxÀÿFK?¸Ï~÷Ù^ÊôBÑ!d¸ û1#Þ=|Nr(2X˹è}ö¿L>ûNûDh†0²d@gÌJìûòzÈÒBs¸×¦ïÍ~1þÕØÃk…B ��ÁJ&.…COXœŸñŠpç ‡À²gÄHÛDׇ¯†ÙÀ6šm4šiýеFÚˆ·ÉûðW×­eøŒþ¶´ÛÅø�ØŽ@0N!%²ÝdBŸ ÔU€ Vؽ+;_¾E`DfYá±[A‡�� lé¼€½@Ò-M ëã´2áJùëaØ–J`±­‰6¿oèxãµnÏ ðÖ$Ù[ß!`×õmÙ³•Cý±ªëÊ?ðxþì?ž�pá|"ºq.áýpdÔ³_þµò|1Üá¡ü²ë4+4/Bp²0 ϱƒ¦™,cÉx ÖFd€Ð53oñÙˆ™·úë˜Ê/ÙÂZ™Áù,Ñ©˜ªÓ5§ÀäZæ Ó‡%\ÃG—[ Xa@¹8|u|�F;ÿ¸¸@ÂË} ½PƒÐ]‰'›% ó@‹g” 3 Óì²×å aÌICCÁ/™@ÊpÔõ¦Ÿþ¿�'z™ŸèP|F!ôá]ƒÑ2?ígf]L¼ÿ?ŽiÃ;âœÊÌKÁN+r) – ( ‹Ê{uæ€Ç½:]ø}ôÈÕÖ{ÄCŽg!ó=ß~cL4“Í�à–>ïš ©Õó%cqóÖ_÷ ÛÖÉw7d> 1=1ÝF;ç+ý¾ãÞ©çñc2¬2p�`H_×|éCŒàxpªžÍì ·0zOäâaÀIxÜ/Üg‰*À�_G:tûŽ·ßò>Ïïðáú/¶¾AƒY<#ë`TaWÀÇjã)ß ”Ér::QÎê9ÞÀô°«‚'ECŒÁè%ÿV¯ÿ‹b‡²eO¬\4€ƒ¤Üë�kÅ쎭oßà> rø#¸d^ñ‚!`¨ƒ‰’&ãlðËDÓó^·šƒõ¸¼H]``^fƒ< k��.BAž�†McûÁ…‡‚¢äGƒ€(P—o~W¯ùÂÖ-c‡šZåÓ¥£Ó}â)ˆëZ³KǼ|>yà¤üŸ‚“ðR~€AžE8!à+Â'0@84&òƒLä—Íà™Õ/ÓO{äØD@Np‰ÔrÇPÏ0‹lÁc-üv. ì eÄ¿žÑ#°.k“¤.ˆ 0\� P%A¹�šà�+€ê¥ü.D×5ÁY 0*á�sÐ?Ë䧦ë`&kŸ5áw&¹e@¯tã+«è¼[’ì¯ e‰„ ²G ÂñÊŽâl|B¢\AÿýárLy5a&+‘ªttsá7¾\7Ÿ ½òø]6L-„7à8œj/Â|t�Æ+Üý#Lâ×Çã�m èÛ/ð&éŒŠŠ° ½sà2.Æà` Å…®}üÌ­Ö_üBôÜN‚ŒžºÎëÒ~R+Vp bÙÚ$!ßP¾gPœ@‘AI ¸M�€/I_¿6¥Xáýƒ@b°õxÈ)˜ù¯ÉQ9|L0rÇ·“Ó|>ÿàñŸ“~îÿ$D" 04ýygà‹±ü> 2|Ox–ìþ÷j_Ö«¸àûó*Ÿo›±OºïïAÄ$*ÃòàÌðF(Ïø¼8€#g¢w®Ø>:ÝW~x+Éï]\h³tÐwL»¨äùÊEøß×Áâq¨û®o@oÌç×?ùð«­ê'ÿ¸ñOÉÂÁ#áü¡¸?Þä?¾€pQ#áÏ_ü7 ë$›Ï¢°£›—�ö|Ÿ¿±Qº¥þÀ�BQ~ùÁë‹AʉæÇ "@S,—5êïúm<%Óåöû2Úä{jŸê��‰=>½°pSÏü¿#ÀÃc½kž ¹:þ@`89jÚóÆ&r€p3Ø�Ï2õ•¤õ˜x�\Y¯vHHw)†ÅÁe’ؽ+ãçé¾—6ÃùVòðÓ 8ñÚ q àŸ}Fq Gc ÙÙ¥$P²ïvä"„OÍfщQ D ?Ö{Þ LÛk=÷K3‚zAa®ÌELÀ¨ Dt³è°P 3Y‡„×2&×0ðšæ<ç8�72¹áæ+™�9B,$pÄ„ÿ£Ñ(÷ˆ“Z^BB„·Œjm¬Ôé¼cSmf1„yHÚ ¦ÊFŸ�À tFF³ä”§žð:‘îÀyDŸ=á´bCâjΉBA¹N;_ J.³‚°¢]ª­sÛZÕ mË×6èK’ª×=µ­R ǩףò"Ä…Â' 7²0â�’7Tx| µ¾]^� HΨðêüÔ¶ù.·´�8“ ?«?¯ßaÃa>ç"uàm•Þ©ÿþÀÙ”ïTïÿ ÂD8&?€�¯x„= ƒwû^ksŸóVë^|ó×:ïïýx>sœvýÛNgÀ°Ë ]²-ɘиF†ûà5÷±5ÿ+÷Î%p çcã…~kš¯Ÿ¹†w%$’Ãb„¹wJÿÿ Ë~@½"Ä„C�OIÒ…&� /0˜fDdéÿ´ µìGÇk=퀴û§þÔKËâ>;Y1!''�¶Ùÿ\0íVt~/ö;èJ-Ä„ˆÃ!øa?ø®Ö8Š¡êŸŸ€p Ì—Ì �¸oçüp¡8X6w èÂÇ:DË!ärNy÷Îre|µòÊ¡•ï¨ é|úðW¾ �ýRùõôS”ƨ‹i D|O~Çà&jAh_¢uûž¢2D /Ò 2y8B +Ϙ ïŸ@ybþy¦Bð«Ø=æœÆØŽHä��Ϩ[¥|¼òÀ‡üñkRvËerâB8i–õó@óhO¾ ,PxÕûòI ˆ …‚O—�|èâ¸õ€ùã?€Ø€Xž<@!à+Õ  ”mÆd\«€MöKæÜ°¯ò¦¶mbÄ®ˆìübF *0åCòöz?ž�[:€<î]ò 2ö~_âEXBå,< *å {-¼“þyaÄ^_Ö4žvBòÿ„&‹�…\y§Âªý YK/ð¾MKh@ó·Ò¤ýÔÎMÄDD5#RË »ä ^¹ÅCp©a€ø×Ôëùeô¡¹QOb/{ë¨ü8‹Ë%Ëü8…凼¿ìH€`æ¦Zc0Hh­_R½†ß‚ÎI_H"=Rú�…{{Ø ²]O$ ,@m•ò¢®c<Ô÷*†}#V:¶HŸˆf‘‚Ù�à½Å3¼éÀÙî+ßý�Žü z�»Î~ªÄ³ƒƒÊ"¾¨Hœ*‡Ð¦<{€-—ñ‘ÀT¯jùj—à§/‘Ó ×ý…öù|L ÷c˜PmÉ Óátȸˆ�aeÃRÖâ,pSÝû¯È,A}1�vì½ÔX‘Šg” ‰# %üG Ùõ{ãý¹^Áê#%�/gÊã%»û•û‰’ÇŒ · @˜ !CH!ìYç鸖Z%¿È‰®üö $(‡w5¯ÏOç nÿR<÷ò–d�=ᤔð*À«à1âMÀóÈTâ¹Î`C@KyóÙˆ*žô¹¿b0_BsMÍ;kžL§<÷áP@˜‹”21ÿÓÄ&Kü÷�Úæ!dOóÞ@J¹†Ž!a¡øí²0­8r6¹˜p”×<"BóC±†éðO± ,§Ík0P<©s¾!¸ˆXþ+‡msôJ¼÷�‰MfäD¯=á´ È…ûÀ¢²Ù/01Nr×f<v.]ãiË]ªÆÃ‚ì¿Ëú-p¥vÕAÉ´K¤»j äÙħœX�k'Mçžþ�6Ï=ÿ �ĉ±"Bá7[˜,*_ÿßîné–xÙ½jŸé>ö¶žw¿ª ¸ ·�þ£™çÿöý‹ â6‚Cl*à ¾—�³·±H)�zó�s%W{µ¸k¼ª½yÈ)Rªÿu¸k Ö 0EŒU ÿð Õ{y÷àünªÛß?J8¶25ÏÚ5^ïÿ‚@À*s} oKp˜?òAë^¥å˜zïj$V¹~ &¶³¼·UÿB4ù}ƒ„X€™ï€Í˸ÁÌm¸À0‘WtÁÃ[Œbaqgü¿àèä}¬¯™¼ÃÄzâÍ|å*FYäZ¨ç › ™ü<ðï|ÿ€àÏ<;^xiˆ žL 'µòÏH!’ä×Ë= %5ÉJ9уȤÌ�z�XÕ¿ß,G¬7oóÎ4Š ˆf!ºfª�hžaÂÚæ¨‘<Ã�6¹†‚kó‰� �[²Ï~gežüð€�!��÷ñ!Bnoü0ê]ÆRn¬÷œõÕwæ1aÞTç„,21�¢~>,Xà>ë�¸ÊLŸ�òq½Ó „A"+JsRa¸* aâÚòÏ»D§K(ïu@ÚžÏy}eéÑ2Ž÷Q… @Ö“8<Ø{‡ßVffÒ³Ò÷A¾ºŠŸ  Eªù¯L#×¾3QGgPá"a Ïÿf—ù>0ïÇb �‚!Á±€¥”`™;Ø+¸žÀX¯'hðGñ‡¼\‹»òsGŠç¾núMÍÌ&ãÛ…»´‘ØH8?¿ÁéR_ø¿ÀõxÇÅ+G?¹‡Ç�¤Gýö¤è¥×.FóޝKä'xÛŒ!aµ„v�’ðÞa´@_€™Ä¿äúB?ýi²­=O>g~äú„5¿Ö›O¤ˆ„a•ì8€óþJãµÀaØÏr;<8u3Ÿ½¾ÿ�@‰Ä(=e{+� åKçø VÅ‘ùÅ€üé|ÿ�L"r‘Í2‰P`òùŽY¬~XÀ4³ÐáSXü¸[4˜|Cp·pöK «ÂYt+Dë…Ûp¨‘blþ€?Dæ4/§~úˆ3Ì5pg �™âŸŽ4†,ô}~Ž4†,ô}~È<÷;/‰ø&>¹yþ %‡†y�áªæçü’T2|·�Ý�ݪ¥cq�ûª•4ä?€Ø€+eˆ‚>ÿÿ‚(  ¥ã€ˆpÅ“““““““““““““““““““““““““““““““““““““““®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®¸[Ï{Üç9ï{œÿÿàèüIqÑZK†mmp£Ð™NZÃ!kàFÐDq×|BšÛþ®îúx,ÒÀ‘ì Y@ÆTƒÄÊ“=ßzӇШ—158ÌØ×hªÊ Ù’lwȱñÀp!g¿°~�Ø, O@16Ø8u§2'뎶ƒ€ZkÿG�ìÎb±¡1Ô,²q ô J«ÿ\}øô7€JgA"Üâ«´ ±%ƒšÂ>öWÆ¿p°‹l`€Ä�1¿w‚À¼@\p‹oÒ>‰`&éQÇýÞ¶@˜Ü‹6ñˆ‘ð€„2]úý×R`hS•HðOÀé3µ€OEý ªŸrß}–÷:˯´5ð ‡ôH¦RßáAˆxPÇ€õ#0´ð¨Ü :SÏ PÚÒ Þ„(Œ6Iàl€$ªB¥>x–S þ/õ\ê°é@@³œcú¢òGüÇ!(Óó”x‰¢íÆ,ÅÀ€ûL!.~ ÿÜK éì+§`ˆ(B€€‚€ø"ˆ)fè¿�Î7îP·ƒ>/ï�ê*×T`$ã¢ç ŸÊ‘åTçö zé³~ƾ"—¶î—çÿ¿ûÝð渚‚9,ºšË#ÈêC(aíœ'ÿ€ªÆøÍê�ù Æ„#(2Ñžš�Hî©kæ�@  ðà@ , x1€TÄ)w49‚l° {Ùz°•ÛöSsîè1Ã.°=ŒÀ6\ûÀíšünûò|oÎíì% è,y.2»@ËK€Â†iÿ{š7¥Ý’ R&¿î© —x†ŸôŒS ^ Ð(H|,`8Fäíï*LVÌÉc~§ùYð·Èp � ¦w÷—°ß3'„¢ïó0Û 3Ëü¡Ü>Xü}À‰¸¾Ç€ˆ`èDõÍë×®[…@¤¡l'¸"|–/ÃǸOG¢…¨Ý@at+QÂëªÖ?„Òz2ÚíJkÖX*”žä¯€Âñ šUaßüxNåU�œ+ü€XYíÛþ¯�ö¾ˆÁ¡_¼ 8„ŽÐ�Ú< ˆGb\I*˜ ‚Õ8X3ÁÿÞ\v“ï—W\I–Ìðy?(�à_Ãþ¿Î«ìþà¯(ÖeÞ:cþ”ƒ‹ú¢]¿ø "‚aÊ+`­^P“Á·ƒÀX¯Îj�.¢ u×]u×]u×]u×]u×]u×]u×^ÿýŠ °LˆÌÑfÒ̇„@~@êˆðÀ›ð@V° …$ŽÈZÊP¾Pf'ÀÍ,§æ´TÍ““ÈšEÈ7É·%7ÖÄÎwCî̾½à?÷“˜` à!À`sáÀ® ïýĉ4 e/û~ÿÿûÿðì ÷¿ ¸QÿoŒp¨à¨8ÀÌÍKNä3ž7öá Xªå!iÝ=³u£“­ºd"¤1+•û¤“'ûp…®UL¤¾W׆U¡K×�í²€�X# _ s¥®_ÃL·?úÿÿ—¼«Y ÿ Ôëç:GWß`Zö`FóÖ;}§€#3„›½‰0eÝ—-p9ðýÛ�× £ŠtÚ,-¢¢ýù@XNH@`D A\'ýEHc#(°;àÃ1w`Êàü�\2S•’ |.£ß‚b4ÿ@êsàþÿðW×DÃteKÛ¿øo„À $ 9Ïó67ªÿ¨aùí™±½WýC–»\+�Oë¡ß•ÃJƒâ–އÀKàBd¢Ù^r?ðÔw­ÝÝÿ¢‹°3o“„I¾}§¸@ < 0"€xùi1D_+ãÿþàÀc·~& 8Ë�•¼ñ¬fZîöŸŸP nÙŒÃSŠy°û8òbmaeûý°á€€¨�†à b€‚0ÓubIÄ™søY;ï¹À0¦L�ü$L$Ä50KÚ›f&M€ —}Å=O„Zt#ñ©W]u×]u×K§¢ziýÐ-,%Mï|LÅÔ��\ð5à•E¾×ù÷€öi̽b/ý™ ËLÀB?ïƒ8dkù€wCÍ@!ÂBÁÜ´F±>VUN(j¶ÀªNN–¤üOP�hÿîY÷bå"ªqAªÛƒ°a™<)u ä[r뮺뮺뮺뮺뮺뮺ë¯X?/ØnÙ„ä¾Ô�RJ$©Z½f�ˆ jê 1_Ḅ Pàw°aËÞÁ£(˜ï……‚  8 b–P6‹ß» <` '³¥k°S Xà.hÍïÃO`€€þù™ížºë®ºë®ºëßJ} ƒž“€]‡XÎW'É|€QúA›ØvšSLª @JÎä,+,ƺVql¾�â²ÁÈ<‰h¢áÒ |?°Þk]»kÅûTõϤRHÿp«z½æDlÀåÓÞ.Ì HI�¼ëˆL ºv{Ú­Ýï˜ýžõ×]u×]u×]u×]u×]u×]uÅu¯^ÿÿaº†4Ì¢ÇZD³Â@tÁ…Ó{SëÞqém,¢ÃË_û=Gp€&+é\ŒÈ¡»�Oþ{íG #‚#”^ùîW]u×]u×]p¶ òMý/¿ÿ4 r”å�—¼¯É›�&6ëŸI›�x¬Õï¯ý¾Ãzbõ˜6f"vÁÓ´èëƯ}ùöÞýÞð‚Ê¡�ìQb"8ý„ç¾çk‡˜B–Ž7žõ×]u×]u×]u×]u×]u×]u×��>”ÒÇÀ&kÌ#¤æ~¦½�±Ó>í©�Õ„œt—ïö WÐÅÓáA„ßBÀ;Š�íŠîþ2XÙ…[Ǽ À€ðD°¬Áè:~�¿¼ØHÇphè|eõ×öàÔÉ0>¦”ÿ¿x졆„¿ƒ‚ niwÿ·]u×]u×]uï…¾Ìl9‚ÓP§hûЃ9ÚR1ˆ‰„ß+Q5­G>òD«@X ]¥*)CO�€(`T  0Œ÷jq@ úÈ *ð஼�»ºjñHàƒÐ)”½B€µ×�g/бg)n~RÜßaóÏ!3¹�·s!7s}}u×]u×]u×]u×]u×]u×]uééDóIÃ�fSp‰†%ÿÞð£–Ëdÿ£é¾ªñÿ 0'` ×6’â?í0 ´F8Ô·Šh@ÀH@  È  Ó º-‡ƒ¯ÿ@ÔeãbÙ£?9ˆÕ˜ÿá@fÿÐ5­(Í_~ŸÿÿÐðä=É^ ~¿£›¯úYÍu×]u×]u×½€¶c.A¹£p/–‘y+n4ÀÒ¡°³D§ÿá¶&ÖÿûßÿÃ�tOzÏ…ñŸð<6ü€"KÙÖ_DiIè *€@bà²øÆ(ºƒßõ�I7*Š\•ËÔùÞ4ü"lÈ7ð‚B_ꌑ�¸áÔc3EEî¿{ÁA'áX~ºë®ºë®ºë®ºë®ºë®ºë®º÷³1ûZÂÀ[ã€îr/þÀAùÐ'%öŠ_ÿD$ÄD&D^ÿà ~`1È݇—Óíöª!ÿ_èbIIv5ÿ Â`(€h p'%aŽ˜µ ¥‰Pú eY�æËíÞB]šë�PaWÁÔ¢Q6  —"³Í—äêƒøi²Í†a�ª Hƒüý=u×]u×]xRš})óÖî‡ÿýò€ äTrùPÂKÿߢ0.Y_È߃Ík·«úàaŒúoD®¿<˜X±!BmÃsãÕ`@D| &�É€ðe.ç„”‰1¡�Òï,c.ïi Åаàe.íÍDÈPûø»ÀhÂ.ïi�Ĉ$]u×]u×]u×]u×]u×]u×]zÖ., ìoò@ÝóZÑ?‡T`=Û�0Þ;Œ2µ©'>‡*Ù‚0²åÆ?F2¼?MY.Ѐ政ð߀ €(€k>@…Oì E(ÇÏ¡õüñãÁo¾l t‰ „×L B–f±ø>x’î.ºë®ºë®º÷šSéDœôÀŒkªƒ=ñ 0!û`Ô‰ë� :K 5ù„¶3‚†Z·>K»j1{ €à 6p(O ODàŸ* ýҲÈK%%6Ø´ônIp ÿô Ee‚°·JÌ`³¾Zð?ßö?¯{ ¬%rVVÿ_ª½µþ*iÎÏ]u×]u×]u×]u×]u×]u×\W^½oÿö„À¡À)Þ£‡ý{N"2ƒN—^]±Qe,ð–�X@Á~ʵBg�ç�݈0:¸L÷“€±bi޺뮺뮺녰ҙò-\”kŸkãW 7‡I3s–䙹ÿöÒ) _´U�^ö€@å·¾Šõ›Y�‘ï `0K‚¹� ¶W=ç€4@ ö!0 ߥg¼� ¢g½u×]u×]u×]u×]u×]u×]uïô$Oa¸LL 6ÛëO—¼ Ú^`àÆ‚ÍúwÃ�­m4Öz½þjS‹¦�­á¥ƒNÇ/�_ñŸÿA€ <è@pÅ@&G ®C×óÞ¬lÁÇ'‡-ÃÀä…W(�ïxÌ·`p õ:rÿ÷®ºë®ºë®º÷ æ©3€d¼v”D4×q�ÿW!ÆÂ.ö�°ßPQDS‚ÍýÁPC� ƒ ncú)ž‘9pƒ¯†ÐÑ&zDq4 `k1X!}möþÁ\;N`G´!­^÷ûÿ\¼*aÂ$`¬D+û ÏyTs`„)hÃóÞŒ €Y >k®ºë®ºë®ºë®ºë®ºë®ºë¯Rz}qØ.`ÕÌ#þùdü»•þØ`5°ñ�ù_¯¼� R&}r±PîXဃ(Ô¹ÒÁJÙ´�Ø KB„ZÀåŠd„9„º8ž—j“úÀòÔ.°¼Mh¬PF”.Š2q„ûoYÅþ˜–°`Måx‰W�‘u×]u×]uïk›œ�%ƒC\·ðAKà;};»Õ´Q9WSÝýþ‡Â!ÇÞw/ñÁàý€t[%!äbo»Š0ÚQî·È#ÿàÄÀ@¡@€Ø ¬éد?r.æèL ©F9H‰D)› â@»"±‰T½€:0j™GƒîiÍR"HLŒˆÿô�u×]u×]u×]u×]u×]u×]uïŸþ�fÌ“»¾ïÿCX2·wwÿÿVîîÿÿèéŸ5Xÿ2 ŽLð˜J üˆlŽ�[wO^D^N¶›TNä¬ýo€,z¢pDxˆ™Ù…éüp @„çä:Is_àˆ`@@‚0…½ |q¿b&8ô3íû€®ÈŸ²¼(Û ·àÖ� æ®T0Nïô¿Ç,îÈÄü·Zÿ×ìø ”ùD¨—²±|½þ$f#< ¸Á0ý:RØÓð!hb ´O˜÷<5׿ÿ7‡|ïÜÀQs�N»i�íBaYlïýÂH@\ãvÀXFT�tó^¶?°GT†“ûÖ¿»“êO‚=ãä ²Üg¼±„xÀ`™K?3À¢&ªÕh4‚0{a¥nöÛ>�´BÌ‘¢Örkλ"h9  ËcP'îá0&8  h @dÖk&ù°……§,(“qã ~œ S�ƒ ™(�õe³`& \59!M*n#¼déàö¼Fÿ:®¦Ã°FA¹¼;;kï –­tzîp¦1ÄQw_ó{AòyˆÅJêίöˆ˜‚¦}pe‘\ÙÀGW\þª?â€=€ð€@\¨@ > €ÀÓod M ûŽ~ Dþ D0‘ÿ¶¢ 2”)Eh9/õP1ðJ$ˆŸ€A°¿ÉÈ$«j˜ÔÀ…i¢‘üÀ0²¹´xfAc{ªª�ƒIH’H@ .‹„ !5@A0€ÎZÁ‚ VA%xßôà¨ûXx€^¸ïÍl @o ! fÀ}ùâ p¯›�æDDÐÛߺT.wýãwÿA¬wÀ {•”�ÀÄ@éød™¿F~  `�I™ƒ‚‹¢ó¿ªÀ6I(µƒ°ð€ãÀyôM<ü³œÅ¾€òù’x)ãÌÑ×?ç}ËÿA¢�|!ÇÚhA=[ÿÎJÞ„NAápgƒþò_¢$€aÂÀ p„€@\P&<´ “B8]eüÐÀþ§þ<üºhRØ_׌I  ¿ýáÞ«ÿ Y Z†Ã{àИ Q²àßõÏýá„B� Ç7݆1u0%f“Wþì0†ÔÀ¶žU_•Eê?ôé€*-ÇöZþÏaé/qÏ»X½oRßS–ò(@ìƒO Oú ŒÖ€êÝ €¨A�«r&|ñÈûàТh¤/Ý� ðʹ”‹0œÀ¨œc=Ðÿ × p‰Æ¿ÛDwÀ\�j=� b²�"¾åü$Š–ÐÃUuIçäàb–ŒÀÇ ªÆ{üßô]ÝËþƒP$/‰£îÐ%iˆMÀ@ƒþØ?½Þ˜×påÛW®x ˆr†dÄ`n�ô­8µ0•îÈ 0€@B‚¡€@‚`zØaæ&!îPã  šì¯_à´Ì0YˆA¾ ò Eô¯_á|4ƒ\¸´-áR” ç8—ø>ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºëÀ@Aš$l¼þƆD†CQPÉ“WùBˆnDwàj›ÚB_øÚ·õ0 €!DnÉæ Ø’bÒ¹€$FjȾ¼,P¨Œû+˜!‹|#Ó·íÙó‚pŽ· ˆr¹ÁfK¼{‡+¦ØTQÏ©e¨Ë;£ùWï÷+1n’mÌ-× �r�eü¿`À&Õp@2éÚ}áàˆ´¤sÛÄG£#}|:Gཀ!£É3pCd—Ï ÁJ¨ ÐÎ_YÙ7ô_Ãø9ÀP«µiÿl´Ìò/-ý>*¡Áåù<\ë-oóüaWtf·¿21•Cl$%¡£½°æ÷µ¾ßÝ¡ù"T„˜Š*˜•¿aÊ„H»¯ðC"C·4¾üV/WSÃ9»ð ?ÕïÁÔ11e±ß�SÁJŸ±þ(LYkçÎK@¯iû�ü›¹ëñÞôãD‡�åÑB_OÐxOfõ�T¾«[~°¶¿ÙMÅÿÌË8š í…�¼Dß�ûÿ…Ϥ¸ÿ˜Bï3�›÷>?ï“¶’¥þµ�Ÿß¶ú×øößZÿg‡d"…ØÛÝ;Šr·=›|&(œbµàßÌÞõÞRŘžï}üâ,ã*¢xuŒj¬éŒmüZÙÃè%éž¾‚ÉÅë5KKòÆ/G ”ü.æ¹#ß/ó!ï—Æ„MïˆROÆbE!§¿˜¬Éwǽwãºa¼*÷�œf4So™žIÜ6ü�úÿž²²ü6_¾[+ E)|1[™€¦Kã/‹àu°p{ ¼@l.€ËTEy¼DîrYþ2˜€aW`"àk"LÁ__Ae¸È$LoÙ‘^x@ž,�ÕHDw�zð^^ìY~ûPë̸uF·ùï‚/©h* AžlÏÉ|õ "ósM”—D¤Ñ`胫–³âùÀ’MKرUáÄAP8oú•2õ,*ÏïŸP½½¦µ›×ÓÅ󈡠T°óé§s)�õô½n•�Þ¿ó f\Ûd…ûºÄ°šÿòøøúÞqô°œîÉ šëÙ^w B<Yàû&r)ãŸ;kg?ñP€€ãÀ'©ð+ü$sj‡„ˆ 2øÒŽí ËuM¯î†%yºßˆCþÏ l*ðõùàKg„Íï¾`| Kw„Íï¾:ìXÒ‚ó€X"XÕ‡üÒý˜?ø+to3þëR^ÒâÖl&­RØÏk÷7ÏìÅ}¿,Éiû»'yëÐ+[‹@Œòc üÿ ébÓòÚ.‚‡„yÞð¨Æè‹F<­÷�üOäíUÀt€+�c1†{@+F˜ÌY„‚‘„ª­ÏÀ5XyÖ!¹¿aƒå{xœc ~ð0¿q¨¼í5F¬¢²h~wÀô++�3tð°>²‹ß-Ûú­¯}½¦§ƒ„ \‚yóÅ�}7µL—¾Ð ¹cp1s7ölMÿÖ°Ívà�ˆ P8 Aæ)`ó.€ò)byJ¾Aæ)`{ÄÒÅœ’¾˜_p €Ú=Ó¯>FGø|:�ãý…�rÉ1\©ü¥†_1qD€‘e7àü¡×œ‘ß1#!GhG¸ ãÎ#U^3ÌDM,ж�¥‰C^öù‘â%ó‰ ''r™‰·hùÇóûçÏ8 (”²Š–‰¥˜ÙÃÌ¥‡µ,‚&–`JùÅ„\H`P%_ö_þžFâ8@±b?˜H‰(Ï8à ö-…þ �¹ÆyÆê`@FÍø…MàbiÅ¿w€UÙ4h¿ÿqáÖxivÀ#×Oÿ\‡¯úÿÿä.›PŽ b’¦°Mè³ñxMÇ¼àŽ±ûÜ@�¿>÷9Gãñ|R¯n>8 Ë�¿ï�ë�$[ Ñ!T×À:¶�¯@ ¦GYÈþÐ ûü;ÿxoh'óóር`JUXùÇöá‘hhý"þûq -™ ÔË㳜¤¥¹.Ìÿag:;�qæ+»ú ùzå{�ç5Gußžb»·ÿ¨ñ0ˆD6*¼w*ÿv‹ò—ü˜p.ˆ‰"C|EÈO@ÁhÜ7èƒ÷\=�ðËÀŒøè�•ÎóÀÐêÿT½Õ_õr9;CWÏC+ý*~æ·Z^T0.B4$\N DØq÷�‰„‚5Çwû~…­y$„ß2ïÀ××þ‚�Ôïg‰ ¨Eß�Á8€ðYèø]¡PàFÂuw¤‹QÛð09A-V³:¢#®üðG¶¾­|7¹ ]ßËë~^ºøor�ÏŒí~Õ…òb"g(€/À-Ñóù½Ÿðï�³û{¼8Lld`\ŠN.4@@@`IÂâgòþ¶—ïàäÈ»Å_ßÀ9É‘w€à]ðA cꌗ‡ ÄÎ*(œq…„á±CÊĬÊ>ÚI»Ò †Å·L ïÒï×ñû˜l/“…!‘!Å’g™&vþ0¼P\&;JÚP•�&=õwõ‡©I(PY9H&íÜ‘íbÐä¾’ÀµˆœKá<˪!°¹›‰Œ8–(#›Â+&<â@t__þ ¯è)+‡Ñjð'…èŸàälÿ¸Àµ>É€JGúsÛ¶?ŸñgÐ`ªª¿þ¼8i•WÿwáÃá‰�ôãð»ã¢Y{Àl´¸pý¨pKÚ¯ŽIWÄ8p^µíö;í™ÑΖIâ$ƒ¬”/' A,C�ÁzÔ8%ínn<õâ‚aˆ„Ú™yÒ�þ{ô-Àl@Ýžc@!à, �H�!€ŒkÆ%â€$\¨×çü£€,ù•�³�š¯©ù€ ¼án�ì$J¾ùátÛ‚ñ+‚j¶6ÈÓT·…:p*•€´Þï«.²–Mî¾v%.pñƒ�ûãMAäJX}*[Èž³[ž�HâIrÌO�J–&Õ×ÎQ)j¥””°K]|æ×‘nž§øCÅ%'²„è^óB+“ÂòáÙñ#!ñ†À>™Óºw”5ñté‚Ò�ðd¥Âé¶ЃO¸Ôh=“–‘)k¿¤¥™ èpHWðö¥±)h å‚f^äRÜ^³å†LÊg–‹Áa1óç`Ðúžqøƒã|å@ÏQ_ç« À—§ox•b­<üÌɦå^@ˆ“×ø{èG‰ P€ØT1çÎEès€=j+ÛžÜ)ã*|s€=:·¹íÅ¥°/kº}]õ^Š˜‹�¤Á{—9ý÷õ^¤ˆ„g?þcÇ÷ÿ øžðàN X�?âX‰cÝ2ì”_ˆu÷ê÷´reØ‘úW´^ÿW» ´Ÿˆ`Á€z¤•T¥Ÿdç–ÿºÿ€<7%ÏtE<³³¯øÀÜ—=Ð8%A!¹p³ÃܛڧÐAÅ;Yåªc6.ßžP©ŒaA¹BâÀ�ͤÝUBG=B3f£ÀØh ñu”BÞ txñYÚæ×'ijœ5¾sHóßÀÉ�{Ãa4"`�€àl "%Òþgs«Y‡™ÜÃ@6«SÎ8.=2Œ–F)¦@wiT°eĈ aÑ=È[›t‡D[’)Æ×*ià:~ìw|µÃÜ6v¼¨qÄ P!â6üÀé;šætFߘs¬÷Œ Ú±!€ã?û‚ö%Ï‚[çÙ/|…‡¢žfí$mø ñÊ¿�Ùüjñaw¯ &yeÓ6‡» FBHñ(õûâÂ=r™1ðáÄÄÃlü�Ü"�zÏ` ¦0‚)}çzð —Cq!q!°ã ñ~¬.n"¥·ð¨ÜA웡h®`Ž0lêJf‰LÑÉù›ÿdÛY�-µ˜É¶³LÚÌatT „Àh¯%¸£&T׎ÂL›¸ÝÛ_ï‚׿øÏÑ*·ŽÍ¾À˜ýžø¬÷†Î… "ø;<¢+¼S®üÂ2Ó¯]�û< Å•¿0Œ,�zìoÙá‚ÂNnþVk Ûþ[ôµ�X*ßÀ}3òZËØ[? …q%ñN ZƒmVc`yjÚ¯óýG>ZÌlYFWzl?¬‘Ç÷Ö7è@ƒ‰_ß–óïξ)|˜¡"AxPû‘¿pN£^ìò ÔÛh&„‡Ÿ‹ªÛàG„‡‡I!  %{¶fÏñ|6ÇüZÁ¸þÖtÌÿù¾ÚÁ÷gMoºèK$� Š\Ÿ)–g8oq0£§ŸÆœ7¸´jy^‚½J˜^Öô H!µà û]G|&Ñ‘ßè±åáƒ-¨Bwtt¡üeáÆ}á#ß8ì""Ä â]î:9׌a+ ©»?éJð8èe¹žú¥yòpˆƒ‹_ëûá熄(©©� ?> [ž‰6¥³Ïø‡bB¢Af–þ¯ÄÕ�\E|ÇPϯ˜pi'É1B¸�)Ý�@0±PÑ´Åt¥È;Çd jwæÄÔŠ)ÉóΔ®DÅÏ^ËP¨�+ž/€÷rÂ$ƒÔÕ`ìgƒ±žk†¡S…qÝ„7àà‚£d6%v�>ig›”Ò]ð8ÒZ&sìH�ÊÚ–þttpТ?ž¸ø~�‘¢?�¯·»?ÀlÀ XAšh4¤Àx ÁˆD"g�F^u€”2"`­Ô°.œ Õ €‹¨T5øÿnô3&(Ï00 S½.0Œ NÅ�¾b@;�í‰<<� Â,5þt$ åZ×Þ@žßÏ 8£Úƒ?·Ïñ`üÀ‰ðÉæ :ìÑ�ó»mC‰ ¿�cç9„+%-q¸"n¦|ø/ç2Éó¸#o¦|øñDøŸ6rÛñÑÀì*o«ºb³¯ÁêéÌ+>¾À• eÖöÀ§œ�dZDR>u´לpí Ù¯/ðJÀI 3ÏAh2±:?˜H°�ã<ï$h�ÀÄ/š@p8ÿ’ãðéb@¡)yP–°NË€@uØ`?Ðo'‚À/Ö#¡�ÀX€ÐZ{ô ¦úþ7Wø•Ó &€ñÁî§�ë<¯ü¨wڞ݀K%,·WŽ„"Ýî� ’�F³ÞPûm @ðóé²½Ž,ö÷®IœÿW€´¸lZÔQGÇî.†ðËÔ.7÷ÚŸþÅmõ?ÿ-$U…¶þè|n%þÉèš³=Qÿø&I¤)}·± `銓¼ eU×ÿš|g£ ]ÍÿÞ BTx½QR›L¿ÏÂ$Èu=cÿݽ«4?û *ÿ¼06@l‚¨©=D“ìLj¥¼¿Ïðª¿hšl¦Õ/³×þÏ ­%<ÿÄi$€àúï$4€¢ÌgmD.övßp“�KøH"  䃰ûăÆÐ07¢oLŠþ /�¤cÇöô±}ÍE�øÏËBïïнjÎ÷ÐÊf—Ø2À!‚5  `ìl’{è-zŒÆÉ¬.ÊÎÃ(Õ¯}>5Z© 4«òœ§ƒ (*©PY–Quÿ] <¨,Gœ[×C+0ƒÀX ÀÔv»Oÿ_À2�sÓÿþ„adl »xW^¿È´˜œž,a‘~¤½Að“h|8±! ˆ�^…6ú¥úß.ÿΟÍqoGHÎÙrÏžõEùøo±‹‘ˆ  BCé¼?µÓ�{tÉ©[üHʘå�/óÖ»Óý�Ḡ¼:›Œ4Þ¬}rì?cG³ÓøiLx˜éè<ŒHüyPà ŸD{‡.æF›˜Å”"pˆ]ÒrbÐ#ÜüjÐráÈ%LpJRqXDyšÈp-\€öXœÏHõrwÃh(^@ø‘Ü@W•kj¿&U=øBǼBÄ@ñÀX®ïS‰´Ep‰uîéq6ƒÈ® YB‚Î`-a­ŒHëJ¯�¾L$©Aã.—–\±³YÊŒ»òXé�+VZÀâ(HL.*UÌa<í¹Ý×2ì °Ly%ÍA·¨ì؉»< ­HeI®ÏRÍÈ„¾„Jìð-j ΓSž ã3HL«"ø®‹``aPx–ë¶p¼?̵קpž¼úø²D>p˜Òæ™4ëf3šþ ÒnfHâoÿÃ(œ¥ì$’nlHñ= 8_ ÂPˆ2 ³E+ßáØµaè±²Eæ÷Ow°j·¸~ü§!u„…˜Ý� 2Ïü~!ë~ð6Í Ò¯[N߆ìŒS Ü€p¾Z…„�}ÚL-‹òrÓåbÀ†ŠòcÔ}âM¼÷c} ˜iYç¼bâ ø.Ñþ»ÀB A•¡ã›KãÐõ�4¾<›‚€tO+[pœì"î×CŒp°²€XiŸ}tY‚!Iøƒ˜“Ä,^ Ö€²‡‚$úå�°3YsÖþ%a9îÉ€bs×–øa<(Á»$.ŽÒJ$ø˜t˜íÑì4:&`b¾ê+Ý¢d¤ ñ€ÆYà{½�ž‚€ØGt�ŠW°šô{êÈt…�ém ôf凃øO‚İ̉„9â¢mO…ûðVú*/ßxàè¥�{èöQºä»äVïÞ"å::÷Aˆ º›M_x ÏÔ¹öÚψ$!‰dp_´�g€mVw¾RE£M"þè°W��ølLÒY¿ï”‘lÓh¿¸9a§ wi\à0]—át}ÿ®þ ±{IP»þûù|"~:ƒïú/S3-ðNÞ¶Ñ�" —žæum™¹Çwø¥¥t"¬<,bº/þõð­Ä{À‘ç…Œ[^¢¾ñEÿÅ 03ͽ¯™‹ôjÕÖ?ÿ[Šª›gûà­ ­®XŽkO¯ßx¶4‰h³O¿ô °­ÿGïZëØÚ’òì5ÿñ€Ò!1:bÖ€%�k¸}xt ´Fîï€A·,„Zûþöÿ—»»ìܸÏkÔ^Áþ ‚€Ë·þf{^hJ!NéÍ?ià‰Æƒ‘õÿx4’Èè¯x>MÓ�?ià‰Æ�Èúÿ¼�%˜íß}ämÀˆuÕ'S#/¬»ÒöÔ™–ŸC‡€å}¾Á&5ix†Áà²�–Z¸˜pK믽˜~}¡¦»’ÿ˜00 K€ÈÁøÄŸ�;g¬‘_E¹`<_LÁMßá˜{Kß­ÿ™çhäç0àQ´ÆÒ€-æJSbƓи‰×‰ÓïÀèé¯=íË�­ ¦Ö(4C§GÃPñ�ùáâƒl.J>!îçÞ¶#Ç|”oR ¸Õ¿äJ:k€ VQ™a›^aòë{€Ó,Ô2Ÿô`5V}@%sl«¸ÍRUþ?•²äJxΡõÀ¤øÎãþ?L ˆ$*úK7…2yP@”MÆ™ãð–ØI³Œ2Á„©'ž~“¡ž#@„ TÑgŸö•Q [T¶à€I«éŒ¼6Y %3÷úªM'`€ U‡�I`€ƒTaö†P¥Ã®¿Ǿ"ÐsbS§“ß±ø’„‹¯”qÍÏ?R†‘â̶ѫ? Ó håûØ4w[}ÿÑöÖ£gY‡Sïì9B­zÖÀ½és?¸ <¹�:þm.żXűÝûÝÎÞ,7†p-ÞX¬å¼¥´}ϧ_*œ×ð L™Ît«öƒ\X±0Kã~mÇK«î, 8(Sÿ]æ¾@9dã¸ô£)#á„ 1y¿ýúÒ_Üy¶š8·Ãä‰ AâX€’7ßWbgÛ ¥¶o7·ôÿPì–?#oüü‰Ÿt¿ÞǼÞß×ÿ3”ˆˆ> 3ºóï£Túáp&ÎU½Ï T6ÈÕ�ÔPªx…âõøÏ‚%¶�ãæÏí´ý”3~R~¸�°ê|ÏÿçÂmU¤øÇÿï?¦sãë#^ûL4¸%f^6‘|&á´¼-Š‹$@0lg�«ö/òs û4vã»okõ¾„¹¨ÿ®Ý™CÞ,ÿŸsîÂZÙW~º¾$H‘�`iŸUîÐê(ÓÕG®@é3évÈU F‹]½ó9È DƯmó÷àÃÛÆîþß#ÀÊpÆH�"DD}ÃV+ÜG²çå{Áòq׿Ü5ÏŽ÷È=èOÚåµ>x\š:ÝîÁö—Ç{äžðA1E²^xoºKÏ &�už"çï�nì¯|…¡‰ÖxÀ¸¤Ìy=Ã�}BA(Y‚Ø–l^ §‚Ð,Díùðt„íg¦b€Eq#>�À ©+€O.C€O®@àZ¹E«—Ãe*Û@P›¹!…®ÃOŸöêÉ « ’£˜ °ú™D^/ðé¿áoûâÂïYf <¾”šE÷ú]@zð3Û7¦3¯0áq!¡!€^ñà"²cù¢%ˆ“6µV¸E�ãŽûvt‰»®üe§Ñpà6–­Þêø°»o<¸0q.nCܶ÷¨im~[_‡" 3 öZ9>� @€*éyüþðàèM$x&`q ´‘Ó?Êõè¢0"EÙáuÏœ’Ÿõ%i‰`áWÖ|6˜q»þ®wøÄáÂb <²àÂX³y÷¾˜zõ�| bÄö/¤ÃÖì±Ch¾Ébaýíþ?ÿþ|b‡xBä‰$IâphDgUàÎÿ¼ é”öü0ÍUæ½±�ßøtˆSlöü0éЂD@ðKãÂ4VÔp8nŽÄäšäµõ¹X×ñ‘}âj¾·¶·Í¨á„ìÙ™4óŸá¹ê_ÿèXf,SÎT«ü+–‰ÌåÀê|¡vÈL³?505ߘáIï^ªþÖ"Øoõ!½x õ ïÀ_¨oz €l[¯”Ú.—/ãs]6$ò‹Áìp#)óÉL< ¥Æ4@. ª‘ö )C"Pˆl/ß;<@Sß!™‡°àò4@S „C~ïË’„š³âïçÛ)r@ ªÎ+þï©yrE±C!(ܯË?CñÛíGü_ñe¡DÏh_OþcÛW¼Âa lO µŠ€=�;ð5±ß9Ï<ð÷<;Â9RÂ¥÷(‰ÄÅÚÌ­ÓTèøbífÝ5AŸI†HÅŠÆ'�@/M<@\õÀ;DÛóÀ;"6üÆû ž¼—8 eÀãJÁÖ�[=KšŽ±é6³¬@?$mùŒå à8¤Ó í4Ëc�wÀ`Ø|ŽßàÙ_9Ï<ð÷ ·>᳕G1ˆ‚‚\—_T§ÀrZ¦¨"o-ù½w³å©{¡ƒÈá!š=`…¶Mù…ë›òÈ8È¡#O]x�Ïü‘3ÎQzx§VZýÓðNVRý×Å<ç€à »Óßþ¸ì³uoÿùOÆÐX+‡½Ãðlwœ0÷¡Éì ­Ç$óÎ ~ÿ«§ÚÿküzêÇÝä«Ü»• …Ä©È@nN`ÃôÏt1¸0ÏÓ&"�(]^�üÇÇÕü‚jU]> Éz?òã¿T±äbªçÞ  He&> í_æ"IŸà¢ý¨Ï0ü$€ÿ±á”¹fç"C¨¥ƒ¤$�øq!KH$�þŠ�1dI!Q%§÷PMsŠ’�ø L—9�E|þ pª·ß÷Ô\”ìh[NüjE; „š}ü.N'a!ÜWà6`Bž§@!à, 0“ˆ »•`Jõ >Ñ™Gó`�®»F6n�æ…ò 2%ÂPÀÑß|Ôûè| ùßà<Ãó¸_HC ê�\»©ùÃöXÐ_Îðà%òÆ‚_; ]hÅ 0k4uàp^ËàpÙp¾�–Gjq’çÿáÁ{,Kg|ðà%òÁ¶wÎHèú*T bgˆšr0v³P@"‰ °.2þ±N¿Ø/®Aù¦â]r á£~EÜ&pâLfî®@]Öwzã„Â&€²éšï\Rì¹—˜L0cŽ"ïÀMØxD©^z Xâ.ü �„IJóÁ‚Btïöˆ{Iì¿ô†-É=ˆ%ðX`ÆÇ¯‹pY3Oö±[ÿr�±-|:ÄDÄŸú®´{óô[€ÀÆø÷çä�Œâ¥õR¢¢)(æLœlâø>‘¢ Ï( PÁÔŸ>8É�1¬ýàR>|q–*§u]ùà€vxf‚Vin�N±Lú©káöÔèÿÑÓ¥S§1ÆA§.xüˆ2Úù"!X… ,¥£íðPOAš¬4¤Àx ûÃa¸D0áú^['΀šIƒ…EŽ|9oÆ�|°Â!„Ø9OÏñ#Á—8©o CË¥3°¸Kï�‚%†;§TnF}˜iØ‚c3§þÓ%ËÙ˜€Ã€TtdÄ—(Sõ7§–i;넚šÀp{iô*•†€«%Ÿ‚•¢_ñ’°Ãp•·d"¯¾ ZÑ|åÊßã ±9l!­| !éø="Mt›óL¢QÀ�©ð ¿Œ®`Pv“üÃ…ôd—8*È‘@ú…êu ©£ �DÚó¡À¬Ñ”Ë8®²àh³½ë.Ð%yÝᲆ‚!„>÷ç‡Qþqe5àí½2üb’¼UÓÓo¦FYFz´×ƒwÅù%ŽþÏ =þ½TPáüH!0úëPzX~Bv9MþÕµ¤½ø #À 6wßܸågð§ÿóp¬²ˆCÖŸ;à{Y`Â�d7ŸAûåã¿ íøÓÍ. ïÿÕßàØÎ‹þ(*åR\߀<Õ‚dT£oï‹Á§ñ) HV©ÇŽj}ñ"‹r9ñ_?èBê��óHvwºøZ¸šš7-C—^˯}]?|?0Ï0¡ÊÌñØ “’MJžwÞÝë ¨R9ùÏæp¥)¦åO¿¯=s“M†\\FñÕ¸?ßAIÚX¯¡{8`s¾ì¾Â.ONÓ<ýí9Ÿíso¿.kNÒW�órY6ÁÞC�¾ÕÀEà‘å–@ëIŽKWƒü_�lÌñÃv³þ*œ@U^¿%õG‚Úÿï^‹ºº|XÒåõùøƒõ…J@V4ß⺶¨7{ßþ²Nø씩«¢Þ Å’Gc§/W8m ^]î¿+}{wüž_œ8_Â!À‹�.Èz´ò6ÛÛ2’ŒÏªôïñÑ!z±Gè¿Õ`ÀL9BBω@"Øáëq/çž¿ã¢Ä³xh×2Ǩboïó€Kã“ÔÀ¯ÈêìôþV Ù–P ‰.ÏrexD“³Ú@YåÙ Ø]ÿkCµß”=)Ïß›!§ã"©)‰Q‘vôS~Aç’dúÍd(97àpžA½O8‘¢o™ü¼a7 £nL „Êb#À7ç]¦ T—ÃHhÍØ‡²ü 'Lôx˜‘¬†ØòÅQ¬ @P, †ZªgÏ,µ ç¼àK‡ruOsþû`¸z“‰ëÝ`8Ý'�îsø7ÀÝ€3Mg‡$d¶}ã*KÙ¬qMk<$�e•®x ¨¥ýÖ x¸€N5ã´È?-kº×¦ÍW$J ´p"¦ñ{ãÞàÒ0Z"ïð$[oÄîσ¨¡êÈ´½pH¶ß„ëóÏ$ @�³²qðc½ü–Ûøͺ<Ñovwš©3 æÂ9ð¸RSØ·ž“Èø-vMÒ"(³ë\±„ω¸ˆâСçW¥ø_J=òi�ït¿þäˆrƒ L'áÛbY’d=Ù1*O[„Bî¦(¦¼Üû¿ä+HÑä¿§ ˜ø'„G%‡ÇˆŒd³RFlpœ­W©ó7úâ’�„´|øÄENFÜ™b˜oÈ×€ì·�oèÐt×¥ýV*n‡8p.€§,V)×Ï«Òñ€T EÉq,\ÑK[º}¨¥ ¯âÞAׄVæ×ÞÞßp ­À¥.Îöƒ)uyžéI�yàhlxr†2ÿ„B¶L‡_ªÀ¼íNS®ÌÒ&ìíî5'@I?­üÑõO›ãŵ2Škô€#¶ÿµí¾ãþèÿ�ªªƒ¶úþÿ­ô¯ÿÿñnõOÿP#«^ Ë>«¿Ç¼ÿ²¯ýWÑï±/ïûï‡ø@4ø™w?>¼ßŸ÷ßåøþÿÁ.€œi~˜˜áó9Âo£ï×�']�ã^¦„½®ŸŸ�«‡ð(Å? -Dc6+ð죪òìz¤ æ¡ †¸·60Ôcs¥|J5ÈZº²-ða[œ[‹7¸Ï}+Õ|»| ‰Ì7Ô”‘?ëÁ ÓO“ò(”p�[¼ð\8LŒ |±ÈññØ �Š÷ð}› ‰¢C ¿¸Tæ¿sŒ ¬ì4ã[çƒÄ¯;F³º‚'ÿ‡=gA�ôŠ3>45çÐ0ôÊ3ϘD6. ¾Qø•q‚+gÀ€ImHÙ fžz˜Gƒ!?&Á@�°�ÂàncñЬ¿~Ü„¯;F²ð¼ì9 eÂ@P""„£);àºf“úã@ß>°Ž4V}az(l\›+\=ž¿Œ„ÄáÁ gÜ€?l?ì 5à‹ñkÝuÀ¤KUo÷„TäÆ aÍæosù¿Í0á¶f÷?›ÿ8•‘«Ú*J¸ÿ°„I”Å «öépäü²áßýCñ;Ù¿›|ôˆÃáA3ówù‡Ä¥_¼óì,é¯ÿ ‰$AÌE$n†ã!Áu_¾ä7µ ¨*ÿa@ÀF\½Ý£uL­÷·ÌEÚaÛêü[àÕŸÚ÷w­g÷]SŸ 8<¨eL@ùð l?›ÏïÜ_!sôigòÕD .׸í£lבÿ9ÀüôVß`ÓØ±`ìö…e¿þàßùJ›x¡\##Ž$z(̹¦œ&„âAH‘ŸÛïs �€è½6™ÿ�HÄñ�5&Åâ@2š‘yã(�¸] ºUŤ´Ÿ\û#‹âX‡8‡¡DËVõÀxƒÂûÖz‚�ž½ë€ŸyXŸ’ï��@T@^Áƒ%äï/‰B!!×�!vkÏTÄE®|œu�îÁ-QœñÐØ†©zàp.‘«ÌÒ 3~vÁ¬˜œ6lÛWg’RoÂMnx‹5vx’¨³Å€&ŒQPºoàjÚdfiŽØLC€ª\©±,C€º\„D‚`Ó$±òxsÁ&!`­þ’ž)ÄÜ"¸$©ú‹€=Ságˆ�® /ˆ!vÿ³‡dÇØ*6#Yh�ùðŒÖA€÷F]øÄ� ¡‚Lk„F �à �OZë‘�’tÃÒ0WؽíºCóL(o¤§0OÞ“U¢FׯZÝ%ë¾÷;Ì)Ÿÿ¨vXÅ P²D€^2DŽ~ 6ˆ h«I’ÄÃû1C—âK]9FêÒ}ÛC:6•Uë ÷‘ÉQýJ‡í\/“4òª½ü¡8%fÃ:p†[©‚-.ÆÆE Ãý¯Útṟ/wƒ¬×‰òálA"Âr¥9sø¡ùGÀ(ÚŸåwV&ˆ9¿Ô8†WÁ'Ì:Bä"äŠä.`n|`:°Ìbàó3ÿ¯Ïõª«3µ¯ì_€Fª{žÑÕý;PúÎ%T{è8ßc¢r& eøm‡Açaª3Û~°ï﯀u½ß^C8ZÌúðy97øl_“A;ã!ËÁ2û? „P‡ 8OäÑþ_°1ó�À%/Zÿÿêñ;R€µ=:Gþ(NÐpu fòrʸËhµ�xÃýÉÐ]…;$C,#àØŸ(iÔ+b·»zc`§(VÅov 1�±N&>fßǶúmo××øíѵ¿¾ÿB…„á ¹-íö‘d_-Eà…Ú]N÷I†>PÒó"@ì/à0^&¤*†…Þ!‚òjH`wàq¬óˆÔDMÙárzoÿÑ—x„+Yá›Užñ`ÇÎ]àcXmäãba‰‚T‚à©LîÏ7ßóè�Á’�•ë*3Í÷Þx8Øh&®^Ž MÀ&l�Òÿ»~TÙq¯­C…q"‰«8¨ÀÊ—rvïýùeë¯O²—ç?DÄÏcòÖ6'ż2)c€?æH|ÆuÁÆgè¹oAüc{r]· þp†$Iëø"xÿ�ÂQ"Dˆ ä� 5—i°*úeÏ0Y–]¦@ð×rLQyØ($ç::Àm&�Gó r; ²çèþb<ºÎS#,™–LËÉ™dÌ´¢?ª E �PP9x� ¸[DÑK‚1†sà}3Ì”"ôBù2^˜*$â@΄RÂb{84bÜý8& û ·y8­†‚8ßÌL`‹ã` oé|ÌÒW;ò¾6ÕpÏ1^%s¿$œ@�!–Yÿƒz>ܯàðaÇqöæ{þ‚s‹ `aŽÚž ^ñÿ%¦ÔðXžÇcÿŒdaQ‚w��ÍòÞ·>�ïϯ©©`˜«Jè,‘¯C#²#þá-°“f«:Å|’L;éÉ"Kí|F-–pHãvÈ?Ùz�ê–•/Àü¢‡$¥ü@¸dPóR™ÿM ¡4¨ïœ£æ{³ñd’;æQÙŽ»?µÛžë@!à,o$#p¾ ^$6iê�þ™à`dæ Œ'–ÊŠ”Ñ(Ú%o ¨IaxFMsÝ)“]�ÃãññrãîÖ`ÿWóx¹q÷jô?àê !Áøï]ŠbÀG€!$ßW¿\´“7Uûô‘"Ncœ™Òô{Òã^;^þ½úðjgT¿Ø˜ßP-·ý{·Š$DLõð ùr¹½Ÿðùer=Ÿ¬XmÈM/ˆêC‹¿Â?áÂDŽ#í÷Å“eïÈK‡j"j ëT;oùŒð¤+ %@»‚·“Ó¾ç1νdL²õú�Îc�zȉ4í}³‰sô}§öÇ»ÙâËÇF*‘ΜX€L}ö~¹¯YüâŠËV³¼³Óø¿¹£ûÛ«+QÞ[æ3;�÷ìæG÷±C"Ćž#õ‡çË0ïï`ß0 Ãa¿¿`ß æ¡Ø±|@d28W£ 4R¬ðÐGé dšÿ<8#òB‘¦¿ÏbDžtu€2—k®ßïø·§;«ÿ¯Ä 2 åÉsguæºEzü-ž¨J,Wßwˆz‹ô€êÚý’„x]6عn½àÿÑGð{ñŒqìÄxãÚ°+ Ĉó”è@û�° wÀDàöµ« QÀQXKÊ×_ Aë�xp97åÃÉÃ0Ã…¨V©U ¦…Èàp¿!~Gˆ ž‚ù¯³'“È|‡ë{æ¶têÆ÷ñ•Î †˜GC¶…6ÞnätÁ»ó²ÓIJe†–_�ôÏ|ô•,¾þ"oŸ‚Cn ¾f²F�–„šÀteÒ?È%®y€à%#¦ÉøH$×<Ÿ‡á Äõã�ÀIúÏ« „\83HÚ?4žê5�ΨÀò8g5 Àöì 2àê"3�*Ô}‚ ^N©äŠ�µ'G-žôØãa0ªcÝ=[Þéùƒþ÷AÒ#àø¾‰Ÿrgü"¾ šiû¾ûëîè8¿ÿ»e‡ÿȈ²¿É÷~„/ü4„:ú€îêý?"“jÑ0ê�Ù9üßålÑ9àt¢óŸÍÿ/¸àÈ5 ˆ0nþì‡èÑ!ï€!ÌÌ ¹Œ ÓœÜÞ†ãIÀRt@¢ËÙÕï‚ZsÏ Iäk�vÀ�`,òö-ï… “LËÀ°©<ÒåÞî †ß…+ÉÂúyQWâÝ î¿ájæº/­Æ?—ñ!a,=×Öþw y/—¤;úT)$ï«üOÿ�à]& È�ýv«—Ïþu˜Þe¢ïÿ?8¹yÞ`b�[cÙ¬—þx]gc¡0¤H ³0–;ùÏÑ|A„;ÃÎ ?á±à7äÿw@åÎŽªÿõÞ37v-ñ—ÓwÞ{vÓ_ P[¾~»óñŠë¯åG½ü°¶ôzŸ£O¹·Ž¥ûÿ‚ _Õg?ˆÂ!P»À;’flÓÁ»Ùˆ"ðNéÇà €‰q²¼5V]Ì —�Íý®ùøe*7%yáÖúýÿ•õçßø“ô°ˆn Z¤�ØDþ ]”Jèo·"2A/É&¾ãz3S—k‡€Z|‡ø Ñ4ŠïÝüBn'¾3q6—UîHZ£_óm~¤˜jÜÚ\ÔK;GµÀ³þ ‡žËç7[Ào¦GÄ(D(@Û'x@.“ßžñ&“Íp ²4<ð€JÛßžäHažk�Çœ„ b¸Ä˜zŽz,ðh‘âNõî™æm1ÆT™ƒˆp,A�Èb\UÍ`xÉrûàä1.2�Àäd¸¿Q…ý„Äç ª€§%•ôN£ÿW%ž”nOÀÕé�ƒHÎè� ¶¹ ¬ƒP¥g`âÌ`ð�”ó£ÞÊHQ‘.x-ÖR�ÜÏäÀuˆF§Ý�ÌwÈ&%¦ÿ­ÛRGóD×çsÉOEâüuõÐÂÿK( @a„«Ž'¾ðÍ(¾Ï¿ŽÒy[-à:/"˜S]j˜<æ†ö¶5ÝDÁÚÒ.m'hpq@‘Î%¬Þ&Z¯à ,[ºG·•JI˜d| Ž€Hòϱ»~Dd¸Ä® ºÏ k�FKƒå `ÄAQÁ¸]‰�ôð˜!ût§yïP #x]¾x"¸3ŒÉX”…ÖRShÉu:`áfo®Ø+°°�†óƒ…óƒJclh±¾³¡`ÙgؾYÙ8÷ô^F`àÁÀCÆk�âk�ÈÉpÀ“~!‰pyeÀä3\"B¸0žF �tl ® ¨ötÈðoÓÝ×1Å`wKÞ>ÖBþ-$ %È: ³$ØZ¶¹ã�YÉ€æ$Ó¶´™»Ë°×�¯Šq5Á†»Lö$U®YÙg„Š©&{΀èR®ü€€,� Ŧ–û=ðË|ÿ�%šI¬¤«üJæØ¸¿% 8»ƒ€ÓF'Ä–¹8×'‚\KÀ,9b)_ßx8‚Ë2™ì»6zËÄ�ÃhØýšzù¹¯p•%'GÀYåÕzo»HÄ&¥î¾*Ìñ»:‚Ì·Xf5EÐ ]Ô³úïßßûz��7£ ÛîaÃúû8GúÏüµú3±ˆ»Z…EyË£¦PåúèµÚ“ÙÏàSï�ï0}®Ê9û¾aÝ›ƒš9ßËþÏQ•Þ¨ÿÿ/eÃoÀ]>˜ež@oÃaFN÷}ü¡èíV[¿ÿˆÔ8ðHÌ„�ߣ"®^—ó†åü¼pj�z\ñzý ¿?®ÿ…êßó�-þ\s'ÿµ�õ·k´êÿ89`0îðeFwJÏÁÀ<³Cž,”†òÍo…ò(xBøt |Xi:²×1þ?iÅæ'[­°;kÆöÑçFûGñ„Šycp¿àf '${7Ü4ð ¦7ŸTV6wÐ w,Gðò(¼ˆ :i“7‚¹ææÆHó¶‚›,ì qÆzˆâ-ž`x qýó¬@ïžðDp¬0ãªÂüX«NÔ ' m;´dÌÝ> 0ôŸfÇ—1Ù“ìÀ9ri‡:gcd f@-Lƒ »àeÿ¥ Ï }]¾@·¶½î€8Î{€A�ĸɀ0ûŒÁQXGâðР’ ƒåê�ïÁ°]ÐC쫼ʺžüÆÄ:”J8(ºØ�M“O­å½5àL�Rp¸NÓ±Ïöà,Cg&Š?G7 §ó M_‚:s¡i€¢MsÉÇÁÂøf%À×¥÷A€¼èvôÀ<Å\ø–¬ô;ý›ïóÁ‡HêäoS¡y\ˆþü—ß²øYS~%ô[}e õø½.ßsç¿€o�茮/ô¯ÔœüðÆ 0 ªó;³¿§Çk½ø ?Pa¥—ž7€H› dcÛ=bFF=³øH4©J[¼‚AcK·¤*ß§ØBA¥f–þqÒv]=³úö¢^ö~H*¯kÅÞßé�[?ñ:JÙüŸ‚“ò‚“ðR~ O€AŸE(!à,Xrx÷ÏÎ+TÜue7‚J9íþ¨‘sCyò[€ÊêfÅ—‡ 8c˘ Dà 5L£ñlÊံà9³Ã€<°s„°%£L†À8㚊¿Ý1óNX‚I–+óÊ„µ…ú¾ çj}Áµ}Acšý…òc¡8(oØ'õÉÃÌÖçáôJX¨ªÈ€!  Â$é`âM¯ÀzJYò§¦@xHó¼‹,‚¬÷Îl $$$6àyeŽg+�&°™<€3“ÄüˆÌ†Ôð#ƒâlðÛã?=ðû„ ð÷ £@èG J5Ð{’,C±Ê3ƒo±‘â4ÅŸÜ€¼I"�ŸÄž}˜Ä #¾y!$è"`‰¢Àà€xGƒ7ÏK†ó£¡ðUúÜ÷¢¿$ [ÿ5<ÿ…DNr÷égWü:L3〈t`²Ã¢p‹¸ Íw^>Ñ›�ø8˜qß½ñU£½öŸÃcü>¦` ÿ³$t�û×üF$}.ë÷ïäkA*ÃCr¨,êG–µØ”H�W~@B<»×b‘=u®HÄ9ã•�Ig¿âÂämÄD8J@ÈC¾øGô‹±ñg-qðááè&¸D/Óày?ξ˜ÃÉ I(饟ð1èÃ�r%Cüj …šd¸¦•Á…Û¨2®¹¨Ep•Ì`ð€^ÇÄÜ‹ÀzRa~Ý`ã1q˜„ù¿�tFO,xƒB@†$x]"÷‡¤sRßYµÿÿÿÀqÌÛ� ×Rî)Å…Òhb I ½ ”Ïòn�kÓÝAºb$0p´‹€ÙÜhüýê4 !æÖ7Ç{Ðp/#xJ�³)ÏãxHá8ñNuæb_A ªA4 ±¤ÇQ¢*1qT\Pf¸ª. ;°¸]Éÿd]Ç¥Èûˆ8ÌAÆb `8m,Ë  �ðî”6¡»ûÔë¬ð\2éï8aÐØtR“ëj)áÁ2ÃkW—ºg¾P"í,«é�á¨"ôqº—bEòÕÄ}õ/à4à[éd³øbüŸë5ž)|*Að@È$ Ÿ«(zU{o+à¦ôº½<:M "°‡ dÛ7°MÉ®ÉüÍ£áþý§÷{?À3·N°/'ÿÖiï¿Î’…%OÄ/’†‚!¡#[-¸Üèsz�ùü(ê-º“°p2¬��¦Çóþ,âN€u¾þÝÏîOá×ãˆC$æ11¢¤\rý\íÆõÝ�ìã–_ë´mÈ”.©øL@r@YœoQnÿÁ–DØ|¬–?ó Y'„€n LCø#3ÆÑâØrXi�!ÄE—†˜!Q§„!‡kà¯Æh0)´°è‹,ŠõŒ8„yfÞ~á|„È%‰ p€À—v)o3×à7{øøxÝþåß!\‹Î**À°u* üήTç�P§;<ξVçpœL[ô ”»¥Ùn>¸M‹-–ð6éc^1¾Ô$ ? ‚2GNq|ù^‘» ÷€äjÂ}��8�«ÍqùÇâ)pøHÀyŠºyðèH¡E+‡ŸL2(5ãëÿŸ¹à„dCÒû öÙyО3—ðéŽ_¢†P>‰W:øsŽaÄg—7Ï? Å–Uó>yú$#ÈÂ5½o › ¤}�{Ùyoþ"Ђh™Ë*œ¿ˆÞYTrþ×""Y÷~eðbRüÔ˜Ÿ-tÄt|#/¨^,*bÇ׸q–½Vå¡|„ÕÄÄJëýÂSÍÉQ39ð¿BØF•t/ªðqÎ  qÌalLu0ÿSœZU„<ÏG‡Æ"s�@G€)>Vú»€²/›¢¿¦!AxØBã_<+øJ8o}Ÿæ Ëå~+ì,™n?nŒ{AUàE²–Æ>?i½ÿû§ùÄÀI’�_‚ [;ÕSŒ&a€¡à TÑh |àÐì WæRR 7�îc‚�­àŽ|7)Ôz>±ù“Oå\ÜqößãW‡«‡\@ƒ‹”ŸA4Yî–dUhWQ!p^nÏãôñ�à‹è+"Z<%Õ�Ž Í墸 †"aëü%ô�Ä‹Ä Žðë—€)VŽs;Kø i�¥áÂÆÇŠ,x~žs|Y1°ÐeÉ÷þsÔ8I(™bl¬²‚¢\0³Ãb’ ‡z6Êܘ u~-†Ì�ùÚ`À½=ñH=ÚgáŠa‡ • ºý±´4à/ô+ÎÁT‡‡<-spó¾ öñ~,/%ež(6Î’D_k?Ãt¥áï°á2F™Ç錚¢Há'Œ8:ÄjxiLƒ—i‘ísù¡TF„ÐŒ¢P¢‚°¾x]0pÔ›2«íŽ9sÃ…C!› SŒSã p€¤¡¬¼7LøpAˆŸãzÚÚïÈK�g  M“Ík%ç»x\6†Q9×ÛûÿŰ�ùÅFDKÛCu¦9¢ìxãÅ|ýÕÖ˜xk¡v<:ı1D <¢+Æ'�WòV¬Ç  yÉãKDæ%ªÜ²¸‘Ø�ŒùaÙ%/æ>þC“­~cïDí†BÁ<¦GÀtA¤ªÝ¿Üÿ‚JUngþxpé1‰à=ÿx[R @“Š¡j \%”h©p1Ó¼ * d"û‘ÖÃï!}!ðŒZ\ùôÙAYBòÈkп%ÄJ&;æä%>ÇC’Òì«@�XâŒÜS\ (ðŸ\pz�Âù2FôⅇĆÜ%Êmî€áë >À>¾±ãÁŸ 3“#‘ø©-ôçIúêHü«"øõwUªz�Ö°Èà™[Omÿà; ¤ª/þÿ÷IDUw}ÿ*‚¨ÿ‰p|H¢ZuãòÉÙ‚áéÕëªu(•…P¥K‰pz$CyÑÐ.ô?x5rÏwѪ�Ú€,Ÿ/@!à,T|Îã/¹!8CA}ÅA Ø{rOhígêTU$Ñ3v;É BpG½'J¤Ž�Áˆ9ÀDx%i&áª)€˜EBAYä?èhAp?2ìY¾š…G—æ}óqßý~½‹�‘QpàUîó„°ä=3„°“�K ïs•€ý­£ûßà,¦Nhþûðñ"b!±²ùâð=ð´�Œ08ÄÉãÇ8ZOÆp[‰Hˆ™ð†R¸‡Ùý÷›-gô·Sºç>nñS={êÑÆq` ïà�Ú£ýÏø'µö¤ç‡ �ŒB / „†lù {ë‹öþà üÿ®_·÷@€ü`d(?ÑübÄ»DøÞÑ7Â� #GC’/x( ȼOd çGsŒ «ô‚.Uü2Û�Ö¼ŒT͘p˜ˆ¸Âb"áÀ¿p¬dÎ¥áÁÜ1Lt|8Ldn��Gx ö…,P¸@#˜^ôX@zƒa¹ýÿÀ:’�]üÑdŠ;:øt‘#DO8ýN}ýL>ùÿëÍ×gÒ©Êgý,ÃïŸþ¾p/?â\IįÀ¹×}ÿÿÏøÓun~û^à9ÍÀ÷Iºëóãúˆ>‹ŠŠŸÛè’ÿáK¨îHN”}ˈ/¬LÂN¸s“þÁîóË$Ñ4R¦HJ䎟’*ç38øBÃæ¿8³Î³÷ÓïÏà†×¾H�½A›44¤Àx ø ±nz<Ô$l  Æ#¬�hð5w‘)•ÒÀûTG0zp+tôí€p3ÿ±3ýTb·AÓ™af–-?ÐNã;XüÍ€ÌSÚ X’ì+e¡–P0 p >1%U{æÆ´Ó(<ògü_=N©˜ÒwâŠrÊ|‚×9h¹`ZG§ä§¤Pë¦â Üb5WxÁ�ƒàvä0G®ZÀ܃åÀT‹ngûñæù¨].Î0@`l‰x‘~»_@옷kºÙ>üß‘t_Ãù›ý €ÅyêŸû�B"hÿõZ6úü–Þ©ÿº Æd¹úù¥õÀ‰Â®H‡bSRªÚÔ•·ÓöÛŠò{x;ÿXßNÈŠd ìBúü¿¿…Rކ“S‚¹Æó]€!§Íì”…‚[ ÈXývÏÉà6¢D‡Ü3)·÷›Å4M“sÞ›÷ÿn÷ìñ æÖù"4WÅBò y„ œ¬¶«~Öð7Ya{ZíÀ¡æ£“þØ['Iÿ~ø#L°^šÍ÷�EÇ�oýÔoLÃσˆX7€>“<ƒþupIíè}š'›b~~¼eJ£{ý"—¬0A 6ÿUûÊ$¯6ÿôÐlÝU}zƒè¤ó÷ý7ÂÈ;K'YJ~öÙüÀ‹÷ü“BEFÙÿ÷pað$Iç[/�‡1úÀŠ CO`¹Z"�»ï}"úN?ãþÙõ/ð€l>‹uÀÿ¼KÖ—`·Æïi>Úø 8 7°qÎíÉßÍáßÜçPnÐcè#«a„­h?E¹Ÿô°YØ_+Û7»òð�Ï;ÿm?pnx¹ÿ+À"UÃ6.·Uši!° Þ¨ú¯œ€-¯Zÿ=Ãð e³ŸõDŸü4øÔÅñ7&Ÿ`ð®wçñ]Úô¼ñœ,'ÓÙ_„‚iè™�$ß^ Š·Ì·ùàÞ×jŸÿ� -'Õ¯|ðá» ¸¼®Ü4“þïý‹Îœo‹âø£ùeí=ÑÅǵˆ�7õ�8Ÿ/Söb©Ôìy¦ ¢5�•2arxtÍU´ÎÆ„þ˜þg…¨•±*Þ³v´Y$^ –œTåš@Æ£–ï J¸voº§Ï‘|_E™áÿØs·=\“Šˆö‹ƒö3„RD¥×¼-hôÂ[ð�fp€H‹Ùh ©ƒVmž¬I€¨*!•¶ˆ†±„lŽúô=¡e�GÝãP$yÙcÑ`Ií‡ ¥ïW†# @×á�ÌrJx`­?c\Q�2 ¤Œ q.]q¸0Ïÿ–Ã�· Õ fž.¸zAÜ!±íú‰4Æp޾�h¸VÃ` 4�€Ç¾ZÊ L „ò9RgQg¤P:®ÄÈ5�¡}Ìáî‹|ñÍîøTIþ–ÚéB$¶k×Î0¿Á'�t>aˬ{t-¼�ï~6hqï‚;Iuô\@ªÝ-uøpÆn\õîXƒˆ»  â9ˆær­ßºÆJγÒÆVA¤Y ^%4ýÃ88î`ÿ"½alyLýOØŽÃ0Ó¹^ qšË=ø æ™5Ï~¸`@h>€)“Ç¡Ëï¾EëÉxý±l×™Ëâ[¬|!”í|Ys‹EU¡\JGv»3¦«Õ]¥ð€c‚ïò@ŽîÂÀÍm~ !Z8Ý7Ò=êZ@¥·G¬ P I¼]Š> 7Wëü�¥­ü8uÐ$Pi„y�ÿ‡Kñÿ½àóÕÝ�áp±oYç{·Ýûöo£×€@Яõrþºê�ì1 ÛåöaÚÍ&‚*'ý”ð Q¤©Ð§æúÇO¹·‚Í;Æ{÷ýl×vŽx_Ý;�ÀÛ¾#Ú·/M74`‚ÿá €q€˜ÍÁ@ ž¯½®¾¾ÒVËÀ Ò|ô—k€'7Gµ°�aÕ¨€r݃þ<Þ ù®?¸ ’UvÏÿƒ˜¦XŽj}¯ÝAJW·ÿªREJÿ¼ [$›Óèûé)%M·ÿËT‘*UûÿÁ‡�ðL0@ ?‹KÑ�éxýG@!Ÿ ‡)w|q¿àø<±áÑ߹ߗÁ/rÁƒð/ï°Ó^ƒÉl8ìöž×çWâG±Z Sü+}.’b+–îû>‹ù˜7ËŽ?¯d�qa!!·� º.„�‡‚ÏE{ †@~e¹ø Ï*Ä@¿ß‚öÁ0J`B¨j8¤g7çãTâB«l‡ï•ᎠCx¯ÏœN-­ü ôFLµsÏÌý¡ðö8´·á ýøÛ"iøˆFåz߉H �!Ä×”‘SÏÅœ‹À¥*§Ÿ‚CòŸ‚CòŸ‚CòŸ�†kAŸRE(!à,Xzg†š•C|Ùa¹DÜ—¹Ä!¡ÇÅsª3× AöÜ@G€h9­¸Ô@q¯¸¨j/wü캸ʆ¥ï¼Wà�øðL"!è x í@–-gÈœò5ìߥOî`EØ¥}àò‰G Åkyi´µ ”.üÆí[¤Ñ±¯†‘vdgÇt’‡á‘´òßæ-ƒ¬%pÁÏʰ—óÀ”ë áâD‰ã^üCJ À>Èç:_§` NGõkýÅ<ãÙ-éØcœ�ú\}úOù*ˆKãòúàïÿüBøø�"O†B$nsþFÛå¤..1ëx0ø�ï0 —.1ëxe$µÞ[ÃŽ =e›ðÊ\ð5fkÏIÀçªüà»í¯oò€QÔ¼ìêƒ*³ÅðÓ/Î .;,ÈÙïÂx] ¸¢Ñz†a²o†äL`K†a¸ ‡ Ü2ØÕ9R0½Ââœû§Ã�t™=Ë(!„‚¡w$ ý«¼F§ÙÍ/»Ø~¡&¶CóÃã!Ä ßÌçñvºÿßÀZ«up2­ppÜ1„lº>0fÅ}ws„¦,Ë F ÃY}¸p.“\#ï2oÆ ÃPBsüß|#‰ Pd"âPó‰PQs¿MSÄ6Agמ<“Ÿ~_ôò€dÎþãÅ ðøq†œ“kš^£ð)á:Ȃڎ ïý@?mxô Â:.ã“øÌ:á!!!Á �ǡξJðü@\ê#ÿñá îëŸzwŽÄs’+ÚÁ”Wk»ÿ‘ÿÃb¥ÿ¦A+Ü|rµ¶5�ýÁg›Û g]׈$%Ï^Šýð®,@L   ô?—Å {$]{þ�¬v¸Ÿ÷ýø¾>d_SÿûUozâ¿õÛñ|C§~ÎPZŽ›khÿ¡0*þee´ÿÁàIá,ÿ$#ؘq=]}r�œ„u¬™ã\ ²™ØX)-œ.^…àg"Ѐ-Iîž�o›ZI·O@|Å”¬¨j,]i ó`J{®mC\-¹¡ùz‹^kFdw‡°=€U™= D–~µwà`Af±ßgzå XšÈïóÀÁúúá‘T{ùÑÖû×¾îûÔ½x ¸hŸqtÅÆO_áç ŒóD1±¤,(_Q³8§ÆzƒƒîšÜ#Üo$t~ ‹"8’§†6 i )ƒA\Õ�Q(ÀwÊÇÜ÷«½}eûÃs0!Ï•ëܲõ,ÎÕ…ÿC{'©L¥(WxÊ�—NÞ|a+ÄÂñ(¼@®µÕ?ÿKðZ×uO÷Þ$H� ⊲«Ë½ùÐvu/ÿGŠR_z]ï΀YÑ}ç>¼AÅÖñ•Uaï©T�‡Hˆ˜p)÷(-|SCþÀö’�ñJío�–vÏø>öµ©'n¼‡¿ü=‡nñA‘½2ë0�ýǽ!¿m?ªû}üRÈ,!ãþLld8ññˆRt1ã ½áÀ»^>2 ôøÛ“ð=!ŒÍçiq¾dLÙýÇ*–<#ã8F éBÑ“8Ý0ãÆÆÍÝM w¢“ãŒnüð>>“Cè©ñÆÏÞ¨JAb0¿Ñ*�@mÀ?Ÿs@!à,T3É ÄP„ªŽ‹„˜cu©ŠdÌW…!”1€tßh£Bð àeŒ¸¬"èt8¶3•aäÙ‹žw�äÙ‹žw äÀ�ñnëüNø·#Žè¥$)Ô)a8ë\WA^ˆÀW&!ˆ réq$ïÜ 8e?°RÏôxÀQu;`,¤Íúo>ß7º¯Æ]ø¿GÜl¸ à÷ºÿ ÛèφZEö¿æúŽþÿñK ž¿ö×à÷Q øp˜_2÷ŒÂð¬D8nâåÊÛ•q�tLáy9¿¿ÛDàßal¤ :ý3TÔq#'Š×SÃ4˜À½µ�K  qZg tË×S»ÚÍ?[™„`/Íd- Ž‚FüÒ"ø BEV¯gM,ûý(©Ä¥^šjâ‚4 Å HèÄ#�hØ—31d ìÀp/m »³'ü`_‘“‘y¼8L-¯L-8³‹XzN˜$á8¡>,87¢Õ¯ ß%àLz;òˆðÊ÷¨~_Ÿÿä„b�èµ—s‡�³ûÇà‡çý<Äñ"Ch}&<6zÀ*.GÛÖëxrî/Oßù«í؇p ï¦_Ý À>O�øŸ€7-ôü©!‘@¤K½÷‡`„þ”ê$@hT‹� {q“¯«2üÒû¯Ý�ë[_oÝb¹!XŽXœh`~1Û‰$ê¼@G„"¶~̪!m bHd ž ÷5à87:xófFZAŸC=ß\™z€ù4þy Jh`\ lóAÒj[ “©ÊæJ�kÌÏØ…€YQq2�¶ÞÅòIsÓ¯à{�WÁ>Kˆ³|SK#"S·Æ^cÄZM ®Æ$ho6yTå‰ öϸP* žhàÐoùøðNw–í¿™ø”„’FEÜ 1‡‚r/9¡4ÙöIëõ ÿÀmÀ kA›x4¤Àx ,áßú`åÜ÷¨;Øœ£z2ù‹ X 4[óüŠÞ`@U�Ë@ÏdLfUà) B¨Éznïp«Lo~8À qM1ƨè+–Q)l€»è *ú å’å�öXà.jŒ0`0˜¼ ¥µ­©Ws»Î]°@döp8¹Ëo‡lÈöpï{<°`ðÂ%ôãêa�AV·‚À=#¡üEg�ó QàÐÊBŒó0<—Ä/ýòP²�§Ï1Àkâ?÷Ÿ‚"0,Ú'|]˧Ëhñæl8»7øH¹õ/‚ZЇ^Ü×Ú É¿’;àUaÛ[1ð ×ó= þÞÈ;0ìV;C@ ©ž׳÷Á€—ê¢! VÓ:Î[½‡‰ »Ø Ýíùù,  ÀimTóíc»ü¨tþªYölj•ßäÀ¢#àŠò»}€B¨UT &ëÓâWûz ÿ\O{(¥Æ =‚¡­TãîÜž° éCzéÕúž_KðgŽ„¡„¹Bõ¢”wO𼿠mÊŠnÙ�ZlW¯ÅêøþCR=øÃødHq€ÇôEÏ2ýø;‘S6™ ßü4Žg€ó¢5ZöÈ ¦ÐX¯:U?~Ú·°êà™³î@Ï>q:ì0šl`¥"Ýâ8€Èi&þÚðâ¹¥hðDj5Ãgcÿæ"8à•�y�Ñûåüz,åÆíûý~Àà! ûñíààWVÝÁ½f,¼�¾é*¤º±ùÿy7O£»Þ.b,Îhÿû°`е;9ì*¦¾·ÿËÂF²ֿÿ¹ðì gÄx1›Ò3€&ÍéeÖxŒõÞë‹.–žYÜFï|ëßûÿoö‰†î �ý ÒÜO?>ǽûÖ—ú€N¯-¼ƒhRÞz†‹ûþ6~ûEÿ%Q�Ë-o ÄŸÖ 7Atú>í;7ö ÇY�Ÿ±jþ@>ðxÑÊ~ Ý'óoxTh”'äYËMçÕ‚ Ð'þ£'‚0–j¤vAK~ôøHß!WþûñÁø–c»þ»áňbˆ]€ZýtáÈ oBïðtåBk¹t¿ŠÔ,%YY/½tÒr¯_ßBÂW”�}ë“�-7:ÕýöÏc8ÕQžqªQžY< ÷©ï#�hn�vÿãÚïÂÆ14NXûÄHä}Y›Ú•ÇK‚€ÈX²¯î z¥}+lð»Lž‡H(!cο¸8_�d:ðO&#l)ßåÀ �mÏȽ ?¬_�I{GŒ@�¬`]Ìwƒ'‰¼ðÐ bQ‰ì©AÑÈâVaf·ïgis0*vÞ>‹7/Nzô8€€T,îP—@q&™Bƒ 烤Ó0iIv”Æ ÛŒ0 W<Á@�¿0À ©O1ˆ XL.ú¡“”pÛY !·ð²ê€鬇¹rˆ ÖC€|¸XÀAlCH÷þE1Õ+À#NŸ�`YT#ö;�N*î´“3Kß© {™ZÌ„­ªÀ}€µÏžðÿw~ëÑø¤H‚ávggØ  É|opËÝŒ°žA!q¿#>'�C„?7àp¾$7qP€0`�9D¤!”�ò6tÕ®ÀŒ¾Ž¸ûå–Æ|àѰèØ:;\ñÙy‰f™gƒ@0‹Ù5ùàà4Ž…%¼ñ<¼³ÆCNÞùÐ/dÖyŸ‰Xd€„�vBEÜA2«Á�­¡²½à÷¬²3£Ì8† 1,�«ð=ÐBDÕA,ÓÀÚ¿Çh•W®82  ÎÓ±û¯×àõëÞÕ9? "><¬RX€J{“Å›{÷‡€6¬-¯ ¼=Éç™Wá·ä)KÈÀh á&œï_ûÏøI£jõÿkÙø‚ø@ÝafKÉÚçT@G¿\¬mÀ{ÛH.xe`…è&N¹lôRÙÌü ¶ŸèF÷ÀÃÀaµ}óçÈ›gÄzz˜ˆõÀÂ'€D•V ÿm߀­„ºâ?[œu¡Õ"ž nhß4ïjàƒ b à�˜Xõæ0%úið˜‰I4æËÿ| ÃÄÂm[Æ_èÓö°„¦š)¤_„?2áˆh2Y4:Š~B?ï²à_-õ/5û€´¿h1°"«ÿ¤“ÿ;Ãi†2pøô'-GæðzYê¿ÿôÁ°ï±®$ß¿À áèNz�ÍàôLÚ«þÖ ‡}¸—û_†S;Õ?¥áÀtcm¾ðñÌÍñ›Ã‡k =€^}*¥Ñ‹ÿ> á.Y–¹ßÿ‚›ŠÕ>ÿåö3‰$>î#,Ë.DÄ:f£-‰rªó¥'¾øãQ·ÄK¾C¶šÙ©(7Bî:}øx¼VºË &h¾Ž•ølK•®žúCfü…üH ˆSð—1y9÷€—}qÛE/ôÓ›370`"|¯.:é"?|±”ð Ž‹·*Ž~ù|2‚/·]ìù7¾Uƒ}CÐAeyí¤»opaÁex·r©ŒÞàÇ�øA úùÛ_ µ2ûÞ¨/€-¿#vïì‘£¤ãkçÌ0`0‹ÜŽ�?0ð’‡ÍB&œ�Ç/ó´¯8D6”TY™ sφYûBa÷ÇÔª—ýŸÃÿøG¸de˼,˼0W 8d_È„3}à²f) §Oû` #’6�€±ÁQ'Íç.Æv´ÈÖ…”dÈ<Ÿ«å€µ”4ø<ïÐùÃBA€å=äõÉõéR¯¥Hò)fü¼ ë@FgÏÜâ}Š©;áÐW1lÕ÷ÿôøª›ž¹ØF¾Åvÿ½€UŒƒl×÷~ß¾ÜC(ëZÜiCúìÏ:ª‰ &Ó¾àÃÕ¢A&M5î 8 ?oQP«BN�7çÍÆˆÁ¤Â-Å•”Hóóˆ �U;—:0ùäçŸà)ŽNpt†�TNÏËì°8$;K@b>žeWË~N…@éØt$—âü ¯˜8„Ö�¼S0d*€�3«£‡fˆÀè¿g�èG¿Æ÷â×ÿ€HVŒ…DɾÂHÖùü‚£tÜ ’ß?ð¼2·žÿÕ<¡£ü%¶™˜¡¬e¥È?�†z� þÏÁIø)?'à¤ü)”AŸ–E(!à,‚Øø˜Žw|«VÌÆ.釳×Ëàœ@`l.íq¯ (Íå_bÎŒ ¼ï@O£¯;†ãýx ¶©< ¶T1{ñŠ*èA†ä0ÀS`Ø@.B¬©:<·û‡™ËjÎñ±¼ìw·`Ç9Ž; ¯ ì·Ì‹#ªy‚v[æÁVY$ò€Ã#âDâTU‚¼øý$‹\ö°F«€.¦ÑO½¬%…e„¡§Þ�y©zÿ%I‘'E€³·=*ýÒÇÁXœHƒÀDp’8›œà+y;©÷àˆO$oçt ÓÈîµø>H‘"P(@UÒ#ÆÀ·œHWßÿuô‰€·ôf8´+å_½ êÏP[ñTÑoýlpYÚH‰ÿÝ`ÃžŽ‹w$ºÒÝkéÆH�"DÂ83ÜFš|>ü-½·Ãì.Ñ:]ï@™Äi§ÃïÁí̇uß`¦Z:Õ�HDNp/t©p]vä_”�¬"#A#¡Ñ>A–äw뫎~û>ó¹{ÿ`ÜOߨñDA(–'°`7†u¾˜Ú«K›ÒÛû÷^Œ2K¥Û´Ë柾±€6“ÞxÖ1ývgÀ -O|½<"õö±ï�-¨¶ïéáÂ! €äþ,­n"?^²<»qõúõ—qa h€Ð€ñæh-hÕzç¥ìø¿ÛAâÖ‡¥ëžks[ö¨8˜‰ Jx‡‚ëïÂ+<†ìþ�@± ˆWq|›€~4oì| Jäybõ {7ô2a�^Æžq‚ûŽñV(6ËUåps$ÜJ(˜ÁÌ“q(¢`8]Dг�ÁÅ1³ËùiQù/;ôp�ÁÀHøà ààGÆå) 8Áv¨–Cª@&¸4ÿf\ªÉ�u%áÅ•Ê2ÜC‰ŠäÎMÀp@ð!„!f€ÉÉb`Š€øKF6‰fšWrGSä—rGSäaq €" ‚ïy@òT˜çXpz\ŒèÖC€«\Œ{5�‚‰@Á‘…4•Èq£Ž8(€Ø òm¢¸"Yá›Užêq6�%ä˜D6Yá�ªÏ œ@# ¦\‹ëóX1ÁÄ]ùÂIYpq›òÂ4×®q „4§:?Ëó ôê»òï&�zï‰CTî»òí“#W®Ü"Qóø°ž”@'9€$RVÓ°V•®x¿v¡l�$(ÿ¢ïÏï áA!8á8°lM@Õ›§À ¾´í¿â‚颈„ðâ^|Üp#]�% @ ÛΤÞÚ/yΠ:$Ч{·‡ÂDm4œç‡…ä¸FäÌ-¾'|‘" 9ƒÛl)9×ðzšŽÝoüj5ßßxRhë¿ø>MNÝC’Õ5ïï¼A1Dža0à7?€pªAﲸ Ïà�:P|ó¡Ã, @2·è}[Dÿ_]ÌÖ‡¯žÿ0øOùÎ àù4Öy÷ü0yçMgŸá\+ GL3¿ d‰‰¨´&\žÁñ’;@0>�¢ù±6NóÌŒ°³,Y¨Â̱f¢2Äç€p»¶èbÃn€8<'³Èì°G ޵epÌúl�{·}>:1=Ïóm„…Ï ¦ ¹{n�!þ…íü2áÛà: ,ù´ˆñÿ�ºÝk$ÉðDa¾2 Ä�h r]CÓœ>Ë:S·€H› �wϑ߰4IëæÂ°ªqsRÿ63ð�½ŽÕ·�í;~"Ђi@ YÚ RË�@’Û„@Æ—7̀ܒ@Ñš“Åë[úiºØp0Æf ³¹=šŒoòÆ@ªÚ�,Û6¨‚Y@jLð¢ 5=…À„É„ÚÓþxÀ¼žY¡ÂÉ uØÚS\YHmÃ5‘” pù¸Þý7Œ8Ùfü˜¨³ŠJ%ʬ ïDVzð=ÂBPô4$"Uœêi‰Wÿáм—‰ˆá¦÷Ÿ�;;Úüñ‡*ÙS³òb"Cb>åtàtØa!»ÏýÎnÀZw»3X0ÝßûB ˆ$õΘãJ{Ÿ®¯ßÆSõ9íg” -Ã"çìý€t· ‰}ö~ðûІ…‹y. fpjrüŽ_ª  ƒ=•áD7–ü`�¼°U åÞtt3Ó �q•Îß—òR‚¨p,h€5mžo®<ôCçB-|µýNŽ€mÀ‹Ÿ·@!à,¨ÉT-Šœ‚a™„›o/R߇Džü |°à—ËÀà—Ë |¹÷=pèü°D½ß#<ý`9‰7°5ò†27°ÍøbC·„ØŸ_�»YáÄ^æ{žEîc < 8ñ`sY\‹šÈr\ ˆ‰žÈq¹aÁks"÷,8 -ÍP´ŒQs‚à¨.‹Ó&\\óÈ5侬—.%× ödÖC„©rfM`p~«<â"Ä«ºÏ}¿êç,ñ·Ð{ý ÑqAE\P¥9WáÁx�!³Öõµµßžõ™f»óÜLX¹Ä žA„éNÚ£u¦hkÀ1Nõªk‹5 xu!Àú¸Á‹wËîùÔL*‚‹&Șq0£¦–\Âc|ž_˜½D‚³þ‹áÕ99Ô»­©Ï×ßàûjçßßäÄDœÐ(æ+âÆ>ã ³ûÛ¹_+®|>Ä®r?½ˆ$â"O0˜hÂ' 3ÿ§=Ö~G¼ÿù˜ßªrÚˆ$EÏ?'ºŸ^òmÕ?:ðáñ€R' À!÷i¬ø€O�÷§®�ûïøòy;Tïû~H˜‰Å?úÜ)¯œ´-®^U÷Êžj”+R‹%ÜXFï4¸GRDÃis¯Ãˆ¯_9O:#š?´ëPùŸðDžÆJQ°Ø€«N2Ý{héÚ~ü‘®Hסäú�À“Kš_�À&ÒÀM¥üd PÒ^§Ù�Åç@-%ƒÕϾI,J»çú„pp.A›¼4¤Àx Ÿ#� à¿Ô_@Œñ<�ßø‡í0”ÃäõÅuæ]ðIêq@o@AŸÚE(!àŠ‹è6¢¸®€Ÿùt¥j+ŠàHŸû@!àfA›ý4¤Àx»ôC¤ô�eˆ„ûÿþ¢€‚—Ž QÀKNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®½ÿÃw‡xoÀ° e%2¤Í8û ‘ì‚î`#þ 9žÎ6Ì FüjG¹Q¤  -€>DÃ[ˆŠ¸Q�‹Q2Ppw2ÀŸ¹Vâ"¾5>U˜ wß×øý`G ‚TKÖ¥}›P`pÑ­ïWŠT•´7 jwÌx¦¼O¾ £ 2ZnuƒÞ–À x Áì$&Á}·Ø4 r”DfzÁƒDØ?·àÄâR{W«ÿ ì*'Åž.t�\��ùÿ¾"üœ�Ï�€°`®Y.Z)ü�hØË%Ë5Dö ÁFÿ×AØho$�À~`+@zX§\ÿ@*Œ™`Jh’_À·ÄÔà‡ÿÖÞ½å‚ ÂÂ` –èY–݃ Ø@‹Ž@?]ÓÌ…™f…ãRÐ ¸îy¾¯õ7]V¶û–m¼£Á «¡/Mm!¹@Ô¾¢À«$'¿°.°ðÛ�$‚~ðN�÷ß¾€€4p�Àð1þ@±IàÿÀèb‘qG˜%p;ð�ê0åß�¨0¦\@¶Po;"뮺뮺뮺뮺뮺뮺뮺뮽ÿù<`˜‘ÎV˜’V¥x8…øì##Áªœ,_î *wè+±)ÁßÁ 8¶‚À8÷¸X?Þ\[ÝÀ�`TÞm’ ³î¾®îå.A¾€?Æq‘šL†š?ø=€À5¢²ð nSßÜôWÇ\ID}jaf5U÷y¡Lï£JH(¨W¿¼‘%³” §s†@ 8ð@ > =”TÏ9.€9P"â%õ¾ÒV5™ð•bÛÂ@Œ€‚'v‚chÊ€ �ˆIU¾ÒS ÁÓi^/¼$ŒÎå/ÿ lÞ`*ðÎÒ?õv7ÔØúü`aူƒ�€Rì3‡¹³Ë<°øö)»:-VS‡?âú–×zÿô > ß2�Pº}| ‚²s/GütË�¿Hr¨ªëõã hQJ,…�Vþ;TÁe[?_ëÌ»K>°W¸–uÌ:óþ•%=ù‘¿ûÀ`\4¨\Î�  ‚AÀH( úü£‚>/à ×§jƒX6÷òÒ5‘Ì›X¨>“”(ˆ:‹°+KÎj�n ¤þZFLO•¿KýAO;ü­\rîªeÀp?¨"k  I$6GKòËÿü0€@d    òà GL€rÏ€~kÓ“¸=€¥9]1 ÅtÓÓ“Z]u×]u×]u×]u×]u×]u×]u×]{ÿò(.À Î@ŒZñàpHèB ÇÙ²†±G$;í$Bm¶Úÿü€4 @ (4ç ÕÒ ¾*G÷î ×ùB+bCå¿~¤Y‹øÜ‡ð2­ožlŠA°@ýÁªR™¦Â‘z"$J±Ò•š‰0È¥wÓZßø}"$ª£fú 8@CH �b xyñ ]ˆ]÷î{fó=›o>ÿÿÿÿ <¨,øÆ.äƒd½ûžÙ¼Í³mçßÿÿÿᮺ뮺]ÎèxtLIî¨%–¡}¡ýô;s:-ïƒEå'™½t¿Ø <„>(a_à’ÐmÕ«,3#¬¡þ‚Nýk2Á[ ¼wìþg<ìà�ªs™H²Ãóx_T(“ÉÈ›ø²4ý^Òx J&&&1žoßѬ}Sž Æ~ï À@D€H 7ÐÀCŒÈlêB£�ƒÐ§PmZ©!“xøÊztîs!‘ÔÇB „ >•@Ó­f,Q²nziéÉ®O]u×]u×]u×]u×]u×]u×]uÂßÿøM3âVÝ/)n–ª³dV6ùàØƒ/†š—³!ÐlãÍÅH¿÷§[i°Ž•® ÆvÝépQ…ü_¿ÚÆ`p¡«Šä¼ÿž !¾€€@¬Рv‚�/L¡;óÌn¼§®/¤G@Dn&D“‰6D€€#l�G¼,œ ±{¾×˜e^qRô¬sþÔ`—µv<ÌL¶@@)…×]u×]uï4_ÏAÈq“pÁ±¥·¯8&ESí©IFÈïHrZ2XL)¨ÐÂ`p2€ýÀ áC- ”¹#Yvëƒp 8pU¢*}.@†D=Ùò²ªX}(žÁnÜÌ™‹£«ž� éª.š[ÿ}á¤Q_âƒël ¡äù€ÿÅÞøƒ Ș€k®ºë®ºë®ºë®ºë®ºë®ºë®½ö·±±0ç� �ùza’ÿg–+'\u|´¾ 3�O´G/×öX#²….I–†ø �€°(€äX‡\^W„4èTèM~!@)g€4ÿèĵyžôI‚!@äM~1@x°fÿÑËÿðW¿ãxp€™� Œ`ÿ<}‡]u×]u×].4§Ò‰ãàŒÓ £ÄñÀ¡ð½È—¾x€Ù…Viêþ¨8?)�–]~¾ 7´Ê‡éï0 ½—¬H~¡ø� * €ƒy'P”%'A-ÿx‰Úí×xrÜ…0² Ó¸&Xˆ=¿îÀv'dRuL`å¸;Œ$Ø%×]u×]u×]u×]u×]u×]u×^àÒùr À÷Ý{ vø0£î¬8¿÷´ÅäTe]@Û¦à:Rê§`‘áª@D½?^�@¾„@P�U^ ×gï+ž¸ û>À01gŸxÃ@ ¸”—ž˜HHAv~ó¸¶�§Ø1ö`œÑgÞ 0k‹®ºë®ºë®½éOŸ‡<™€-°v(«xcð|¦ù n!;×W²ïaب�…™Y#äßëÒ’E\@4Éÿ‚ ‡œÇ„€+xÃ2ˆ4¿¶†À 3�ñ`/ +m´ÀÖý¡09Dñöè{žO]u×]u×]u×]u×]u×]u×]{ü?°Þ ò(1Lû{À³&DI„Àå�æ09ƘVǪ¾˜˜@�àa@Í­^{¤ ËO)Ÿ^Ü÷h€0'žZ뮺뮺ë¯ÿ°æŽ ôµöþ½†žƒVšŒ1ª“;+ú`DÀ \ "Uvp«ºX¬D ³‡w@j¬´Áþyÿ¹ü÷5×]u×]u×]u×]u×]u×]u׿Ãû à:¢6EŠØ"¸Bôm@¶µŠaîô$ † ~¿ñ/¯0ï!¥€û1]³Þ <|%žð¼WK=áàb€â³„³ïO]u×]u×]{ÿý‡0‘Ï(ô=Áãôˆô•‚$&{¢ß~�€RØ€'ðúÄ€€ЀBªFІq…#ÕMEíÐ1EJ^¨Sý¤?Ò×]u×]u×]u×]u×]u×]u׺|êŸÀhdô0@�™â³ÿÜl¬Àc$)½àëÝ3Éú›ð"|/LpžRÊÿð}#¼ØP¤üÁ_ÿ8$À@ 8x?�ŽG…_½¤1À B’Q‰´P*‡lðÄülûÚBo–;¦”!¡-ECb뮺뮺ë¯}›íbaÈ%¯UÎdòÀ š‘ë€TREþ—ƒ„)Šþi glï€c@»ÿ p:-X1ªÄȽpað„`‚±Þ… æ¸3 Ìë@% îÀ(g°uà΀ƒwÛàÀ0³ãÿ Œ�u×]u×]u×]u×]u×]u×]uïI§ÒpçN’W0,`„ÿÞ•þÔ™ 0ÀÖô …~4ÿß`*H=k‰kŠ#þ¼+½yÜ! „€@˜pzärDß prÅ ²…ÑFf ƒ³0ë�€"÷èXàÍ8}v_éO°[űÐê®þêð©°©¿€ã äq5,Ô$?€ã ô(ž—z–뮺뮺ëÖ¶v·È7cS^û½…ánA\1KŒá÷Ù÷ý?ÿíáÀWt7aÿí)ËBþ¯c?½¢Æ:jŒª«~~øg†@�( `<¶9æ�-Íi«ÁIÊ€@àˆ]ŠHÚóDþ@@%€áÏ0"†‘›‚¤ŽR÷à}ŒH]}È”ØO]u×]u×]u×]u×]u×]u×]v¹*Ÿ=&qð€ÇtÏúÎ…ùŸðcq ¡ »Xêæi¤Æÿ ˆ%Ay*¿ësàH˜¹Qkñ¦óÀ�Ê4ǥسŸþ,l­ì²’˜çO €qf¨Ê‚Î0 �ûÀŒÙ8É£IØÁ(j«$S‚ïj9P`]a3ÀTïÞfdÐÛçDö0'ªÒÀЉ?{'®ºë®ºëßk{a Y�…q]÷=öð(Ê‹a“`k#Ü2 _óüPö^²ß2á”Ï¢E󌤣ÿ­�ë�]ÿ÷ða�8€L 1Àœ.`Úì7Rœˆn¸ÆGÐ�3O!°`E‹}L’ìNÔ°€>DU &%©R4fÅŠVXà³êEÿø+ùLö”{�Ùs€`„`<¸@ (4ï)(…³dÁ‡å Fl„F@îºë®ºë®ºë®ºë®ºë®ºë®ºëÞ'ÿ YúW ÖwiK}³áÎÁ�¤ M•#Ø'I*¾ @:+Ü?‹39ŸÏ9œ|3)г«óxú†™Z¢ è¿{,�Ÿ˜0$$àŸ0"¹š�ï�,É‘²u¯ÿç�# ñp#³Còë�ø",÷‰2ö=ÿGÀ`€@HxÀ4ÊBb“Š ébýx±;v¨OLàßQw`�Œ-vsÜp#çõàZd+#X7Ë.i¿#wAÉ„®ºë®º÷ÿ‘B„C&êX©W&h€•?¯9yp'Kdú e=ªúÀ™(é‹jlAùý�¯:ßíSþ€€à°€@Ô ˜>‘™¡6WT@ÓX#)'´¢ ø/ÖýP#C ÈŒk™²º¢цru,_ÔPÑÀ¿^š wN�ÿளLkç.œã`$iåý6ûÞ„ÛGx£ÝÝ€€8„ÂÐhw�_ÁæšÚàÉ : ?½@ó�}p*A@îºë®ºë®ºë®ºë®ºë®ºë®ºë®½õÿÐ,ÿTd`‹OJ5 "ð#6œÖ�pÿœÙ8ËF“‡)ªêž«ÇxY2Þˆ¤wçø%ìÊA4 ¹Ò=&†Å™ü*<Û¨v!nXð`=˜@*Ð+—€ð7o([@�� ¢hVE W/à~Þ8 ­ @ai´n„ÙEAŸÿì8hêAM^=X$›¼zâM�cÒ¦HÆ® Ã@¬3z;6” ”ùï6Ûjˆò`òŸ< ÿÿaÈÕ†þ«5 ß´,ÒÇ£–=Šñà¬Â Á’l_µ�Xžö²lµ¬S`�:Öƒ?·ÚÌ9-þ ¡øñŸàùL°_ÆX÷�£ñ=ž�Xhƒ$`úD`6¤Bƒç�xj@(3ÿý‡#²æxöz-áï$Z CÞj �$¢�Y„†H(#±¬‚€ó�d 'özh"Î5€ Ï�þÇ|èÅ�xy`Òâ=áå�›ä<{ÄLÀ’,ð¬ �ˆˆFÚÙäG©Üâ0o{gœ� �6VxÿùaÉA`Y,ò¢@µ:Yáé�Üñïv߯¾ˆ„®h¶wBîÍœÑlwBëfÀ‹ªR+ª/—¨®+€A AšBhgøÆpð ‚|Q‰âçAèÈùߟàh—°)Ð*çHõû¨„ ƒ—Áƒ åà< ÛÀ ÖÐ ¶c^î ›ÜàW/à~Þ8 ­ 31¯+��½Å%ÿ¥ÃaE´µß;ÚX(†i,îLbÕœü¿ÌZ™Çåàœ6Äÿþð³p³tx):<Èö1Y‘¨†©‹Yáîé.×~oI$±G2@1eïÉÎ~È×µÝ"ÛKUr,™á‡ûÆ Ñ¹8ô-È¿„6žŠÈ…üsià¬È"ÆÐ#6óÒˆbU·=3o=(ƒ¬œõfèÁaÃÉ�{¨Ë¡^=åŠÿˆˆ“Åkg¼‚.däV“=䆹‚s?ÿØp�îvYó¡áQÑ|Âcм0¥N!òôJ0l’S€‚áÊ AL”L$âç§A –î”FdQsÓ  ™¡®ï†EßþI.7>n7>`œÏÿö$€¸B«’\z8 ™…|x;Lc ƒ’7�Ãäý…¾<\9A()�(ÝóÑ NzÅû—=�Å›ž €Ó€¥ Š—&HI™#À-HÎ'B¬Ñ7`¶Ü€ ºÁH Ú"ú”@›öýw•0 _½z .BÎÀ=ÒÝOzG�[°hãÈŸ¬xM$W^¼±S�Mþ{½bi7»¡ßøÖ9uˆ1!dpž¦2»Êâ!º»Ç2$$�xäÇ¡^=ëŽÎžÚj¯E•t\® #Âã@O1|rSÖÅɯJdõ¨ôJࣧéfW-aÚ-`¡pä !q`wÚ$ÿ�Ãå xð"{T9 “£À;�¢qå×ó&0—·Z”˜Híû© ‚H�ºdzA‡÷5qï\2~±6Øå_˘vý¾°À·šþ^©fÀ9°Aš„jpFDfdDFfDÿÀ`wÈÁëÅLÀ?¬ÙÌçI�Y¢¹Ï†ÄÂõ(¥}÷ëÊ~]ŸðN_0 ‚(" ÂâL�ÌEcçC€®2VsÐnÿhˆ!½ðFhœyRãŒpúký2¢Bß­cµ9ZÑÑ!iÖÏ} �A9 `å„F‰¤×¤Ma1Øëö†×~#fSçËëi΀5QøìÀÂP¹-|ð‹±ð MÅž2I¿ÏOð@œ{@Ÿy‚I3óÄFׂùÕš`Nv\¿ c�¾ÛÃK;¸lpr0ƒß\…åX“Yô*`‘Ù—kÁ´ƒiæÑ]b®ðšuÏv#\Ê×Rd|à)¶ëžì†ÙÿÿÁ7ÃÌóCšÁPH ¡a b2O²1}F )KØMºù…â–?D´Åô í½½Ó°„o£ÆWêóÿhñ•ø';¬@\@\,6Hžˆ{ª-’ ãÀ—ÝD91¶GZÏdÀsKÖ›ì.ƒ€(Q§[Ó}» ×ðøpXÖ$hh}4>y…3­õÊøê,ŒBSŸk›  ï¶éVÏ0L8W·ÏQ=þ¯ÿ‰Ÿ²ÿ‚uÇJa+öä?ºBQ�cÀæS´h˼͕³=½mZŽ­3?×�ÖKAšÉl~ Ïçòø %€§HX€¦P Ø]Ÿ¢¡ýÏÈèu1`aj_z€ó£˜3ÕÏÔ>(Ø9Ÿš†“‹/ÔBmˆda9�‡l ð呞-ëäø»ÚšO÷÷ûÿ}î21Ò'åF?øv{¬‘ᅫÿý÷c˯0Œ-À6 _BÚ]O¿b1øFܪšé€ÆØÀf"RÉJïmmVG=ÄÔI'nà‚¸Œ’ ýï¦11y†à€4bC‡W?õÙåv8 âÆ¢Tõµàçïÿûà4†»ÊñdI„Çþ 1¹^<©�èÿÁ‚Á1üÏbmš\ƒMÀdøA¹oõ):rß�stÄþø;°¦²¡)=»ôOÂÍU)j¾ eCe/Gÿ∨‹XeÿA(6a€0àk#o«ŸkD–—‡ûœ `ÞtÆ /¿a €ÇVÝ+,‚kN £bÏ{¬˜g`G8iÝû œÏÿ³9à¨Õ®Á˜4½H/ÒÌ[ÂMNÿ€‰œ^ç÷ZýþÁ€ ‚L@D .Ef>„λAÛØ 1B/~€¹ŒªÆG} vö& XmïÓËÿðVP$©‡»Ö©\¿Ø:‰$ªŸûÿü ‚�€ð¡ÌƒµB¤‘5U›`v׳FÕwPaÁ1ß3³NÖ—$�BÙûç>ÀkÏ'ºX1¿ö¼,äŒ&Zþüýì×ËÎZl°J„E%?°LŸ…ºgŸþðá€(«€xà2Ò¨¶¢1¨ñ­ªzâê\c"v„¤I_¿ €1i\}q˜J†¼kk‹)~¬ °‘¦þü$ €N¸ØD.<YRÃc„”–Ç@�TÖ4|†6‡¨[€—öbRoºÇzˆ‚ÐÆa†9^[aækÒ9-GÏž;ð•Á+ rÞe€ ·ò ȨY O{ø{E½–ÃÿÈLM!ø¾÷õ‰*†]ô “ó—ïøk�vÀT•^y¢p¿sx{yù¡oˆÞðàœBÀ9°ÇAžçE8!à-žg€KúßÏh'~dDàq ‰ `€ $€›ˆ3ªQ.!¬|_{;>;JEÓn’Ûïu¢)/;ø€L^´Çÿû=$îÒý}Ç8Jýív¼? á€&„@ ¡fRkG´Kïu9)SðA°Ý÷ïÀ¹T+çäCþx1C €`Ì�wæ‰AÀçVã ‰W~x8[޼Ù1<ìc °ï£ÔIðyp v*%OzàíÒ˜ÒöÃK.¶�ÑI—q [HL¥ÖB<-�ˆduÖ”üíïü¿ø�gÒ�óûwôÏ‚p»!`)X €à(i�@ ༠YÌDðÙ¶® —·ÀÓçÑ�¬æ^±™`?6oAp/Oø?ÌÍßø ‰7ÿ^ …à¿ Àޤö/ð‹Âx\Œž ‚Èk@ 0³ "TÙ›ÿT–N¾ìÝðaëï�zãOõh‘dzÞ-6°!2-Ïg¼ªÿ–«Á8^DO óÂÑAv +¯JEwž0Ž•¸ÀL*ðµê×¼ß�×É/wÿàê†Ö(:­’Aûªð¤ÓŸtØt°F48ù˜[íþnþµ�hßúàKj•¸ åX�;øSG D8…C‰8Ñ$@á=˜?ퟜN œ�R €€0àL¡yxŠp÷ÒŒ6áª(ûš¨\’pBÉÃÀg ˆUó†ç'/„&ª&ûø)Q°Nddðˆ„ðˆ"‚ З¿ò6×~x˜ÓZÏ‚€®A°d‚boð“·ç„–ߘ'Ã5 arIÄâ ˆH@2+ÞrÜ�ü…xÉbd#¶D XEŸ@!à-ŸÁÈ0 à.7%3pZÏ�õgÞ#Ë�_0 {¥Mó&ÿÛ@H=*ðÄM¶Œà@A‘,_�þ‚•÷YôRIQìùËdÌ�Ÿ-W°þd»÷ñ Âä!QÀ–*À�pÀ%\¹êðõʥ𴞑㨜waûfî��¥Ÿþ¡õ6o§Àý• ¼0N¹ ÄØ¢ €2w×™JÒ@"<ß&ÿà ›—g¡rIÁH„„€Ž€Œ2%: ªY¼ìˆÔ äçûkèù‚p¼ˆœ0 Ä�€²8"€@$AÛK±(¼®¯‡ð-¸ ÄŒ»À •ï—ÄŒJ5Þ6ÿ+ß/§Î¸dÓAt@Y€±ô¸ìïŸ %Õ/uúÿ¸¶> Â舟ÅòÅÄŠ`Ÿ§K[™¹åàHºA› 4¤Àx òþ ÃàÞ &Düù7x ÏqBD¼ ÀRžm¡]Ÿ¼>/óë\]zÐ'Áˆ Ëàà !¸�ŇX¯eJ YŽi~ß÷ø­;l3dN@ÊÿÛ¼ääéㄱ0ÅC�‰¿G¾¾{ ¬'�„ÁŽŒµùÞëàœ¿†A€X€€Â_wÃtæßð//2#ç Éô i§³ldÀ…¿£€cñ&ý½°Lé^öYcXÓ^ÞØ´ˆÏ–¿p`�ø@"bìÛŠ§þùN¢~p`#›}�˜UââUÿ·À zþŽA0.[¬ý«CÄ,‚/î»6?ìhÄ!ÿ@Èk¢ÜB'Á–b­�a%ÍWÿý·‡°ï2T'_ÿöÞ—ÃA ƒ È0 ^LÖ±å#ù°v›'>YÆÑz+[ôŽ÷A�èG��·|#£aº¡„Œ—ø‘; „ÀŒß½ðu9ñ «·ÀZ_%!Fm�;îR[ÿ[ØîÔÌÙ‡˜U;SÞcçx!e¯ŽÛJÿ½xu¾ãÿÿÁ?ÁAø–ôA›,= "PCÀoˆó?ûç‡x&Dc\Í•Õ7ÑÃ>Y ¼ÀBð—½�±46GKò¿ÿÿþŠÆW€ ‡Ô"ÏÚù¯NNì\ Ÿ�Æ ¾0ÓONMhËð †BàÞ&#†žÂZeßÿÀ²fÑ+n°�ϯÿ�=É ,a÷q° ‡¸ t [ÿÌ;ÔBŽ»_Ù¸-Ù\Li½ôJhI]lMÿ±‚”yVG�®ÓŠg?ôò2S4X|˜q­ ÿø²ï!¼ ý{ª f¨_“ûèvæt[ßx VRx;3zñ¿ð@H  €°*Z$� a|�Ó彡j °Õe† £ŠÙel½C¦Ôš T•îž�½ fh(fGƒz7 ™`iPÀÀ8Nvbw ÏèÇ´œîÄýÞ¹Sï)×úpIÿe9µl�ØYøytÇÝ<ØFˆò s«r1ÞßÊZlqW‡Þ=cªÄº¯ýÜ1ŠÀ 8û±*;þþC%Á9ÿÃq_¥ð0|ÀD" 0èÜgÓ&Éß@¨�c!Hû\_Åí=žmèvêtÙ›· 1à>Š*8‘b" 7�CðV'Óþr“-™·?¨ P¶W7:†KÅ«ÿÙ%&ÆÿÇõÇ!TÎî;Ô0N_€lq P �^s��&égÂU˜—´ÿ€ 5ú¾æ¼nöª¶#÷¨Ì3Äj29t_Á€s`GèÿªÁ~T¶ÎÕÐn×ø8!ñTÞ=»¸1àY<âz(¸cÈ?8oé%Ôö5_ýàÆÿƒB"¦x}uá‡À™¬�áûßÜpN!`¤üI:ŸK@!à7ÇÁü $0‡°Ø@Qµ­¨V¾ÉÜ?ú{@ -ë’ÃeW(Ú£9ÿü~}­(nÿÿ|? b@†Ð@`ŸCúÖ^+dS;ø[¢×­Ñ\µª/›{påŸE÷ž�úA›NQ ‡€à?çòøA¸@’ Y„‚€õ-š¼à q3LOÿði´M¬ÔÍr¹~ü7Q»A½0a�FrKdKü "ÜÛ‚&�¤Ç¿“÷šÆ� I…µ×ê�‘÷ �/“ã6’„ªŒ&[Ÿ L/�®a^rÝ6¿ÿàœ¶éë@Š.hÿë˜Êpÿ¥ïºŽ(ïÞŠ•#lA ÇÞ‰3!…ü0úà·É÷0]é9(D®Œ[i}hi>s!Hò¨ ]XùmjÓ‹ÿðWHÁ¬1²õõ8 Ëm!ª QïSÿû •Ua„@ ÃÁ�€ àr`h×;t.£.ÃÀr`4ißÔ"FpJ/ƒ˜d@@2CÀ�à 2þ¯«À^Ÿ4ð{k¾oÙ€»ƒûÄß´Y&žßxÀÃò\:qkV|Øaäs€$ïKÐý4$¸´|Ýñ°G<“Ø m%U‹’yÿL""ÿøØ&&s®bo:ªxÃ:—üŸ —ìgQnj®ŒËà|  @ € @$‡n�(­±ñ~p¸ aßa³ “†ð€AB €0Ë(IÿcA�à$ ‹º,j·bÏØ �EÎ2rr5dàИ¸~Ó¡-N+?}Àth%N;¿í¦ÿ‚¸#)Wm®¿í ]CÜÌÉÛÿ®ׇ™»’':ð@ <@ @0€À¬ çò|€Ñ÷Á‡AªÀHH§37f) 8&\#8]à—cªÀNÕ¥ø ‡"ðK´]Xµ"ÞEê…¸¦b¶‰ÿøëhYe;}tp:@ÞBË"‹Þ¦È†àÙÝ-ÇÿÕ`œ*S*ºãþßൠ\K'? Çþÿÿÿ† @�@ H€ ¸ž)«¢>Cúè0à J2GÌSˆd¯ Ë€} |Ÿm@!à8ãàä1‚l»�Q3à k4G÷ß[,yï¸G …Â�` £@ÌÕÆ­ýò6î¾ô“Ü@‚m%z~î�_2ÍÿŒ„|¿ Ò�Ý#¿¯Æ¼#Vw¬u(ßt�úüú& Øp/€íÀÍ¿‡¾á6…Ȉ¼ [Ä� á@*–�1¯ùõÀÇi/»Ý˜~ú¼Õ»;î ÁPº[ß¾¯ ª·6õàëÁ+ð¼„|ÂBL€H† ØŒ\aû$…&ô€Ù àY’7þð4ƒ ™p`.ÁÆ&O‹f$kÈhÄÇ</˜r‘¯jðL�„áV„A�d"‚wè&ûzœ*Ο†,ŸÿønZ0ÍùnèjYa•ûîœ.O°‰!w�aõ3¿Sþ�»ú÷ü ç¹ =[yÉ/’¢E* ïÿÉH|þÿp¢ìvßžÿ€p9¼ï}ùïøÒ£A›r„ÏÁ1ÿhCŒ»u}ÿž¤a•†e$•±ŸÄµò“.៟—€ Ü̧ {ÿàw1츋2_< `q Àp‰ÀzilÙ'°]I1ÌF¶Ql?@äØà2qPÖòdïýq¶Ú ¬aNgûVmƒ?(t½í�†ðÄ=²k%ú~ø+·‘;ªn @Û&²Q‡Ó÷Àp¶¾Då]�‚SË—às „Ú Š À X/gßì$Òmf¦RíifÃoQÏÜ@�ƒ#VM%¿ø2’˜ç?_èÙ MÜíï!0^äîÿàGb$™\íï%ð#�€  Z!¬ï`bŠ;]ïü“�P/0ÃÏlÏÿ†�$‘Œ™]üáþ‹q‘²5õU{þéw�øpÑj Ëê­û{î¿x‚\àÄ8? �—¶x²**à•P̸õ å/†`„ã8/‚' µXd m�ðxbv½Z“¸ûm3ôG² ¦Úa¨D ½JÕ¤N=æƒ ùW#Ò•\Ÿü VŠjcö|GEÀ kЖN*¿`àËw–üB²ü5²ÍÇwì(+ Ž‹”IÛP†ÿÆ5Sþ?ÛyrU²ø?Ì&ÓºÿîÁ*áÃŒ…×;CV&݉P�X‘B¦.H¢¦‰ßF5Uס)0„‰ðô=žæ^@®ÿ·"Ä-„üÁÝÿi~c‚aì:4²)ËØÄ)XjÈqÖÆ,ÏY c¹‚7±f両 ®ý°„‚"8…1ˆ^öÍæöm¼>ú‰8Š�‡99-yѾt­*Γïý£éZ*YП:XEYÒa¬ÇûÔÝ > <Ÿ‚“õ˜Ð „ÉAŸ�E8!à‡á e@à0àk<‰ì¿¬GLž—v*]ê=õ>¸8MÖyøºÝŽu<<Ï…êßñü/!(@T€ xD»uõ�ƒ€ÊmîÔÃÂ@uΠà÷œ£Ú‘~`àQ€=Zõö€êÁWà“å¯_hLûºXèÎ< jÿ©«½Á‡�¯¿ÿû÷ÿwïÿ¯ÿsÂP„"ð€ 0OMÂä!œà„†+�XHxIà¼h!j=µLÿë€óvÜ•oþ#ÒÒìÜø�£÷ü< Øú?â¯)/ß»BeG�ô~�ø� åd¦ÿÞÀòJ£þt^ ‡a€à°: ?‘Õöþ8!•pjÏ»+»€ ÇØªÇè…ÈB>†‚»0`Hð�xtHfþ�¹ìÑàç�°J~Ûßñ¼}ÆOùȽ€InSá÷„=/Þ�Ï� µÞì¡é«vñ¸ïÄË q04Yä³Ï%‡|W÷[ûo¼âîðúaúØ\„~2aü2À»¯ƒçîX¸Qs¤ 9Ä⣫LÎêßj?âhÕ6 ¾õkÝõ›íGüM�A¼ð ŒBŸ¯taC‚$…ÿá+ ½ D›À´6 ëœ ?Cý /|çþô¸z¶Gý»çà -Užß*Ïž ‚öÉà—Ä‚ƒ”@G€ Òùº+÷ÿ,I{qOÝöÁlAAH8p,7 „ „þrrƒ¨þ÷œ®_+à iê?½ç+žߛÿ¯ €j BöÉÂ"ü: ‚àÿëññ}ýÿÀ=¯\xùgܤ.B/ ‘Spqfåêä Ø×üò—øû×øàg_ý×ü ü~(úÿßÿð?÷ÿuÿÃp$ƒ~aØ!È=>TUÀù©{áë ó>ó$½ðõ'‚Ì�Ãpù7Ø2¿ÿŸðL¿@�H /�|ô¸\‰ðr¢‚"˸£?„:‡÷ÿãú_¿_�üé*Ÿ±@!à—™Â\¸&~H�B Ž÷ [6ãk< P„{€Hü7ü‚ø•Sþûà…¯&+tîhâU?ûh\„^'á ÀH8ex3öµž”Ddl’Tþ¿ÆSëP•ïXíÁÒ˜–BW½cµÁ+ÌÃ~…°R" , „Vå„"àê“7†øi`•øÀTù[êàç«SÕhøýO�¨i^y[þö<›%§¯­_SÿÔ‘á†#‡›»°þ›q;à”|�‚ƒÀDx jŒîø5›ªçз� †At sv «>ýu‚jgß­´I~wÎ-µ�J¤ø?Ö)JI>à–`¨Õn|ïå(p2~Cq^³•ü¾é>óýGxÒùA›¶4¤ÀxÀþ?§óûà )Ç”0&0‹Jß/¿Àû6ØBø¿÷àé @¬N™VNp‡b‚”Ë02K›ýà а›ù_¸a¶Xòß0ŽFï’ï�ƒ&YHÍý/® Ç@�¶P 6ô�.ÿÜØ1j�ÈÛ)ã”�«îàÎL7ÌË,þ¾ W™�);Þƒg—ÁCŒ¾‘fÀS°Bø¿ûëÀ”Îë÷€ª¦¿Ÿ3ÿww*˜RYƒ L�-®rÀ@¿‚úæoˆ×Ù‰J«ÿúÂØ@–˜$YË#ý{|p“όу �à@ ¡àÆæŠžHz-ørC¡À½£]Xú`0BªP�Z§üD-9WÝS1{½Ëò àÁò LšT!ó�üxÏí0M#¼D…diå˜Q,ùõËK•ÿÈÆ¨ßóà6µ·¿Ðý·ë¼tÿŠ’¥~"Òi@L ’’AýoÙñ‹¶õ�'øÃ€!+ 0°çÝAŸÔE(!àïÐ  &Oåz矂¯Ÿ¯Âù ƒ0r(" �`N¯­?žpî dë¾¾¹õ2^ïÞðÀ[¼oø¼á‘€m>�€è¡Z°ù¹Þ8– ` øuÉ0IÙ<Ë/à›|d«óû·ÿÇ(‹ 0Žº‰åï|?�p º§Üнš¯Ýonpߨ¯ˆ®{öÿ¸çÂí¯2 È�x¤^ÿ™ƒÒõ"ÕïýƒÁ2Å ¼¢ïl@ ´í–¿-ÿÀ�IÚ[Ïÿà�ø[0109‚îðèà¥ëë<ðâÁª"ZŸåE\ã”pÈexâËB! àGCE‚ÀˆH ‰‡^Û±†€ ƒ�Ò+ïøhýh�ÜwûЬaó¯ã˜* ‡Ìn[Ã3ô«<ÿ “ÁDL2,JÙó$û¯äêÿ¤{€þ[oSžà?‚Be àÐÈß6¾åÿ€;åfò‹³¼­8ÏÇ5UÓ•Ô.BðR0xLÅADü°sͺڇ|¶lâ‹?x_Sӕê“Ý¿çGJ |žóð—�iÃìOÝ·òC ˜Ã#‚â°êæ±÷ø®¹?” §øÞïþO?ú}áüçOŸót0`C8à@O�Dn¶deà¿W£Ç;ö—¹â3©Pì]X#oyRcqà”G Àp\¼.ë l #ÀuóÒÿœsrsÉÊ�„àÜ‹ & „Œ‚ð®6E€!½k>°ß” ·¼ «À[¸oîw†Cá(˜˜PHdWYLËF’?™¼A»þ�% Ï®ßÂí�žpÈàŽ¢„0€aÂGžÀ(ë�%0;yIßÜ_x1XŽwØÓW?˜å~7¾:»M/| ¥[òõ �‰ð ÄÂLÁp«€Ñ¡8‹º ?ü@#¥™¯G‹ú¨w Ùo‡rÙëø~E°I�Pùš [zöBèÀ’0»! Œ™�Ù#ƨ¤{ô7/_4nÐôêÝ¡¿Ð³¬&H@,s�ßÓ|ì�E „¾Iþ`£j`TPS©ÇX6(×I³^‚G޾75óþH![§Ág§øÑú¹¶°È¾ª!c$Ædlu�ÿúûÏûz÷£Ý?à›à ý‚“ð ÌAš= *PCÀ+'óðH?â@¢p…”<‘¨ÅWÒJ¿6ŽW„á`˜`l,°ð‚ÇU±ÀYÍÍ©áIß õ{•÷ß.˜l5 2ù'ÿ …ívGø/:ÀÙ¬¬4Hi!€ê³&IF”Èlì¼å �¤0Ž6·ÈP S"SÉ ú¬Ô´¥;–ø!é ­\V÷Í6‰™Hj¼$Ø!ßiC¶øN„´ð¬+.çhÁ#3›1¦±¹¹ÙÒ39³ ¾ÿ.Pàð$í5€E‡¤¶å"8O¤0OAyÏQáD£ÀSià•îC›ž Ѿ—ð'Á3 Š4RwšS0*ÊwàyS`gYkü =ö ˆµïà|€�i§¹"—ðÈáò>™%Ðêò6Hш‰ìì/ð:Ÿ U‹š_ ǰ³ @&(‚öÈ9’ðúŠàekö ;ôÃ…FÌüÿÏÖÓ]Ô¾Ùc³Ëù|ÀBÉ ¨³@þ`5&0¤…½òÓ‘‹ºH¶I¦€*X�=«[?Öh�m±{Vø1) HHÊ@ /l�çâDèÀÆÙÕ'×ýáô, P®à¥H½ð…k0.Õ–‘_kÿXÕ‡H£BÖÝxp�Dfd~Æèí¤sO¸Q$_ ¿Ãߘ¦Gæ0Ô´]»ÿa•~þêX_âVÿÅBüõÿÿ ðP~ OÁIúÇP€óž8@!à—ï‚Gï3ùh[ÀA‚ø b¹›×n4LÍãoþ=лd&6øLA y²íc¨doyY�3¢&�Å'ýWW ��pØB G p  *à„Q±\˜.7¦¸4°zmWS~TúmwÚø"&ó8ÔûQm½�˜^ ñ( † _O";º=+ •E?;ÿáç�#Ôóð¶�fÐÀAuà-Í©€ðniJñÝôû¿þÇÀŽBqÛ@-|^Bwùž­™‘ÿ£q$D («?à ¤WyÅð<¯¼@-Aš:ÀÃÿø6å•so åÞPFpJq‰9`¯×ïàÙðPŽ|àU7MÓóö•a൷)êûû37 ÖyÅ=Oÿ�‡�)n3È ­�­/ ¥¢ àFÕ<à ¿IO/ Á 0Gd�OÃŽó��æ›^ü‰j?߯Šqa£ü¾!�¸ÃÁð vƒ³Í¨÷`V¤xþJù�¾BÔïÿw%&F´šR_P!À„)P«†Èó²U›€´r&îümÚ~*Ž{ÍAÏ_xF.qÂ-¿ø7n‘àÅ(>Gåž/¯Õ4$Ë}áĪ’mà$F¬=ÙÎ’nkÖ‡¤ìÌ;üƒ‘¯·=p¿O?¼o ·®®9^ßÿð‚CË'J~ OÀ/0”Aš]ÀŸÏÁùCŒ¶= »(uBo ~�"@›ü•\æ<†þwLÙ÷%#ýJØ™éŸøý|À“È¥Fò~c¦›ÓðèbÔž@sH³ù8jz1¢Ïÿôà:iŽ£ðî<¦Xfxž˜õ2ã˜ò«`c$Ù�E[æÃ4Ù3ÿýxPr¤eÄÄu&L©€9ŒùVÀi±’ùð>Ù[ᔦ›&ÿ ïÇ‘äüñ¤ÌqâGàãÄ:�À9ŒcÌösci³í«ç†W$Ù3ÿý{]>~<�OÌÚ}€8Äõ@0>xKL¥âйƒœmO ¸i<ßü£Kð³,‹vÆëeù_/~yY¬Ýš¢_ùäBRq–ã/Ø©lJ_ÌÿÿAªÿÏïëõÿð‚úu¿wë+—æ?ÿôý¬_"“Cj—??h Ì;òË9U°!g/þ@m1«LC%¿cˆ–üF7¼ÃÌ_†1ÃáÓ9a8ØÞîærÂxn�ùþÇ€íAž{E8!àï‚)‚0öiX[Üж‚¸ aÀ Á…à}w@9ŒåµþLܲíƒó<Çál�!À!Œ( f˜“ïU44÷>‰Mp:ùp¶> B^p@qÒ– êž¾ÁðYÍùü”ͧ5Ú¾+6ÆÁ€!€ ˆ+�¤i,µ>³�†6|Ž˜\„k…ay `�#Ó…]åÁMþŠƒ*TõïŠÙw.—x\„k�”�@Ä€Õ…P—Qø =Ї§–AIWžÕ�ª@ …#XÍ\Ÿ¸~F+“ö?ÿà¡xÖžœ@!àHp+4€p|R;ñ�ô-‚`à€Þ~¦ |ôj#£ÂØ€® =àO As˜Ζô»ø9ÌðpÓ0»d&‡ÿ/Á(@Á�ÚOŒ��Å#–ô£ `` F 8„„TTÑ.ƒks·¥P¶Ž ^A…˜¼úèF¤FôïÈ3³iµã6›GåØ]$¼&X%Ð ã¾TZeI²DË_O7Këý¹>çæÙ4Î,èO§Þ½óÎŽ�¯ÜúýÚúÁ�%eˆ‚>ÿÿ‚(  ¥ã€ˆpÅ“““““““““““““““““““““““““““““““““““““““®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë¯ðÝáÞð¬IL©3N>è${`»˜ÿƒÃNgŸó�³¿‘Ç.C‚€i€@ÂË`O‘0Öâ"®`bÔL”̰§Ä.U¸ˆ¯�F�•f�÷õþÿAؑà •õ©_fÔØ4kF;Õâ•%Fí È€€ó)¯ň –›�`÷„€¥€€€°ž°{ ‰°_mö ¥ž°GA`Ñ6Ïíø1x”€ÆUêÿÆp𨠟bx¹Ðzr>wçø%ìÊA4 ¹Ò=~ê!€ åÂp@ÆB¹x€7o([@.lƼ® šÜàW/íà€šÚs3©Jn÷]u×]u×]u×]u×]u×]u×]u×]u×]uïÿÉà»ÄŽr°ÌÄ’µ+ÀÄ/Ça TábÿpaS¿AX�‰HÞþ Å´ǽÀ‚Áþòà€ÊÞî ¦óh�MŸuõww)r ðtþ3ŒŒÐd4ÑÿÀ‘ì¨E—€X˜‹ržþࢾ:âJ#ëS 1ª¯»Ígx=RAEB½ýä‰-œ e;œ0ÂPÀ‚Ðà ðQèD¢¦yÈ!tÊ€/­ö’±¨tÏ„«Þd(ô8ë´£@Tdl@J­ö’˜n›Hzñ}á fw)økfóW†v‘ÿ«±¾¦Ç×àÀ@@@—aœ=ÍžYå€ǰ1MÙÑj²˜ 9ÿÑL—txd„*–ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®½ÿù`†ç F-xð ¸$t! ãìÙC?Ø£’ö’!6ˆÛmþÀP  s€…j‚é_#û÷k�|¡±!òß¿R,ŠEünÃøV·Ï6E Ø ~àÕ)LÓaH½%XéJ¿ÍD˜dR»é­oü >‘UQ³?ý !¤ƒ�@±„¼<ø….Ä.û÷=³yžÍ·Ÿÿÿÿ† Ô|crA²^ýÏlÞfÙ¶óïÿÿÿð×]tõ×]u×]u×]u×]u×]u×]u×]u×]uÂßÿøM3âVÝ/)n–ª³dV6ùàØƒ/†š—³!ÐlãÍÅH¿÷§[i°Ž•® ÆvÝépQ…ü_¿ÚÆ`p¡«Šä¼ÿž !¾€€@¬Рv‚�/L¡;óÌn¼§®/¤G@Dn&D“‰6D€€#l�G¼,œ ±{¾×˜e^qRô¬sþÔ`—µv<ÌL¶@@)…×]tõ×]u×]u×]u×]u×]u×]u×]u×]uïµ½�‰‡Á�° ÌYçÞ0Ð.å%ç¦]Ÿ¼î-¤)ö }€g4Y÷ˆ â뮺➿]u×]u×]u×]u×]u×]u×]u×]u×]{ü?°Þ ò(9L<{{À³&DK �ËÌ`ra¦qîtÀB` �„6µyî’€ -<¤ }{sÝ À<žyk®ºëÞô ¸<Ì!�RT“L[jŽ�Ôßö?Ip@P9¦pxŒE@t«ýGkXÕfž¸S¢y³(ÆM<*w)åÿ‚ê :\> „Ú–™lm©l„êʳ#§Ÿ•ÄòyOp,¿ü»ò£,+–íÔËS-=u×]u×]u×]u×]u×]u×]u×]{ü?°Þª#d£ÑZ‚+†(oA VÔ kX†ïB@¨`²5ÿˆ}x ‡x@ ,ÙŠížñà(‹á,÷€íâºYïœ%Ÿzz뮸µ¤Q�ãßÿè5üþùýüŠèg�zoó�ÿè5ŠÖüðôJ¤2³óü=¸„jŸ�èJá òŸáCņ…(47“€°rêyþPho'‚ {6§Ÿ÷ÿú è� M€‰¬~~òÒ�Sð>QsXüÿ� ¡~yAHgÓÁ 3-Ÿ”ƒ>ž ˆ 6[?¿ÿÐjPHŠbð¢€:¦~‡ˆ 1ÝLü@¢)TÏÏðˆ Bñ!hzäÐ+0-O?¸zà W`zž}oþƒQ!¨u5È H€5Âÿgð|>‚ª~ iÌ[­¾é�4"‚ˆEáO€L_ˆAxO§‡@·}_ˬþ«ñâÍ,:Èn�Àô%q¨~Lt³MSðá”Ê2„%sÊ™ú•ÀŠi#ïÔ [0JÖ@£ÿ8€h@ À,*…/�ƒ\|74ò 0b’»€9àUuð¸áp„“‡:pÔ’¹�c'þô¯ö¤É†· T+ñ§þûR@ë\K\Qÿõá]ëÎá €ð„$À ƒÐw ’ Þùƒ–(e”.Š30T™€?\ ¿@àrÇiÃèòÿJ}‚Ø à¸-އUw÷W…M…MüO#‰©f¡!üO¡Dô»Ô°×]u×]u×^µ³µ¾A¸3Êš÷ÝÜ0(Ÿ ¹p¥.3‡ßeÿïô¿íáÀWt7aÿí)ËBþ¯c?½¢Æ:jŒª«~~øg†@�( P[ó@–æ´Õà¤å@ ð D.Å$my¢ÿ Àðç˜CÈÍŠ�ÁRNR÷à}ŒH]yÈ”@@=…×]u×]u×]u×]u×]u×]u×]®J§ÏIœ| 1Ý3þ³¡~güÜ@Á¨BnÖ:¹ši1¿è" P^J¯úÜø&.TZÀüi…|ð'@2�1év,çÿ€ +{,¤¦9ÂÀS‚`Yªr �³„Ìç~ð#6N2hÒv0JªÉTà»Ú€TXLð;÷�™46ùÑ=Œ Á*´°"¢OÞÉ뮺뮺÷ÚÞÆXCd!EÜW}À�}¼J2¢ØE¤ØÈ÷ ‚Wżÿ=ƒ—¬€pç̸e3è‘|ÅcJ”gÛÚ¥W_ÿ À@@a�ˆ€às×a¸ðú�4à¬C*ã_B4Ì<†Á�,qô0:K°D ;RÂùeT0˜–¥Hј)X`c‚Ï©ðüü¦{J=Çì¹À0 áå°‚Íò‘ˆ[6L~Q´fÈDd :뮺뮺뮺뮺뮺뮺뮽âúŸ pÍgv”·Û>,ì @À´ÙPr=‚t’«á€Ä# R½Ãø³0ã™üó™ÇÃ2˜«:¿7€ /¨i•ª!�ž‹÷²ÈÙùƒBN ñ3+™©ÞøÌ™'Zÿþx0Ÿ;4?.¹ß€R"Ïx“/cÐ�ð@ |‡€ L¤&)8 ®–/×�k°Wj„ñ$À0 õvøÂ×`g11ÍÇ>pï^¦B°r5ƒ|²à�ò7t˜Aªë®ºë¯ù!àxD1@ân¡ÅŠ•rfˆ! Púó—�çt¶@q‘/§òSÚ¯¬ ’Ž˜¶¦ÄŸØñÓ­üÕ1ßàa�H0 @@ @€éšeuD 1•‚2’{J Ñ‚ýoÕ40ÁÐ|ˆÆ¹›+ª ]g gRÅýE | õéªp$éÑGÿÁ\f ˜×Î]9ÆþÀHÒ)Ê=ÿËïp"Mœ»Ÿÿÿü0€@ŒÀ,&€€ˆC¼ þ4Ö×HYÐaø õÎ=õÀ©u×]u×]u×]u×]u×]u×]u×]uï¯þ�gú£#XâzQ¨i�´æ´ë‡üæÉÆZ4œ9MWTõ^;À2È9–ô@õ!ãù'µˆ¤^nC-ê_ýô€JyƉ%]Ïï´”}Î@H„0‡þþ{ü ` t  ÖU0nÏàÅØ ¸Âaút…±§‚Xôáî(’Óª¦#öS€‡Ø- Ä ‹óçþì féÁi,™©×U‡`p¹Lé:†'·ê}ÃNöÑ6¦8&ÊÎXïùÀˆ5b(2æJ$%ûÒó€Ià@@o@@—€7Ádpno`Ò€öƒžO| 3€O»À!Õ¾4�BÌŠ2-ºúÿÔý‚Þ½QªvÞnçií½{ëzáoÿü#Ü22åÞeßú¾îüGA]šs´k ,ôÌ…kÓ@�„g3„!mà 4Ü1E;C|gþ«–»ÿWh  &€�xƈȉ$³Œ ‚Ý,H5±ÿ` `1Y§8'�Ýü  8,Ø@ * ¨¥J’|ï**KðK]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×^eAš#l¤ØqÀª7yuàF’/ á=!½V§À x¬>ŒŸpóp¥?b'Ó?õû¼$ý ¯?Ñn¦&&òã´Ÿ~yq­'Ø¿?Ÿ‡ú¥&à‰úþŽœlá@ ¨…ÿ=Äð1†m�ëð”ÕBª¾Á �¾�Žëüè Ì6W¦Ð<`±] øÿ ízm'ÃŽîÕúÿ°¢B¨òÂôCB6ßô.B_§ž �XRXkz?G�ðU—º�„ ÖÐT$Íú(Î#’Æ·C z«vŸà÷l9,ktP]c‘[Þ¿åðap !«¤<&UªhüøÌø¥þycbêg˜gïÚ~/)PÌÞ¿Æ 5I¢±x%eè(ZCÉbð$^HÅ€NA À †¸¹¸ %~@L�ûåùþ(æľ¼XØÕýâ'9„ô?h Å¥Å@5>Bñ�.<£ˆ>x° Ø!HÃðG´­9¬ÝØ­�[á$wØL íIÀ2Âo�ás˜…+?€jn>)èZC©bÀ¸zÖþ0´E­bÁÀÃbø½¯ó=õ‚Cö_ÿüÂ4ü¾·ÿȈ¼�«È±æ£µ±Â‚vZ)‚2’íM.L¦EmÍýþc`&[cº ªÚŸ«ÀŒEãX(Ž¿‚`5ÞÀJH¶ï¼ b2—¶ð€  p`†Œ;òxdŒÁõÁ^ðQà�ºà� ¡ÿ‡ Ø�Kîð É Å¿ÂÉ,<·\Æþ§‹‚“ð$uEòõÅp$AžAx<I0G¦:zÂ@¤á‘ † Ç© IßÏÎWäM81�ÃÍÀä2KÁ*U€p”Ò‹Ãn5í*þK—ßµN1À²,Hà ‘8¿™ßÀXJk8_…�BB€p * É)`KÑNÞ±—Ò E¾¡Àù~ ‹}F€»;ø\„Hr’|€ü %ºz�îeð°.�Ü‚ pjü ës;Q)sD, ¡pB„„!ñ—޷ڢΒPû`@ã,°5nX]g«ÐÉgDC±¹¬0ÌK€D½´{õpNöå_°'^ ‚Z„¢ùj+ŠàH�žb@!àRCаƒÄ´ï å’åàEe?Æ·|’  ~ ýÒ O¯’Ì�€1ˆråµÓð¶ y!sái aèÁN�ÂÓ‚�H†™otªÂÓQq ˆ0ãþ2]Í‚j³°Í©ëõô¹O€÷µÉh ]í:@P°AëÀ/æAšg4¤Àx“”8àЮ?ùé•„§È¤ôxÆÖ_üÀU�WÀI T½›}p‡��XŸêw‚s=Wÿ@²|qñáÿñð À‡FLK;~�àC£&'ŠOÒÅZ¥WÅt8À°[÷áÑ¿ì˜XºÝî¿îL;8Ó§ÁeÔÊ&¡ÑûüÐ�õ—Ã÷óy5µÌø@ a€¨@   +šÊdƒTßÀrÏ‚�1·ßœ¥tC œ§hÀÿ…Vš@(r«{_œ¥3×J¬Ú0OŒiÈÖuøãÍýŒ]ûÿü�›{«—CýM,SH_ð œbå¹KûƒŽ,0à¿€=B:®��T_À²®Ï )ÀÕÛàPBþÕ,w…ü +`qB¼+´6Ïÿå6?ù~¿üþ?¦*Ôê¿]Œ°ýó9¼ü’€u¼‡7ÿ½þÔݛݾ¿Ú ÿ˜[‹j˜�do¼€h`d  �P&,£r¤€‡‚T_À‹–š§˜¨¿¿ÀŠ7*L}eü°Æµå #Âþÿ™¨}u5Xþµú9~ ÉÇÿà)û‰n�ö_È$Åž[=×ýæš4è S(š†Vïð„€"€ã/»¦ÞT�f²‚5&ÿ0Ar’À§î ôçtÛÀÊ‘bD.EFߘ.Rܵ¿tÏýWØ->î "”lì­äBZX솤x‡g`ð#̆¦x‚÷ 9ø'1zÅÐh†m4ØùŒ4ä{º·üÿqÆÌ_c/ùM`Çõoøïÿq"cÙ¿ð @ ?r"�qe<· vèʺJ�ûþäG ±giÝùW@°ß¿ë 0³ÁášÍÐà ’R�Ûü„QDDK|øDˆ‚$DR72Ì:š£ìD‘‚tFøÂ\K íSüï—Až…E(!àGFk§–ðWÓ[T`†©fPô4=†^Âåú"Ô7µxºð¹ \ �ÕA€ €€*0å n'kè<"¹cóûØ^¿á<­Q?�‹@wf£P©åä$éƒ=c|-ƒ˜ÜtLŒp<¤@(žÙ¸Kâ2‰Fþ¡?&lZï’Þ1‡ÀvùõkÏÇZa`Ü Á!ÆÂ €0-]Fü=—<T`Ðì¿¿;»yR/ÐÐ Á±`8ð@åÏ|û÷ï‚QÞ @’¤"·>É!Ô‹ò(àÕ)ñüõJ±ár¼�ÁX  ƒ¢’ ­‰Cïé÷'ÏÏØ¿À^×À²6,•âEÆë——sø8=| ¼±™þL„à*AXÈ0{ð Ügeõ´‡¿îÑt¤¤~?ÁÞ™D–½þY{ �yüÌÓþb‰E£‘¯%g®X'^QðL±°À÷)Jñ¾@_†#.Ͳ_vHËdÓa®Gòñùø'o‘#§—?§þ >ë-_þ¿ôëév?òûËáp˜Qc!ဘk“0ÑÙ]ç € …ïËN©¬~ó:Ó�‘ïýÞVšˆ¶©?à.Ø‘ÿqk¾�ÐG3*Pºýv+/šÚ¤ÇDrcI³~º\²ð úmRϦ�W œ‹Œ‘¯M<;¼ „£÷€h”'—r;)õ﻽ì >Žp ᄸ¡Y{Ô MCÿp\ÛèÀKÕÛûçà”ýXx#Ì~íüÿü aS›ÍÒTQ�k…@Ølš:�òig6–”;„�Ì4 0Ï€ Š» ÏRÏ¿àvR]�1`%gýùØ�Á7™jI@%0<žÈ@!àI‚±•sÁB’@¼ Tì~!$ç `@€�xx(TµÔŸà!·3b/ˆÞ%  žyòg_#þ¿;Ãq�ãgŸ< œ½#þ¿ðÞær A €Lh ó£-¿À5¹” Ÿƒ¦ñ âß·‰¾ ÂqQA 44ºàMèocø«Ho÷ÂäHžB9 À áà€.΀ë/ðŒî˜|¶ëXµ{]3,¿œ�¶ ¾8_ÚoãÂF˜Ì-Šœ�ÁÀm&~¾à%jfAWë/ß̾ —Çz„AÀh$yßÝt©äÿXÝõð%oýþ"Dñ‡àÁˆW¯‡ø F£üš›7¯�]ß\o_»¾±òr6>~Þ¬ÃÿŽzÿ!�×/êPA¯§À;AšË= PCÀ4�‡\ižT³6¢þ?ÿø!ñŽ| ¸V@£ „CHFWÏNûL_¾G°¥;÷ûàYw:o“@q�Õíß—Á°`NPøÀ"摖Ĩª%7¤f5íx"µR?AqŽûð�ð&j'Ê1-{¹þ‰„yžŒ½I�ûœÃ^LVXýàgkÑ~°,5äÐì{ö€ëÙz/Ö^?”~âËÁ=åðødH)±£ÃÁ^ ˜'O¶•þÞ¹¦ºŒJ�ÞÜÃâÙ|ÿ™‰3L8À!ö€k�Wûï SO)>¬ò̆eì¹ÒÿýÌ,S'”ŸVà¸" Â-0³`üW°Vµýþ�lT¨ìŸé~ >/ž¿¾ÿ�´î]¹^ �Ñ|âBažPðÞ•ú‹6ß¡é™dû!Kþ}Ù?€2@µ%I>"Û ªîçÿÿÿ” §GO¯ÿÔ Cß:Ú�Žç>M€Kº;Ú÷‚€€ƒ!„ vh´^ô—ß¶ðg‹”7ÊP²úÃ/ºþþT?.9­}ûý Ýk|3àÇ·t‘Ÿ¯àÍÖ›á“ϧéd†�DE¥Ê¤‰Ã'löœ£!wûÿà2\ìÏŽ /ÁO*²I?Ÿðž”È‘Ÿ—Â!p ĉPøGõ_�&cÿšŠn€~ìèú}±¥óSãò]5Ž-÷Ó�ËÌ"(vZØIë8>ë\¶Ë@a·ŸTOçæµ�1â|.€J` žê@!àÀ„œÂL5¹w`«Oÿ~„ÖŸ£ÿ_…ÈÙ0@„A8e øL@€ÈWH%Ãk9Öã8C†]œkcs~#Y�¬�@ ' …á2ÝÖÊ€ë/¥¿k~eº[ö·‚€¹ \X�¨DHd@€FXÐúðË¥à*UL?·á7’)Ö{Ðá1|’ qÃÞÿï_¼Tð($$„B˜¸0EàuÚÿUÞ#!8 È°ˆ�Œb‚#€Ì%Ò¸O}Ÿxá_‹ÒÎäN_Ÿp›àœâùůÿKÃß…È’ðÀd B!€ˆ`2 N:8æõÂoz÷ûÿPäÝ-Ê·üäs–åyÿ¨@AšïÒr‡1º6Ñ!{ÿõïwwï¿þ{¨ý¿x/ë¯÷`¡a0Þpa9ŽAbµ/ßš[¨l¯Â)ëÿ䢫kµBàÃkŠÔŒSÛž'ïϦÇ…½Å™?Ûòøàއ�6>M¶‰ž¿ÿø l}ÈÔ¤�?QrçàÔ°ŽÕð Ù œIšÿÜçàDi_aûh¯àò#D@¾ÃûSïÞ�ž ÌŽdgóÐpÀb…PÑ �;É÷ï[»þ€V Í+›>D>Û&.‹7Ž| €@@Ï€¼Xª)™M ûôdÖNiµà >ØêE%ócŸ~� #Yɵ3üæ~Ã¥Nä-8iMJø¯ƒà”È]rYç£þÂ!¡uÝÊC?ý¬÷ ƒœp@l)¯ãƒ@·pÀ¹ûõÀØî)À™}óðNdâ“O<ãˆ8ÂÊŸðkžÛöÐýp@:Kl‹ÒÌ ö~ÆJ£†‹„ˆ½ÞùÞfeÌšëÿð€å.\�a ‹#ÞäÓ~ð‚lªÏx¡Ò\øQ•uçÀ\K¹‡Å˜’H@L›ð…Ç Ã-=àNAÒ\øp‹‘(ûÀH†Çø&¹DE¶i¿ƒ_™¿¿äÍ=ùø'0ÎzgÐN8Ú&]â¬Tt÷ð�ŸúV÷�äÂöÆÑ,løštÖÓž ëe&™àÃW Ÿ¸ÿÓf$#ÉF´²¼à³Öýþ;…Tq‰Qo×@²pÖ·RE·ìE÷€W\(ËDØ÷ë©ÙóðLgÏÿ@³r6]Ýwÿèk/wwÿÿÁ‹ÝÝÿÿógó%;cÄkÁ+Jæ… æúðS˜º<Þ2WwùEþx 0‘åÑ Ðíÿ|… *m°àŠ×ÒQ§‰‡ÿ¯÷³©ËÔT­>@ 0 @gÐCȹ#�1·ï¨;cm´UøBg&�u¯ )³`,õÞP;9öú:üœi5]Tñ0 DúAŸ E8!àI†Â< §UK1!˜@‡C ô±$„Ø�ëÖd~÷÷Pø‡éô€%×φ~$„çšÄƒNDÄÆ}öÿÛú™+/©!íö¦èo¹ÑÔ1>š‡ÿCð�ëƒ)ìþèÓñ‡y¯‡b£û5ðN†‚)†‡ð.gGi¾b𠀬�µ¿×ðF¡îËþ¯õø\Œ„ãG�#'àä‚`_qµ?›xzï|o×-:¼üß¹<£�>{%þ L4ÉÁ÷þΞÙý¿5óüφœ»—ç_¿|¯­ŽG;àœ‘@¤ 0Ð󟘎qx´ògü_À�]äÿwár2„¦8xFÁñ€G#}3ÿ{~€Pm‘ÎYLÿ¾ûK ÆÉÿH×Mß¼+}ÜÇÿà=Ío‘¢ßßÀ3¤±Ypãòp¨Dç*ø {0÷ð ®Ì=à�x[L * þàyŸ�¾�Ù�‡ê÷‚×óºD"µébû˜ð‰ølTÐ@{ѹ™Hú*^EðÂø"gà  €J yŸ.@!àÀœ&„pÐÀ)s»¯¿ø£UáÁŸÚü wŸñÿ ¹2 D €³ð“ú°×sF³üKNüBõ€ ›­ÿy„B­sþ–¼Ö½uóÀÚl)miSíà ! ´j›ýzáxy[³ÿϯÿï ¹P!ÁEÃh÷°ï§¼Wó¯ð5Ø‘®M,tø i,Ö ì—   À6 Õê¹ïÿ²þ�¦”jÈÄ~ïE%8›£³\½€–ùËл‘ç„0ÚmÕ&Þãû*½±å¯Á£Î0éü§ÏF=¼#�ø!ƒˆ]&ï&›Ç…å©Ì8>?À Æ_®½Ÿƒ‚q㱿‡æ^„ �†Ý%3ûðÔ7CpáŽWðBìþ»ëár4ð#pˆ"!¦ Ø[o/#m¿ðBæÀDg-î÷y�w¸þ¸ïæ¸ H­“߀*ïR_÷ßžôðן¤�ú{àˆ´A›34¤Àx“”Î.o´©=¼,x! ˆ¦�@11"ü¥'úë20ÙP®à2cÁéÇÄôK?Þê™ZÝÈ~"ìQzÍùQzä‚$£zÊ–5o/p N?S¤Y=mÜø|OÛ4V‡ÚWàÞöb7l¾{Á?æSLô A€ K­s®ñžL�K¼‚]È[¹[ØznÛ¯uøLù+¤/ž!x@L1…‚Q´½ ®,àüYç¶Ü1e„üa’~Ør_ûüY£Ûj4Ðáÿ§bàœÏ§ú†Ã€�G$‘wižöL.¢^/9güð@@)„J.�I-èãÁþü†¦ž«Œ‹g¡ÌÓèžÁl.L ¶¦©ûïX9Ä¿¨BÏnmk—�Â`q ÀqÏ|NoS@…‰¿÷– ¬8#Þ¢QF�¿÷Ï<™IéôCHá & 4¿Uôv%Gˆ¹Ï˜Ï­[ÿ‚1šÌv|îoëy™»�¿üt;vJΰÀ ¤0i:®Ù$œ€l+Ó“šûdÞõ Þh¿`3ªà6 u–)ü×£‘¦¿(›÷Žñ#ß~�‰‚s>‘ÿ@³à+Äq.&zßÿWO¾aÙÂR²°t"é¨?�.¤Qc.ºÃóŸD§±ðà€¬¨öG|Ï L”(Ç—N×ÍöQ ÌŠ~_F6ø €€@@ 0€ÌzAùEóÜx$óœEÐX5÷ïø~@¨a¼Iä—)À €�1÷rø¢!Ä ÓFìÉâÎÙÖžŸð‡Wœ³ÿÿpÆu–ý¿Ê‡•ãÿ.ˆbÝØ|à¢Ö~xî>£>ÓEÙùã×”§g€@`¢AŸQE(!àj2Y‚8D:H8phP*h=ŽB:3ãÜÌÃÀ9òY ûE‹K¹ºïàÖÅnºýöÿÇÁèðR ÃhZ+ bB!$ÿ_1˜â3÷Ûà®Uð·‰íécÛ›þt…·¥Ï~orç¸D�t uX �€kmŠ¦Ô˜ÚŒ¤zgý½œÁö¼ûo·à(µê‘ç û^}…|¼ïÁÌ×jííyBs]¿ÿ¿ë‘Õýx\Œ„à Ð G„AV L3€•ÓÎç{çøgú·˜GÁ“|�.ãwOƒýyÊÓ»¾7©ˆ¯9Lmç»àœ#„A)AdEæ~/ìØ/³Ü£áî�ÀÐ À �6€±Ç¯%ßžü›†§²úwß‚Xµç¿q˜ªóßnÿ¯…ÈFO @ƒ @ð``€�q�(&?Fèž`®�““^ÑÐÀD5æ¯1ž×ºà:zwȵ®/×>$ðN¾! „€dÞ½Zýþïú€@b¤íÿÀÉ%HŸÿÿ€À;ŸptÌ™Œ �KË?=kÕ×§ä†A8`œÑ�‚­Ø!%¿!çÐí¬Cy%¨‰×˜�sÏ B—’ÖG½x( ူBàQešò~8¤/Œ¾òÑ`~zξL2$pl\óS/þ]¤ë¿ß¿€i:ï÷ïëÂäH˜�C<"�3„ÀÀ"³l;ôá," ÷Ý1ž¹Ì=ùŒõÈöMß·°5~ Â0‡ ¬4 ìß_‚_�Êy!�$‚|(%UËÎß²‹‰ï|ÞÕ«õ¿8` >?§£îÒ/ÜûÎÑr0ñ‡±¥çÐPH€R B!¡fæ½Ã%€C�3ê—3Ë‘ï_Âì»\�·dä‡à„pmw>¯&ŸxÏëÇðÆ?½¿Á@[ ƒ@H R C‚pÜ\ü»òWx Éì_’µ=ƒLZ;´¾ ÆD Âü£æ .ÖNïŸý߀ɺÜß¿ à€$€€qørù?€bº7©‚�Þ „H]©yþA•šðë`²ËóÛk€-N|¾ÕŒæêl'€ÒÝòëÛÿÛÔ¯…ÈÙ1!ˆ| p$� ÁVRlðêA],Z@pÖÄñ‘v?Ps§£w@pÖÄñ„_ºàÇ:z5íðNDü<!Ó0RjÇVÓM/ÿéèÏ|rA›t4¤Àx“°ã‚Oü×øko  O  ™žÒÿ€ðÞi¶¸§¯ÐE¢kŽ´öúÜÉ�â,pÀŸX�þ2¶Í¬€àÏê»aú×\0 ¿eZÌÍ{ëÀÿ˜Ùe>;O£«o!"y¥±ð=€l‰«´Oþ×€èõœË–MÐ3ˆÌœÀD´ìZZŸýlJöcDÎ|~´4éßÿß @@  €Þ �x±Çýx:6œôsmk{AÐM ïp#Ëÿ׃£&â€I(¾æµ è(2:S攎5;úø@Ššî>\³ï¼@KÑ&ßžþ½ïp’q7jԯϠG ÓAzªl•~  x³G¶ÒX0è* ¥äÀ®,ýØ=ô€cÏÝ`6Ÿ‘¤ÿG³×ÿÿpNgÒèx³M2ø�þßX ‹´ì·C€ì�‘Ù7C—Àðàm[ !1C8®S×ü÷‚¶m Ó¿7¾ðPbªù2¦˜_÷Öºï?¬PK/¥|W¥PF�k_€p+#/c^ùì�voM_à�OfgZA¾ýà ¾@â$. @Ò5˜™éùojêüSno0ð@D·˜mŸ†·¼É}Ñ�r cœf?ôÅ�vD/N÷™2MÓÀjs^|D¨‹M;<¬ °ÐžÛB®þÞ Z³D`‹XU|a ‡©WíƒÔ¹î`Sá°Ù~ú#0˜Ç¼F.= ¤í´Ëlm¸aÂ"f§]Üûxà§ød0ˆcƒ²½:wÏ�oðEd8 À P3A›˜=!àNÌ6¹X(X�^`F@�[ õ©uõà«e� -­7–4�B¸T5Ò²Étg­ö^§cx]¿Â�0±¯©šzòúèËšª{t(=ªc];<™ð§Ò9‚1’@€:kTGïÐ ‚[ࡘ­ÄÓkªdç>žÞwA¥0Ø;V0j>ô¸ì·‚º¬00-$EŸ½.;-Ç+…ƒà³õë]·"R‘ŽX÷ƒG䂽ñž~óÏÞ8¥5zÿ繌ö’×õÿ ¨ Ð( ~%µÃ8¶‹'ûïD’Õ¦ZJ‡�sepÄ—˜_óIxq€'¼EÊe¢šx†"h,ŒîÀ[CzÉ.¼I HÄw`4†ô‰o^  ˜D¡ö�ɤ2_wÕl•ŒJfA}Èþ à®þýè<Á¡I\ÏŸ‰ñ/R£Û©ó/5„6¾÷¿PÜ×»_ö’×Þ÷ÎeEpN€ˆG� .”H¢ûÇ7½àzù�l?ýÿà‰>Œ€1š§Þ®ç€½f@Í 3TDòm3÷¸¶>µ¶÷© )w÷žÖs{΃Dµ­öÉØ˜'X�ø€G…Æ›³õ’Ö ãÃvèÀÈÉ-¡�¬Cé‰R[(46‰-ú òCnž†L„–u á §AËôÝ3¨lÊ1�ÍWAËôÚ™ÈmŸÿÿøkl²Õûúþ#ß½ÿæ.⦅?ø8#@.ÿÿÿ†Ü‡AŸ¶E4H!àj2Y‚¸ ÀP@$hD.@”·ºÏól§"Y“Q¤éË€zi7æ`F’ïÊ{%ðN¡Aæ½Û­ð® €)¨�P$¡a†TMØÞÉ Ê–Ç¿ßø€â±:žò…v–¹à ÉíÏy–—tÁ@\ˆ„øÑ€ t<ˆ…Áƒà,ÆËùtF¤íþþ "!qeë?À'=µ@-Ö"ízçøÄsËó€»#&'€(bFa�F@ —Ñ=Þùîšx}'ˆ4+Ó�~V Í%ß�]ä=¯|œ)Éwä×z]x¾ ÅðˆXƒÂa#’ó¿á\2Ps ‚аƙ_›üàS_RÌד†´@qXÇ{Ö¼àwD]ù€(1ÏwªìL$è‹¿<¯„pL€"Ä‹¸×ûis4V=ø€X»šd;ÏÜRê`Þs#ÔNO–»ÿ*1Hhý ½ !d‚S_©šÇ `ÿßÍÀ 7.Ù®îh¿gÎ¥ðN¼ÜøŸÕtÌ âC!À�ˆD>$·tçy¿Ó‚aäüµ’Ë€ ¬ŽÕ¯?<¢;ò±úŽ$@àB¤óßð;I缫>1€Lμü—γ? ’GÄ H $ ‚\â•â9ƒ·7×#ø Þ›ó\2À‚„ Ó†$Æ)(Rþ!KAƒå7—î"ö¼å�2›€PH!¶H@3`#Ÿ³=\€‰ˆ' †Žah-2XÀó—µFÖ˜3%ŒN^Õó�Ü "‚è|rmðóvGÓð8e,ç¾gªüÜ2>Ár½ø ÒŸ×@!àÀ¼’aPQ�¡ÅÀë†N³šÀPáœLâíƒÜÛMÀ~u¾[—¡^‚QLƒØÁ÷ HˆÀtOa…vÜæ �„"ìH‚_iuÿ‡]ˆ¬¿>¸ 순HøB! ˆXHóË ÇÆÄ=C$|lAòMÀ( É&a€¨! ‚a¥ó†sŸ�µ˜Dí©ø‚…�Óq˜'\4Â^—oèŸxWA¸ÆÂÑd‡WŠÁÆœ­?kÐi99À"¾Ô{øW¦£Þ 8•ÿŠß€Û€ŒA›Û4¤Àx“‰‚ƒ<ÿaÁ@%3K>0UK|#†sa�@ÕÛÏNñ#Û¿ÀîäóÀD ÁTz W®ÿ�߀ۀ‘AŸùE(!àI†ÁÍ×ÁR̹'bCt €avªÎìñáUbnÀs�lr+Þ”!f †¹»"´0âó=Oiª€*aVYèp‘ÞjÀ( â`@DÁP\m=ôÏû�Üø5î`œ[�8œTw:�Ôîx]²8 (ñ˜/xX@0ÂIoMØ÷�@ž»Ç¼ØÖŸ÷”’m®è+0:dFÚîuðN¾ÄÖãœ,€”�šÖ義ib^°õŠb�¦5`šÎ!!ôMLhÒfíDÔÆƒ2f ׸ƒ! )ÎqœLß÷ž˜ÉŽÆ:ä<0Íñã2šn1ì`8™1P%oìFLÌ•ÛáßËá €Ý€Öeˆ„ûÿþ¢€‚—Ž QÀKNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®½ÿÃw‡xoÀ° e%2¤Í8û ‘ì‚î`#þ 9žÎ6Ì FüjG¹Q¤  -€>DÃ[ˆŠ¸Q�‹Q2Ppw2ÀŸ¹Vâ"¾5>U˜ wß×øý`G ‚TKÖ¥}›P`pÑ­ïWŠT•´7 jwÌx¦¼O¾ £ 2ZnuƒÞ–À x Áì$&Á}·Ø4 r”DfzÁƒDØ?·àÄâR{W«ÿ ì*'Åž.t�\��ùÿ¾"üœ�Ï�€°`®Y.Z)üH£�\²\³TA/`~ø HúÃÃ@ ¨²Ñä~ñ€­°±Ž¹þâ_#òצcåAë°XG²%ïïà —l¨Œ(¤ððÕx)(ê+�;b�?!@nÐẨ9j "«a%×]u×]u×]u×]u×]u×]u×]u×]u׿ÿ'‚ì9ÊÃ3JÔ¯¿„dx5S…‹ýÁ…Nýb6%#x;ø 4Ð@x÷ ûË€B+{¸, ›Í rA6}×ÕÝÜ¥È7ÀÐøÎ23@i�ÓGÿG°X¡V^bb-Ê{û€~Šøë‰(�­L,ƪ¾ï4 i�àôiI ÷÷’$¶r�”îpÃ@þ@€'ÀG¡Š™ç …Ð*d\@¾·ÚJÆ¡Ó>¬[xH�£ÐDã®ÐBŒm9P‘± *·ÚJa¸:m!ëÅ÷„€€™Ü¥ÿá­›Ìe^ÚGþ®Æú›_ƒ < @p`   ]†p÷6yg–ÿÀÅ7gEªÊ`0çü_W¿ø5¬ (%*em4w×¼)°§(!×ç|tݳaŽR¼×uàÃÆ{ê¿yb˜BH@ˆ!ÞÚ&$ Ë;M¥ž) ¨?=ñØ-Ï;FÑg™CÕsÉ‹®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºëßÿ‘Avnr$b×�»‚GB°N>Í”3ýŠ9!ßi"h�¶×ÿà@ á¥ÀA§8xV¨.�UðáR?¿pðV¸Ê[-ûõ"Ȥ_ÆàŒ?�•k|ódR €î R”Í6‹Ñ"UŽ”«üÔI†E+¾šÖÿÀÃé%U3ÿÐaÂ@@X0 xKÃψRì@"ï¿sÛ7™ìÛy÷ÿÿÿø`à�AgÆ1w$%ïÜöÍæm›o>ÿÿÿÿ u×^öÿì6" –"à(§Þ„§D8L3•¾½ ¤}á§Ëô )‡…X¥•¼0H@%Ó1¿ÆuàÄTêÀg¼nAO³ÀÆ +ÅÞ3Û=15×]u×]u×]u×]u×]u×]u×]u×\-ÿÿ€Ó>%mÒò–áÙj«6Eco�n ˆ2ü!i©q 2vÎ<ÜQ�D�{ÿpju¶›éQJàÀà° gmÞ—_Åûý¬f ¸®KÏùà à À@] h(òñäÀê¿<ÆëÊzâúDtA&âdI8“dH0È{ÂÉÀË»íy†Uç/JÇ?í@F {WcÌÄË`„˜]u×^°7Ëö µ›!EùÏó}˜ì«ÁæÁ…‡Jµ×�À.eŒ êhÒþ (ðÛ¼ëÏ#“›(ƒ N•¬ÒéÙ(0Œ&}í 05¡z÷ƒ§dàÀ;DûÞ™k®ºë®ºë®ºë®ºë®ºë®ºë®ºë®½ö·±±0ç� �ùza’ÿg–+'\u|´¾ 3�O´G/×öX#²….I–†ø �€°(€äX‡\^W„4èTèM~!@)g€4ÿèĵyžôI‚!@äM~1@x°fÿÑËÿðW¿ãxp€™� Œ`ÿ<}‡]u׿ÿ„9àbšd¤RZ+Ht,¶m»°Å¢gË{x Y YX@gô‚®x†–1Ù !m”«oµ¾Ãp,kaôX-ï×”Å@{­í÷ó±»Ë˜…„DŸ ŽÄ z{Ù@=P ÏxHRC¹A~¯s Àj€ï¹]u×]u×]u×]u×]u×]u×]u×]u׸4¾\ƒp=÷^ȇ> (û«/ýí1yWFvé…¸€º…©Ø$xGj‡Ð/DÏׂ P/¡PäUW†CGõÙûÊç®>Á�° ÌYçÞ0Ð.å%ç¦]Ÿ¼î-¤)ö }€g4Y÷ˆ â뮺÷ÿàÌ9�peƒÒ/˜ßôÁ[, š0-߯��)|sK,0ÓïBÝöúÿ+Ö�f¤Z¡0À£ap`0Vd�1T*óÑNë@ ÏC#$�Þƒ`¤¢…Ú¼ =û.ÁTJ‰("±®að\×]u×]u×]u×]u×]u×]u×]u×]uïðþÃx'È Å0KíïÌ™&–7˜Àça[ªú`B` �„6µyî’€ -<¤ }{sÝ À<žyk®ºëßk°áÒ”ãˆöÛSW�À ö3CI_æð”:0SôïÈ}{Á( Á„—Üá.x@ŒT &^ ÀJé½zô@RŸzð‘7¯8;õ€@ÊPóï)u×]u×]u×]u×]u×]u×]u×]u×]{ü?°Þª#dQèÝ®‡Í¢¶ ö¶ˆa½èH F¿Aï¯0ï!¥€û1]³Þ <|%žð¼WK=⨜%žôõ×]{öÃkqÚ@/›E3Yzsb–}ï½öûl> ®Æ8ŽmÕ¿z ‡e`ßË“‡=F¤•Ì "ÿ.¿Øe�0ÀÖô …~4ÿß@Z@êæçš�ÿ³9§XÿŸ°„€@xB` ƒÐw ’ Þùƒ–(e”.Š3<f@ ×EïÐ8±Ášpú0ì¿ÒŸ`¶ €ãØX‡Š­ßÞ67ða<Ž&¥š„‡ða>ÅÖïRÃ]uÂßÿø{æòÎ܃/ÿa³>Æ憼ûÀ˜L˜ ^,±�—¼%ƒxlúç߯È)l­­ã}ÞF "L@*Œäç¼h@@28?žñ.¬BsÞd)Ûžõ=u×]u×]u×]u×]u×]u×]u×]u×]®J§ÏIœ| 1Ý3þ±íÔÿƒˆ85LÚÇW3Ši1¶A* ÉUÿ[ŸDÅÊ‹X�0¯žè Ęô»cÿÀ%�•½””Ç?à@x xP@ ‹5@T6p™€\ïÞfÉÆMNÆ CUY Šœ{Xêƒë ÄðàFfM ¾tOcpJ­,¨“÷²zëßHصݲ…(çŽKåÚÕèòÄ¢À³MïA6` az"™G÷à€ÃB!>Qâ!º5O¡Â÷BÞ€LüHî�ïz˜ 07ü>ôÖû`¯÷kÓÁæ¤òC5Rfp¨0w1Á{ ϼ€.�¹Àa&Тùäp ¹®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®½âúŸ‚®¬îÒ–ûgÂ�‚!H›*G°N’U|0€$t W¸fs?^s8øfÅÙÕù¼ ú„•ªÕûoe‘³óxpo©¡UÌÔ~Ï€,Ë#dëVÃÐa>.vh~]s¿$EžóKB°•(  >CÀ¦RœPWKëÀµ‰Ø+µBx’`ú‹»|ak°3˜˜æã�8w¯Ó!X9Á¾Yp @oÈݰranO^ÿû0á! I[÷/Ð0¦´1åo^ÏDÝï¿ÍöaÙOÁÕ`ÁF!Òojñâ˜#„‡Ç ÛÄ%zeƒ «4}Ù B±Â£ÐW«Ñ– @¨aË×�/’Fºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºëß_ýÏõF%1cˆ”£ÐÛàFm9­:áø›'hÒpå5]CÕxïË R­è�êAù{ü0�!yùË{ô€”ó�RJ³›ßMþ'îG�À‘cŸóÏÿ„` „`úÀª¦ÍÙü8»7L?N�¶4ð)�fÓ‡¸¢M0 ª`?e8}€‚ÐÌ@¨¿1®îÂ`^œ’É—ú�uXvÊg‰:†~wþ§Ü4ï` jaLÙYËÿ8¬E\ÉD„¢KÿrYÀR,‚b¶ Ü \ß…‘ÀM»À†VøÊ#Ú y5ðpX,Î>ï‡VøÒ@ 2(ȶëëåëÚ² MCF¸ûÁ†6 Þ\ûçãVYÀ,-Œ‹ÏPjÿßµz€*®Þ"Œ+ŒÀ‚ÚõÁ8‚0�‘ÀO5NÉÁ™È}ïEó}ðæÀo<§®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë¥…×ÿ w^Ïþ¿×þûÎô‚@@¥LIÃH ©¶ 6ò@Ï*¯øûÿÝúÿ„1B‚2Ú`­Ï¡ªªQþ+ÒE9»¼Ìj©(ûýu×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]xÛAš!lÕàÄHa58 Âô™¤ŸAÙ˜™Õç«i3þ°<*éçPcãº?ʧ £Q¹fÕ¾ UûÓ­ýâ ¿ø¸+"÷?Uø#D¼ãõ^ Ð@ 8ß4`%pŽƒªÐ?©)Ç#á‰�¸ µ6½ÀÄzµ2, :ØÜ#àZ÷Á…*’c·¾¾- %}Õñ¥QÞ%(Ǥ—‚òUù‡Þ7l[^«ç }Ÿ Ëø�ôáv.ÙJ·Ôàž;b¸F?ûÀ÷šy^ß¶à,?a‚&4’�øo¾Ü)/Ž»=ú8¿Iµž�áK*âøˆ#€ «l–z&PÁ:ÁH@„ €�ʧZÏM°’=ðM1,z:ˆÂî±k£,v%k t“4×kº`MØxAÍ?¢ÝÑ®×Z`Nx�A0@…†ƒ‚�‚"uoÛà"ô¦ç&ÔObL#vP�Z`ÃÙõÈ·Õê€W­Ég¼:|m’óÞÀ ~¶màc +mg¼ È&„âÆ©ŠšÎð­ÌÁg½µ B¤p»ôϯ® M@²'†É4³æ„E†ÈIq•Wh@Nƒ(P36wW¼÷³p†r‡?*&ßø£å/<8'/âØ�¡q |•†î Mç€6BWHðaAp'j‰½w…™fŠÏ}Ø ¬e»Zäë¥.”>±—¢ZäÎïJ÷ÐEÑÒ“Âì QÑÈ:#ÉǸvm6MÛEUøóÖöL“•ÔB/<)B”ý€6�¢¼÷‘?:l‡ò^{Á8n'ÿü#ÆÍ凹@„Ð�a`‚ŠƒÜö{3Àáýœ;¯<è–%еž¸Ü5—žñ|ðû3R+=óT’î1¾›YpN )’ ‹`‡ˆ)ëß swDdz?ô*ðL7ÛD/!Á!TfòÀ†Ó¯CÀËßíí¢[^z þÞšc[ÿðVTûƒíðá€8‡—À>ÑJ¯{øp¦•)~ÿáÁ9&P€Ð@Vb"Èm…ûý�_â#�NüZÎã¡ ˆÕ½k>m‘ì–zc¾MÖ (“|ů“u€²J(¼O‚t €3‚¡ÂpÙFiÏJ”¨+!@Æ&ê+ï<jé[±ýúÏIzI™5ÞÀp›zV{À’f^xn�U÷k=àŸ‡—À ã”#%$±ÀDZÍZ·ZVjÏP è<�d/¶2)…>¨09…·f ³*ïß±užy·úüg±Õô÷¨T}UG®)Çð=ÿ2T‘{`®ç< —Ào@[AšElð"„@’ ©VT^>ðR07ÑADÍਓ©qXù�¯@ 7­ŽroÀb0L^™/q‡4ëe?c?ªï¼rsd{¦§u]üÁ8�3Õô > Pqò(ûϯ�PVNdôÀ‡FX–võ!ÌU:¯×Ž�¡E(d²AXOøî & *ÛÜÿ^a¦æ›>°W¸–uÌ:óþ• hz'ò #÷€À¸iP¹�:@ 4(A@X¨ úü£‚>oà +^œiŠ `ÛßËHÖG2mb @úNP‚ ê-üÀ­/8Iª¸&“ùid8©þ™þ «�‡~Ö„.9wÕ2€ÐÞ�AÝáÈÖh¬éOýù¡²>·,´0€@\   ðà GHg¸e=:iÁõrW9]1 Å{M“‘¦�)üÿ€N@�`›äG¯ÿïXŽ?ÄÐâ$0V” ôµ‰‡ñ“¢¾xp¬%:á:ûSI™á)r $8\`R��[…‚g¡Gß´¤v�à à�j÷cUUP0$Q<\ÂZä|ø4¡¬Û3öoÏÿÿÀá€ÀÀ €›oà„h¼òïúÑ$a©…v4{ Ã¥ÍD²’øm ¬mŸã÷ìûÙBH@”㻟{ Ã¥ÍF ³øÝ0< O-äE‘|ƒMÀ#1âê…¯€? )=äØ~•6î[Öüò@�’k¹Nà oçÿ÷ÀícZA&zIüàkP5Ó*‹/@a €˜@P«øßö ˜œ±€�Rà6AUAÏÈàãŸ~ˆý_ÃØ&`ð€$�\ÈT .�±ÁO¿ `|Ï‘d_„@¡sĉ Óä ¬(°‚~ý6ÿþ?ÿüÒ C…žˆb‹ ŒhmÖžŒâv‰ $Š¥�ËýZÛU·ÒÀp€¸€"`@€¹ ±ÂÕ´ ×߀¹ SØà{I÷`¥%$’€œLd¼²C„¾üdž‹˜Hù�,û”’Q‰%öŸqûì8%?™û¼Št–ß+ïŒÀãW¼k¡Î œò­€‰¹qðñ*�Ã1b]í£ªÛó÷ê‡ÿ ÂÐ@ „   N7ÜAcƒù‡ŒÜ"gЃ ›ˆ,`'wP´Ï¡ç|3h#!Gõ(Á±ø†Â ³ u0‹ÿø+€xÁÈ¿EžöÀ‡ÀŸ@@Žì “G0åP}PDD®p®( pLgÿȰá8WfP¦¤�œUt“2w~ŒŠºÌD®ÖãÄ omŸoH ÝG›ÿÂPÀp¨@ :ð¬›rSm.�?)À�o¿@²fâ)›K�fâá"í/}úaþ(84&$À£y‰%â÷<Æö¹UWßÿÿA!4”´£¥»ßàÁø@ B  À@<�î“ Ákö˜�ƒ‡Á‡Ò`Ü ‰Úp:COƒ ŒÿüŠ ‰ 66—éÀg‰®è;˜‚R[t’Šûë*a×~ûøx@lß ó :€òy÷ ñ %ÒÃfhQôÉ)~7? 4éã ð\A8Xÿ¥ ´Èýù¾¤€ÿØÕRª¯3öm¿ÿÿ† Ð Ú"?% �o¿تJ•A€ž&3iü@ƒïÁò’JJI$& ÊÓŽ:ëm´Ó�¶×ÿÇ:p(p 8Uü-˜¥ 3„B€†ÿG (`ýê à�€Ð6€€Ñ�‚�¿)ùµDJ�‚t¬ÃnáÁÄ?c°°z0Ñ (N}OͪhcB ÒÓM�Á �Aùº&ZôóÁ(Oÿÿémõþg‘ˆ¾A¢8'BÛŠO¸v™m@v@­‡ÈEö½·íÿ_ýô©ì}^õ¿ŒxšÛE—9ÿ¼�HÒK±J¸BRÔ£­í�Â`A0€àŠ€Ón"™´tc甀j²þ˜ÜèqêœCïÂÀX É›ˆ¦mŒ|X”‘ôÀ¦âãIª 8§ß…€,FĆˆÎ~ Lö"Ȳá7õ\2©äèxª�‡bD»õ›«~@»Â‹!'ïÀŽ�R,ôCmNÌ}V÷{ò¯ßý?þó€€°€ˆ üv ¦å…ýtòÎ\ ²8ÅÅ‚¼v âiRŸ®€Þ@,SåÈaË£¨ÿÛà8¡ÆÎBKÊ2`fËM¿7‚a�¡"¤?1™Zxd8ƆÝièÎîI7ÉûŒ4©Vº,k�»Á¨›ëÙaûÏ|½OÌd�Îø0áÀ@   X^dFXÀß‚v8Ñ!@ûƒ>Úpdì?kJÜ�DÄ Ò뀱²;R€ÉÁ¿€60á"C€ûþnG7 t{Ër@�Œ ‰*é’ÿþ ÄèL%eRRØöÿô€t äˆDD1�ƒr”~H‚# Œå†00àœþ~ OÀoÀÙAžcE8!à-ŸïÅÀ@ðã,…¾T¤ä '<èƒ<‚â�üRSÀ%¥£iü”vJÉsk9€™ÂE pX`qG¢HéavP‰O´E/½™êJ¦Ûðtë³Ñ¿Õݦ17ÂXÜ!0Ê<ª9GÇš^Ÿž‘›ñ0AVr…ÂO?ßÏ|U;Þ¾¼¿ ȉÂP€\@À;è àbS»D—�Gï€à_ÅÊ«Y¿¬<Îð §²`±_ÏùzÍôARÙóýuRù.tÃì?°KT>Jmc¦^ËÖ/¬v •+ÏWׂQ�À¬8?B„ê‰×�À8WÜ»[½ø¬»Y‹^^!rIÎ ‚Ø"†BÂAY€‰Æd·…Ÿ÷Ø^I ÕTr�‘õÿ™€ˆœLDã÷ûàå‹1—¬—³Å„�O_òYïw)*³ÞA—òYàÙÊJZï‚p»"'84% ‚Œ@FÀ@A`o 63î^ÿš ÚRßíœOƿǸè:„ çƉßû`ÃÈðûeµ{þðcsíe¿IuÄH{Ú�4ßR]qO^ ^`žt£v@I à°Ð o.E0¨öþ€Hõ¯;ÛêIöÐQÏîÉ~¹I–¯÷Ý¢Bfdc5#”¨wÿA€âˆê+m ˜ïÁ™ªúåà¤+þsoß¾V­]àœ/#& 0Ad`�(ý¾¢¬Ÿ¯€ú¯ë�…jžÌ—O¥”}4l~ßdRDÛ S(ÊÓ]2„ �ò>“h/ÛZÇ ιÈúD‡l$µ¿"•Á Rʾ! ì�(0H9N È`Aqfˆ¦Ý|�ˆÂñ[žJ>üWëÏc`ÏT]ÚR™÷À‰ÜIdQú~€– ŒÝT'Ö˜Â0C”:7ù>�@ÿ2"O�q31ºªó¿À eLç&èL¾ÆÄë\Â?û©6Ûß^�l‹–Éè©lé—�Ú(BÙ3]1Kåo/Ëå÷ ¼%Ñ#= ïÝF.}œÑÅÐ×ÐŽà2vŽeµ À Èäi–Óô.Jàv @€dpV`Š€˜¯$ÿVâD9u¬½zSàØƒ½‘ ,Mç‡]âB%‰³Ã‚‡ÀoÀΞ‚tØ!ƒðžhp#5Ò¹1×ÖÚn‚_X”s¥¿…Ùü ¨ ð  #�@*á�5Ÿo¶:'x葉Œ=l_é­ŠqõpLù‚øMÒ…q Ø B!`PÁW¼zÀÂ\ù±ÃÜ<`~þ—ŸÀ3½.üñ)D—¿ü-´»óÀ=´DªóÞ Â쌞 @à$gàà>hž‘>+‹Óì¡+µ�o·üYœK¿ Ošì—™Ä·ùAhd$οóý¿‡[Õ\n3ø' à¤D‚�À€Xt+—‚ÞJ?À®r~^¸¤Mðð‰UyãA‹¬ðMØx<Ä¥yð-|ºÏãðRl $8À#«>ؼg·zçcOß®@E .$ X"˜ÇD�yÏÞ†õüAÇI¸ûƒŽ“qÐïÜ`œ+„-‚€1Ð]ˆ3ç/àU±ËV9v\=ñàzæãÇ�î�Ç¥8•Qßþ ÂäD∠„!ÀQ$ ‡\ªY!“l<�/O‚�«¶ÑUÜ ºoÏêí´b.�éwç‚q¾,ó �Ëà­^ezE!˜ÿ³ë‰W!n¨ö µ^zHD!” ‚D¬{§„É$ì2»Úø�—þ˜·Á8]á „ $À$ (ãg¼?Ì‚S`nwÆ[$4é7isW’ïÈÍ7sW’ïÈ3T×A:ð®X08€9�¤ Y€h”áî)©”°…ÉçÿB¤|¯;øP*GÊó¿àœ+�  PH ˆ0æŒÝƒq6=óŽ&0—¶³#sϼÉPs Ž™ÖÐ)S^ÛþR¥µç�ø&^à p` 4Ž@^<%èRpÀr‘…PHõ¬M`FmZéœÿ¦[Uâ¥yÀ ɨ†þa�HOEKÖ/&¢ùø(~~ºž„@!à7Ÿ…ÚK�ƒ<�2`€+Ë>†ZŸ®õ¼$ú°\?"³.;{àãÛ•¯ì…àäR{Æ_Ð'äQ?íëÁ0øG`€AÀ™|°g“T[Ï/ûñð_?8\Œ�@‚ˆ B‡ ;² ù¸µªýðâ/Ý6Píõdz÷ÁxsÏÌ„‡Ø—ó¤ò‡Ðåüx%~�„à„ ÀHø‚ "à½2à—9�؈�¿ ݱ]ÿ¦WÎ :ïèß»\UÍCó\¸1jKºKŸTü­-0 wÔü„!�)`†x©š#Ûècy*çðä%d›iÿT[ÃvNüÓB89 �< ”+æ®ok׊’’œµ¼qÕý`Öô¹?Îyrÿ߯Âä!ˆ à(€¸@°Bˆa…T:ðÞ�ŽéïÏóƒ¯z+g¬žø4#¼_˜ÛŸ÷Á§V €á~ćÏÌ ö$<…äDâxdÁp° ]�¸ ZüùÕŒ ŶDHÒ ð½C>ØTûÁ¥P 6æ»(?¤¹šâVãWcPÛ!Þé DBÃö‰ëuþ ˜` !Ç€‹Þgë„* “˜Rx&-‡¡r"xGˆ ª €‚ ‚ J4øU|ð‡¯ëp÷¦´¹­mc{غŸ�×ÿx?ÀeÍÕíÍ÷üòûK:xâ”ÝW“}žåW¦ /…ÑOÀà€ 0Áˆ  €.`@Áx}‰žâ³¼€ ’cûe½à|üþd?FrŸö`gö›Èl_Ø·ðgúG¦ ËùbüÁ2ù0,E‡€ ÈÏDŸ9ªhøŒ¾â§†æ§˺Âäk€¹Î f…„€ŽXåºàº3<¢CSþè6—~QáÿeŸX¬÷ü¥ÇÖ+ϼ8&_ ¢ü2D Áh @{p^�ÎG%%ì„KY-éÝÃp*é~ñÿ`]S ™ßǪ»w¿¬Óü­^Üí÷.iùY^Ü´Ÿip;à•|/hž€R Át…Þ1<�»o¾‚6‹+|o|/�z§fÐçý°kÀ •ÙY¼¸P¤-ö³yèNÀ¬ CA‘AyÖKѯëçˆ6–ý¿6Ûp4êq£»¼2 ¼!¥™NŠ@n, ýtpL_à"à"ÁÐàâ&„{6­‡U¾>ºÙsà™ïGƒá5„¤øøYaV!?ë®>\uÌoúêaJRãcÁøcw51¹b®~xá•ÍLo*¥ÿæcUJ¿þ{ØÕUUÿü0á€h ÐD Á°ð™X &Ömúè0àÐDr£q`�ë Ã‚bþPjÁ(Hhq€A«¼æÕ„–ñ``>Ø ‰‰ùï�¦?ޏOßî;̺ ÿtÉ¥/ñ°Mó÷Ücw51+Å\üðÃw51¬x«:ìjªªv5UU˜ÕUKÿÃø@ 8,> €Á°ð™X &Ömúè0àÐDr£q`�ë Ã‚bþ'A0€q€ægà¥F—÷àp5ˆDõØ&=Á¯øòƒ€@ù�Ìà%×§‡g>’€»å{È‚Ç1œk•h%A”çÀ¯>»Qyà°ìFo‡Ç¼5ûˆKl)™Å ^Øpá±Ä3¸Þƒ}RÊ_ÛPv€@ñàýÁ7Ïþò†ÀÇVVé©Ì¾à rÕ5º Žôkþ\lo¨a)|z$8_˜—õ×…žcþº˜DR”‹øØ5Ÿˆ ÜÔÆ°±UùÿCw71¬xª�n5URÿù¸ÕUWÿá‡Üjª«ÿ¸aÂРÐè@ * ƒ á3°M¬,ÛõÐaÀ ˆäFã×A‡ÅÿÀ� ÁÀ&ßOøë€3o©ÿ»ÆEàžvd¢£ýÀ¶¢kãCã¾ %XJ_8?—õׄ�±�]L")øØ#ÐÿÀ�ÕMLk wZîjbXñWt˜“û��UU@Àø@ " „`àh:>;DÚÂÍ¿]zŽ@n0 ýtpL_üÀ·ÔÿÝü›}OýÞ3°›>ÂÔ�Eæ� ;°!vð·ì€�>F?ÝË®\J_]ºaK‘ ÁþA»‰*TÂcv:Cu51,x«:<]ÊãGëÏý�UU@ÄàƒÃ¡€¨8 €Bg`(›`Y·ë Ã€AÈ�Æ®ƒ ‹ÿ‚p;§þïà¿êÿÿp"nï)‰È3hßÿ` Ð4SdLóÆ›°n8%ŽFç×..*pì„ýÔÂ"—ÿ�„xÿ 7Q%˜L]ÇZî$© �Úë1ªª_þ&3½aÀÿÀ‚!€˜8€Bg`(›`Y·ë Ã€A¤Eã×A‡æ,¿ü5ÊÏÂG]îШ’„L&?ë &)€Cƒª`œâïïþƪª±ãØp€¨ �ð€Èh,HR…kNÀ -ûð &ç{ÅP8³öó𠄆Aš§=!à-¼ÚÞ |ÌñÁJð$Èðõük`¤0 BÎV¹µ5#ðx[(U5|¾1%¯Š‰óA}]îOá¹ÿÀŸ`:²kê“ÿw˜Û”ãŽðý“/ºOÿ^-¤�6 °#òþ#Ä ‚ i¾"Œ DRþé7n…�SÿHrP‘]Þîhxz‰žå=è,�óŒéJÞQþ½ �îýÔê¼ÌDc|µ¿Ür¶[½kí£m¿ý@ãåª3_|,ål·-'öÑ�¶ß¼hd\ÇQæWÿ€ÆÂW´eߢ¤çýøpK‚‘!Ç€{(Õ¼ôÀ?’ÍÊŸÿÿ\^E›•7ÿÿRøPXx3.‚c{¼x%€½§RP-üÀñdS ê¦þzßÖæ$C7Ö~êòÞ󬌦0����ð CÚNûø“™ù"ÝðßPRË þ}!Æ¿¦nï€öÇ÷¶ÞÞßøaÁ/ ‡aL5#8?‰1®šµ>õ ÊÈj“÷¥àOUé™}¸¦ºÜª—%ðXA€%(‡‰‡·°S)ºlS/þA#bV§ËØÖó©]æÅD’ÉñÜPçR™>Á‡L=0Å¿3ýÓ{×’_¹óÓ”ÅÂÝjÿÚDwýQ3÷=7ÿ ÷¥ü}Ca`à°°¡ û/‚9$û€ÁôtRâêŠD¸°æ7Àu�ng¯XßXÖ¸’6ë%ñJhÌ:$üÔ‹ä0Ÿü£Œlj[¶~�‹ÿ¯#'Áùnx·ÿþ�ü=0ôÀ“Ç­WÄcU¾?1¿àΙ/ÏÛßÏûê_ïÅþ:D*´ëú8þ#&L‡€¶¸ׄBa„.¼VµDv/¹;ÄR‘ 7Ìøbg—,¿¾ó à s¤œ—1ª­$¿5ÐNøD‚p〺VÏI7Y”ÒáYð”x,ÐYTÏ÷À ™ Ijª¿åðf<6ÑC£~¿#À—gÉ3?÷´ P:íàд?màôzrþ¨"lDü]mµòI„f'Á•€¿%$iF_÷Žù-à…6Ë’:oÜ;rZ8 ž|¿%ˆá�¸_Kßå—¿O—Ó@iú¨i�Åçýµ�x«ß¾–1ßð3mëÛ"nW£þ$Ìý!uü@GW¶/Ä·ÝþP£=é ß%ñ�Dƒl*ÀàÖ•kÃ_ø™èÙmnÿhÏÿ:”Eéé|ƒA ƒÒ³òUYqñžÁo{•IîуWžüà—Ãð秨Ý/‹ÁÆÏ—ÆÁ8’L$„@ TÇÑ Ê•stÂú¶ÑºŸ+|ÑЈØ+½ÿêKÐ~»3ï¶€ R¦ix îâ4è_芌Éx_mdoÝ¥ûþ^ á��=q(?Åf7æÞöý¿÷ ¡ö[ºðl_ýðJðˆ@CŒ &VL ü¡�c΀²`#$<À_yGA¯88¬¬S›€ u²ì"»Òø( ”A„áЉ€ÀDø;¡žçŠþxÚ;ÙEëbž~w_z0j¤÷póïÕu¿ø(1&ãÑÅ­HzUÃçÒðæ\gOBZQêvÇyÿ‚bþ<0‚"Õï÷�ú¹ƒlýä_‘3Ü&/¼ì£dÛI¢¿|ïôy[Àþ-Cfƒ²ý@ºÉô*ªùõ9lÏÔ­ÌJ…)2R¯»üㄆG%·<™:·ÀФ ØÌN–²~ÝŠˆ¤F@gÁë-Ì–lpYÅ[>Dªð0èü¿Pã!É1öe+ÀáJ:yŸØjJûE§(ѯµxo-fºüà´4¨ê—Ä"„0èœb»lŒ—Ào¥\?z “L–Ñ?¹Ÿ§u[ LcÁ�¾’Ž3 Œý²ýón@þPh7ø(ÇíÙ­ÙeOA„x7þ ûrÝ�)sò §ž^€`8yøFïË Cÿ“€ ¦?ŒÎT3Ãvl,ý`9Øžš^x.0¿ŠI)%(1L`¢Æ¹Éœ}':Å#Þ°ÍŠ8ŒÑôœÜÅž�|Hèe€… û°Wz ¿À„VÐjñˆûò�¼²5sÁýxÅyì7sÈsÄÃåü„�p`*À&БJýÎé•cZÌï¾å ü¡¢sœ1ÿݼè~vÃHÛ�CÀŸwöƒ¡y)”ôòˆa.¸“õº˜?‚sðR~ OÁIø)?Ašé˜rø9a�Ba È]Eæ"=z~÷ø'ЦÅ48Fï謕gß ¼=ý”E·wÝ”Å7Œç7ÿ†$+èš¡‰@ä�ŽíûÈ€Ôn „LJ‡)?zfI}‚Ø3—¯Sßëߘ¥J¨x ¼îot§ þíÆlä§`—‚<—M“£_ÿy€¤8 �@†îàçY‘’>@ññ™…;ð¤U)k ïžÒ3Np?¦¬W¿öƒ¬}ºÏ€à TWåÿ¸ÐÝ_‘_>—ÇOð¨¡ÆÒ9.‚§+bHäñàn¥*¯a§ÿýSöýéþt©‡^Ä"ûÃú?ÁôÃ¥Œnïðþ�ðHyrÿ�¡!€Œ¨„ŽèüiØÙïž }iÀîLÌ·Á/ÖÈ‘) 4 Æ2nbψ´q‚KÅ걘팖½Kã¢pI…XÏJͤÀZ> &N¦ ƒ=GÚ´zÐ_¢I�ٟש•æÙ¹Àÿ�PMi¯…šz54ÿ=ò‰‰ @ çÔ÷îÐ4YTŸ¼•$U|ùx+ àF`à¤å‡S _øÑì?#æg~ ½º}¡þ›¡(¬/ðx?ùÜ%·�‡¯€è�ÂEÉd@‚WœBìê¸|·N(r=�M Aå >¾š…?¼a Õ€$Z+Üÿ˜ô““$fŠ÷+à±ˆŽ²_ † X¥ °ˆü•¾°äë™b’Ûrz¤cE“s#CD{€h¦Ka×Ýf*UUþøe�¥f‘ç‡G×çŸ]ùàVr<ðuüZ+ï+¿ëK „‚|éÖæþÃgùÍ•'þúÓû‚“ðR~ OÁIø)?'à¤üÀxA› ˜¯ ÄÌ�Õ>ÍÜüÌ>”O`¶ æbêNξxtwMa°ÄÒÛûï®p@L9E`ô[`U''L€Ð}ïÁÔ0Í ˆ DyÿàÄ8à‡n€7FQX•cû²ð‡ÁW 6™†”ú;ãcàŒh¤èùqÒ½‰T”â˜6¹º4ãâ—�ÒëAºã‚Ñílg'¥ð¨�ˆ°à¶ƒ¯Ûí¹°ð¬žгχ¬æ+›µà ‘Þ8c»ô4Ù�ÕWçø…¬$€+]ÊUÕëaÀÁ5þ´Š@ÙSÇ%zØp9ŠïwÿÐh Ú½š³)¤Ú `½Å3/ÿ�?©ýl©X±Òê·ÿ~|R“ð?€xÀÈ@ ¡@¨5Àp{„ªƒ~ÜÚa‚4`Ø1�nM#þÜ„eE8¨â�ÄNà ›¡äç'$÷ ®6"Þ·1¨ Ø!‹¡,«ÃöC¤LwÞˆæXÆ�d~Ìyب¢…s¤¿aÈQÀ&:DUÃÀC"ðo(ÌIß°rÇíQ%Zÿ â˜€%Q� Áý÷cUT«þ4Wßh?†Èt  U< ƒ=¼†Xðp«Këôƒ=æJÀæUÝßÿA¢À´9»4)fÓHÉTɰQ4g¾�¿Dþö©ÜPâ¯ÿ¯R±c¥Ô9­þüÃ@4 2€øp&\»�0R ø1˜  šÕþ\6ãÅI¿Æ0Ú**WWÿ!ŽPÙøûfÁàÿ?m°x?‚,$ÃÂn¸p,_*òùNø)Ã51:Ï×@"'ž æ++–]ª ~c™séÁÀ à@7|BS­Æg* f&)ãsAD<þ�ŠJW±*“ˆ;ŒY^aRègèB2Š×­¿çþX~}Ð7kc}oVWP=}O°FCÚØcçé£~W—À° Ó¯©à€ïPA,ˆ—øÂë�Þ©6µ™ý@) :¯bN¿ã>â a”C�ÿ¼'-¹n¯ÿ6¿ß··¿ÿ¿à8œ† —·ûùûñ¥,ô!º|xp#€ “ yñ•^Þ€ûK¡jêÈŸpL…§wd%ÿ(&H0Àì�i´†6£™kNª;ôä{SоžWL¹¹/ð)ª0ƒ{íø¥*””Ðq(VQÀƒ�æ"8óB’�4+ÆB<ò_ÀZMC®DO‰³Á }Ö×%_|f¢éN%ý€B‘@¿ m8sâþ䩊уžk5U_Ϫ>øÁ:цj¥_Ïñ?–(‰�ŽÜ/‚[áQÀ–k4|M(�½¦¢²œ¬<_þñýàÛïÿ‡øŒ¹[;¿ )?�ÕA[Êlýâäà”8 šm\š¾ü'Ï£MÉz§ï¿ïû þ°®§õPàG÷Sܪn¾‚ƒðR~ OÁIø)?'à¤üŸ‚“ð”|A›.˜ùì??‚ƒ®_ðˆ'‚°Ãv/aõµ>=áG–¬O}ëøï) $?ŸÏçó=(tÃ� ̶ÁØ¢­àI�À>Q›‰`×py Þº½Ÿ_O|Å@´ ,ÊÉ&ÿ^ð.$Š4¸€i›ÿBP@\[P  ÛD”AÇûB`é”’°�, má6˜ß´&(ž>ÝsÁÏ p8¨ÌKYÀüy±àA€@„t°8`!,!¸Ð€A¸üBF†–Ž·18T¼BFÂTÙ-!�@Kˆ\(…‡rßúIi%¸…Ä.Zþž!q ˆ\Bâ3ÿtÂqÁG=-}¿¯GaÐ4ñë]Cñ†5Rgd!XaÜ9ó—*TàB'aí€|ÐíÐ3"œSP·MD €ûÀ w@ ŒÂ#)Å8=,|@8!q ˆ\Bâ¸Ai×ϸ"?ŸÏçà”üºAŸLE8!à‰ëðä²ø_¾ ¿~2Àÿ† ܶ‹ Ž©<œ€”+(�eÐ!Ûtì,ËK–ÍÓ¡vÒÁi /Ãvƒ Ðüà§ú @Eò9¸ú.À=³€ÎbL0oHt·fÞúm¿ÿ¼%¡-bÀ¡Ûàíž]m‹‚£pñ³e«`âúúúúúØp' º€ç<§ÿ¶™eÐîeЕ¬P¶8ƒóÌ!« ¯ÿ!"?ŒóÔÀ#B2?‰»½ ’ѶãóÐvy¢wÒ ñ«ë;�ˈþ!{!0TGøB ­ËZA>;nßï÷F–K_à0 ÆYg™ÓIa÷n¿ˆ]Ú}Ì7|ûÛä½?8[ ‚1ƒà𠙬5µM?€Ø­ŽÿÁ$8\“ÍøZÍž¤ŸÃ¿‘iùþ'BZÁ…Eß=x0¨»ç®!Æ‹x>� «ƒ£Ș3nGÂÀRO=n …X8üáàæ4°^aìv·ÓÁ嘠=÷Ò›¯˜=Éï¼.B7Š T P/xURÓ[~2ÍýBݹ™<J–/K.zÀtAd�ÿÎÿ:: +æI3ðÐ|Kæ8´ð¤$ˆ„‚¡"xQù¿(.H9’‹«û³)í8­Jo�/�Ç08æäInE€rªÚ±øy\Ë’º¢Ú¿ëž¹ÿü¿ÿáÁ^ 0À,ÂÜAN-ìu&Ĩ£6ô2¸ªswEî0]+}Âã�¹€"ÒVÚA9+tBù …ÀC0°aŒ¨ÃB…—?8à5Äqy€p ‡%wàOSÙ¾ÿv‘ð³Â§à‡l+æ4“î,ì¾aPÁŸal8�z#Àe=&<!êNoêã'©q÷Îü8^`e•t„Ÿ6ïý¿ÌCiÇe‹ÔsK¾x7Ë”†•õ…ÈDø@ @Œ 9A×FÛ¬pØ4!l7ó`öƒÂ]ç¿8xºÉik<Ûyð?€B:FzãI…Ž/ß©àòb”PÖgÂä#x,ÆQ‘ž` ¯Vü k…„/÷0¼€©œã&.¯Á`¾7ºÝBl�ÏÐ&د˜"móðBmŠù‚#è×Ïál €Ô�ØÈШ #XDS?¿<Ì$k�ç÷�û0V‚½}œ­1­ÿ¡f…6tš¾¾,2lâ)WO?ˆˆÃ"èK|ÀîÂ[d&9Ê4c“ÀþžÚûØ÷Øg‚GÒð ¶ŸktY~ ÆB@Ç3ë“ ¶Bxô _‡Á€€`Æ6€,i³='q ÷ïÎõÏ`…ÐÏ<¦€2ý ó…ä!9@‚â�9‚rØMç IÞduþ2ú憹üÊŸ…±°¸Ã€®ÿŒ \÷�êz«Ã4û�ª»·àê«_ß+8•þ?!{'±ca Ë•z­n.Tn~ýBöBp"hpMDXž Øa²ü6¼ß `ˆ…à*A€A€‡€b©™®ày-³5ü-‹…Ç ž!€q‹5÷› Õsz‚f#Xþ^¼�? *‚À› <Ñõ6#´|h¸$Žñ?œÇ–*Ôøì² X…±ì  € X׎?ÌñÂÍhSø\„#ð%AôD¬ñ¼$áÔó�mÿÚv[ð¶… ŠâËÖÿ_m€ÞH¸Û Žÿ3þ‚è—á` @‡…°»ù�eŒY;ïû·þ¿ï ��ø.ApQ Cà•w ,ÐÅÑiØ~ LÍOÎ'SÀO‚ *�ö*¢Ðç錿Ύ•³ß{÷}Á3ð Ÿm@!àï‡×××Åx\8ã­s#,º ¹xY–N\-�,Ña‡Ï4Dé-�ØŸPœ‡Œó%îÖ¡|˜C(`lBkFŸ�›Û‡€4Êåë**Áù¯ÂØ1�Grqz‰²ÏGÔÿyQV­—ƒ«G¥ÂØH 8€ƒ$0�dV<é_P嘎 Ç”s^bTU¬–ÍÜh¥Âí�˜#‚ÃÐDFb­.o ž§b‚¢=Ù£2ál@À(‚àXóZl°cÔíñ2ò¡-Bä#\ b‡»ƒH8îý÷T+t푉”ó™z{øÐ— n›w=_¥Ð79è.Ù¾Âä#xƒ‹,'…\.³°nÍG†Ôñâ)@õV#eéÎ�ÕËF2Ú8èü8Ìa€@�@« @ LòIÿþï%jç�©Ûð"¯ï–¦½°Uýõhgϰ¶Ð€àM0UÆxû z1q棟S®m6óh:·ÂØ%€¢M0U.™SƒÌO,f€Þ°v3¦(Ï aøRLC¶ÐQâó<ïØ¶€C&éÞ£’âàûF%ø[Y¢&œ*(_ó9Mè»§¨qs_33ÓÕ�Ÿ|`Iê„ïŸÂ÷áQÊ”$)áë�®„¸‰ a'e‡Lrü…¹`Œ2Ì>ä<èâNËù;/à™x&^�€”A›o4¤ÀxÛÃ+Ø!Læ!Ráñ%Ô¯@è#èÌ…Àý§>@Ê0~8Ø„¥›¤ppAD�˜Ù°†ÅŸåðT P€8s^LÿÅuV€Å` ~!ã _ÿQY€ '�l­¼QÅoÁLs„L*ðØ€"ѹŸµi�lEˆ�ßÚQ\â3Üô†¥)nVLK�¤K*a¤'’�ëü QÐ +o ‚"edÿkùå†àŒ¹i‡'í| P+Dx‹0çî‚p‡ˆÐ©q¾»ü1��uñíú ‚cAË9Eö¿iÑѼ8ã{ðòtÏ<þ9 ;즽³¯ù~è>𨘣paÂ1ö€Hkaƒƒt^õ„æ‚ÅÏD» ”%,T†ît%’ æÅ[æ Q\èñù@Q±]3zGN_ó1_¾?áŒR #´ÎêiÖG'óËîçê»Zn d‚7Kö‚@ÆØE Ó7¯ð€Ø)�€è舕 ežk¹Í=®­žD¬s/y®ß`@ NPÇy3ÿÕnƒ±XW »à' 𓜣ϯð)ÀwO„Å%—x<Â6¬R¡{�@*Å@+B&Ñã€Ï�l"«>&ÐÜ�ŽùèPÉh8eÊÕóX`U¾ÂÚÐgoA  ¸š}`;°XŽ¢‡šî”8Ê‹ÞV ß?ŸÏåÿƒxD8À0HÿƒMm¿PÀ›Üçpëd•ü…V¯¥÷h$ÓPyß7ø4�‡˜!§+zü06*‚‘@›ï¿ÂÊ (@�Ý™g†tN/,ßæÿ")HGž èOGCkþoùƒÖãþƒF �×áã¦|À ö;Žî¯ÁàݬŽ#å e‚ e“i¿™‰Éþ�þ‚Ѐ€G†>o‘óÂP>@ˆ¸Ïá¨ý;å%] 4VNüÿ¾á¨œ$Xß/B°,x�îøÃÀ™_:ˆ¢9 f� ,J¢3¾`F%Ë<Ù\±"Q ï˜щQg›)˾xðû�³µ8´Ë ‰¢Ih††áökððºò ~æ(d=ßÍþaÆÈà=Fi9âQŽÆ¸ãbfHç�ˆå~òøØ= !*\¨z‰r™‹^Êk#j€À#†‡÷è¢%Ž$É€8ÂŽ·Íˆ8»gž (ë|Òü °0Â&8 ÀÐ ý‹¥�J;srjæƒÁã4Ðcp ‡„�ÿÿ¤¾ãBÿº'óàK¤–uA*ûÏ1>Äñ…Áz"¼l¹à$!Íå�WEÂìWo 0*·È@|Äm|­ èè㾟˜ýøi�ìù×çÿ·}ùà€üÇàŒüÇàŒü[A›“=!àcù–iJÆkœ‘P¤gÂ&s«Ý ìsä0b€›ÿq³”"V"p`pØÑ-ŽxCî?¼vºŒŒŸ½¨H-^}~úpVl¸ÓU÷„rH ƒ’p™?0%}~ß™±õÿ1H†ÅœRN5û4ÝwGÀú#ðè‚$¬úüÍæ5uP�ø BÙŠÇŠ9b™~¼úî%Àœñy‘Õkþ�aÍÖ^Výð áB^KÙ<òíþ#=Á0Å•à`r¼bؘËýÿ`r–1l§�¿0ê¸kªÖ ®@…òƪŸ|Ü�ù¯æÝpZ„Ò_ÿô d:O¹!_½¼#+×ÝðBB#¼p‹­ ð„ÕÛünd„Z*� ÷àYÂ.´�åÕÛügM ¼v ïéŸúŸ°Z/ànz� ж7�{pälœkW|0«ˆÖàÁút!R ëð':Cú|1’�½à”þ:æ*ëÿA¢x/Õ Õ»û��8¬8Õ’J«‰ý¿À�laëêiíÿ‚�'ð`x ¼� *h$}Ìck|�d4+T;3ÀdÜ8‰×ùšýWZ¬y;!~´EÒ1§ïÏÜ&X¦—�ÿŸ|ƒ¬³ZBßýá”ùmcàCsŸ {èÐø2CKȨáæÁõÁØCô ðM·,^þ Òò+8y°'q„èA û! !»úgþ`´NtîÞî¯^o×óý©î_žüÏçñ ˜«ªýxᨙd„êÔR²az¹~éÿ𸔴ºÉ_üI‡D˜J)gÞßYÆú8@ @ `h•Ó{`;‚•ær5E¶ƒˆD`yȈöƒ#ê&_�Ñ�V‘ˆ¬©|þ°€1 °º¶€™ø1O|´jý€,4U˜T�0väÁX1LŒèÿì¿ýß_îðà«‹� ±†n”€å¡WñÞÑc9Ue“¾ 0e €@hj@? D v)VÓœ  ýûdBìRFךpAÿ ]ðç˜CHÍkŒÅHœ�þý²û�ºû„þ =A…ÿý¸¯…˜‘™5ÿPjªª 8 5UUŸ]AŸ±E4H!àeõàœ/Rx'€öo�³ªx×Û„œëW}ðòÊGÅï¥\³''®ö¸f#‘뽨K` 05\>xü 7° É�’n¿àL8juf[礶âL=íó.ìICOï™wWׂW䀑Ð:Uðèdq¾5ÉJ¾Â1Ïɉœ3�†¾š÷þoᨠK\úûÂrPH7óÿeW5;ÿ¹™¦ažø\„KÁ¨‚`$HDPâŠ6ZÌØy=Tn8îK<Ûýx»@Ú÷ ×:nÞ]`”Ëßï÷�S( SÏ |k ñFP£Ox -Í¡l!ðB�РÞŽùéÿØ XÉ+Á1÷Þº2¢q‰«ÓÚùzE!‡œ 8%RŠz¯¼- <r Ž d7ÉÀË�oè:tÂ`Q „ás8JË|Ö^ÚñßonZñQõà™|-�’@|>&Ô�’ßÿì8ÒÅ ðù烅xlµ¸¸}üî–xÀ ‘-�Ùï óW¾€<²"çõýŒв Ï?…ÈÑ<=@J�Aiˆ5è àe‰à "¢ªN¤6ßq½Ûÿs÷È"‰ —s/ÅÑV²lÿ`Í[s‰®yþ<Ýåþµ8�iOðuÓíÈ_!0c CÂ(0‡!¹¿'7û?°¡RvâGYƒßo�Zÿ0ñXó@Dq,¡öN¹æ<5�SP Ó¹òðغµÿÒn`”3ÐŒ¢O€"µÒõÿÏ�OÌ]³ð»SÏÂä%à ÁHÁ8hCÔ੨‡üMñ„ª“×Ç›ú@ë’Ñ‘�ýg£I ÍaïV—ç [âƒ<âÁË¥‡áoÂÔ¿Áà9ÒþM“ @”°$ˆ¸ û‚`›È),#ßA¥èmùóˆ j<�×½û¾ :¤<.=Ô L¦�Âož£†Œ¨< pEp¤z¤þmònÐ ¬KR­ºÍ½sJà¬A¼xKøÁh@’ jv»ï¿Z©’uÿp,<Ê6Iy_{ ˜^AewÆ_}õµÀ[EÕ!ûSÁÞYŸ~Ú3-·ß¿|Žð#�d,`Ò Ôí*£ •B¼Ï7ùÐøà1üµþÇÅYûüü.B'ÃBÀ…Á  p‚£ÀIN#ÇÏ´z@l—ŧ¾˜Uñ¨{”Ž„b꟡ѯŽ�LL²Hg™ýâe’!ždxø]²(0%/@&…€ˆ»o¸Þíð\‘ŠÆ—Á‘Ùê—�ðR Íçi\Ÿk;2Èx{÷.=µeþ å À|œžÕÏ÷–Rψ]´à@þaÚ`ô€ôç¦#¿w`ÌkP áPd  —X›Ú r‹ðˆ'¸S«Á2úø`+—ÿA¡?Fåá�"]|øßÿÞÞuö§›î–8ÿƒÚÓ¦| UUà¡x>·ŸÐtN^¸'�  1%¾È“ÀNÈ9Hè~ì°<Þùà »söü.DÉà@Á@[DÇÁƒÍÎç‹(߀ ™šÍ$9Ìïö--“u!.ɰºD'àkIá(BýOð<çíxRJ¡(^Hl'+Ú„>ŸKJ/�wêà•ý¼-€Ù€¿‰��`wÏY³þ!ÀNÀóðœDmç£Á穑טgç¬-‡ Þ†¢Ø"¾zø3ûž•ø_!0Ì‘×ËñÆzŸ�3O²J®‘Å€Óà@ ÇâÙòð$Ê‘ÈÌK; `Š�Ž„EO qš50W¦^ ­ËBØzAˆøT„|„ çÜÞ­OÒ¨[Ö GÁˆ ‚ûKšôÙ&÷zØi–(€-ó‰oáØûðLÇ@ƒ@<èéCî? Ænû?ÂØjB#è” ßÍ' îç£ón/«ÀW>zÓ¨\„k‚ˆÀ"™„@�„aW5^¨ç¨‰�8³4˜d�pÙ}xñ'ÿ0Ÿx*íÿÏÀ*ŸÒ@!àeõàœF�èi€æ) ±x³ÏÿG¯3ª|ûi®Ü]ÿà5ÜõÜü.DÉ @à@D, Àw—qš„'�‹$™ÖpÞúšÂâžwû±}�gÑnšÞ*¸oá~žÿùs¨R€‚ d‚1uÈ”E@*G¤XŸB¢ªðLü.Ép±²„pÒOz^YÏ/Ö;ßár“�î* ‚9àÀK’ häÍÂÆ#Hgr;ÔiÖ?À0k§¼<ãBØi0 ¡„‹³~¯êß>´¿õý.ëÁ9 A‡Â$ Y±fŸár“ƒ#ˆ„‚Ž{@†à´±™ð¶’¹PÊæf¿»žûà•z$T ‡ �—„ ¢†áôã÷›gð:â ÛG›·ð»�˜H"à°�@aâfø ´°ÿpóÕº�ræ°—†ÕÆš‘O�&¼¡|ßãëár¼  ü�ÀP`0œ*6«{f“¤3<ïÿ„FŸ@m×£*EðÞþË‘|7¿Âé�œ�D!Á€€ƒbîHªÌõôˆi6-‘É›…ÅðºðÝ0u…òm3ÛßÉálЄ@@*cñ{žoµ›ý€<´rkNÿœÏ=¬oð±»¾b>ëÏ…!+ áf/–î´Ü«àÈÁì]Ü�ÿÿÿüüböå¯ÿ€ªS:ìÁBð |A›Ö4¤ÀxóùžgóáÁáÄc·Œ�MÿÁ´î€ÿÿÂ…ˆZÁr û÷‚“CÊ—êçÂ!2ì-ŠâÛTŽ: 5W5θZ’ãî²CTͧ5£IÌÃç3ö `*žP…ޏË_ÁàÐî±TŠÝÿù€ÎF+–LÎ_ß=á Å—ý”À¦´ IM÷ÿV%ªe#&HËïÇ‰Ïæ Í4ùÎ8G�©ŒÎ–¢qÙð +Ó®è? É\$GäHa€�l-ª(ºa¯ûëSÜø‚xÀ@4 Y82v”° ÙTas�<Ãßx2ã ˜ ÆÐ²h3)ó-•C7UØe÷�©“Œ. Û0ßS?Óû¢~ä@Y®ûPøâ„C‘˜Ag�Œ'L+½§,‚OøÂ* å{Äu$ϧóø�0ÎyüÌñÆÙˆâ4×ß¿ÀÑ!t T|u½ùÞ´wCŽJ­ûúd`ªiÎûÀá¹–^DQ“൯±Vuý¼ eiJx¢_ÿö ÐTô âÃÕL lˆ|štÖÓOð8¸è!{MûðÄ�k.T"Çß ¦£-“Oð8ø\ ý§ƒþü Ï>_ò÷ÿ_þy†‡Ã<æqæ%°(,ÙEÅ'àß�pÀƒŠÁbxüá'ýw�ÚGi§oÿÿÿÀ”@å„PÞ’þýà/w!Çj©þÒãXù#9ÿx $s‚h ùY?e¿~ð·wuéhÀ,Ï\ !Fߤ"Xààïß¼ªæ»¯E‘€,G¥¤&øóŸú‡ ÈâLlϺ’Sa„$d‘�`F1‘PVz‰C 0#dt�aªsáÌ=Ö^æŠgÿ´ÌŒVÏ�ÿô UÄK&ºÈií¾›îãÿÿ„@ÑP€@` Ç@„ѤdÙ4ÃÀçÂã ¼,0GïÞ&�!·Vè¡à9Ð|q{†‚†~ýó³Á9•Ñ'óÐpÀCò¸$Bê /ƒ xyÏqøÁnöÐ1©ÝÝßúø@ @Y €@D !à�q?h8Wõ×_À y ¼Žþº˜ÿqÿ·3Oæ~Ál @бHz {Á22T÷_TžPCÇlv·à€ÀK!Àk¿z0!ûƒHaûÀÒŠÛñQ4+wÏ>~½MA!“‰Í>uŽø4;¬U"“¿ÿ7ÀÎF+–LÎÿ¾ì Œft½‰í€W§NëøL+%p‘¹?ÿC à   `=™4bè苯$º” ¢| †á›�?8�#3&†Ý Œúf*IRœÑ„_Üî MQüQs?Ò‰ì‰ÌÀó¥F ²„~ýá1ÎÇ?øøš@a=ã¶Ãaÿ‡Â]‡3Ë ëχ?Ÿü®Ô`”ÃÏ?™‰Ç*š? ØyŒÙA³Ô'öb;Fšïÿ ¼ >„ Ž·¿;ÀÖ�¡Çenûø ÈÁV.Ò3�÷�Ãq6'Qšäø@  4Œ(YR˜Á„_ ‘¤6êEgû ¤.øø ?m# TâØ‘wÀBd=ŠNæv Ð>)·ÂÍ Û÷À �Øÿw^#¾� 48¯Æš¼8ÎxÑ–÷Ëø.œ€Ì �€¡Ã!Õ�#~@±™íM—(È÷Ùè 2#Úx3ܧÄ~EA š&lDqÞ»ïÿÿî3*þ„7 eÁ†´�t öê&‡Ûö‹�"Õÿ«�ûµ"÷Þ.ÝAöÿ@ú,4º-\�úš™ ;÷<2·Ÿ­ÿþ~Ãçì8&0ÑS ó!âíÚiÂûoÞ ï xÑOŠaa€Q€cëquЕϻp‡¨‹Y~#˜]˜|±%†²ºÑ�Ïïð€¸!ÂB Â`îݰƒÿ®€zmĬù3„ÇR„pϰuY–ýtdÛ‰&n_Âæpˆ.¬¢‚�˜D_/ñ¢@¹ˆ&5ó[Nq¿´4Šzç®_ÏBm³—(÷w0ð€@ŒÀ,| -P†xüi­® �³ Ãð+Ô8÷ç¤$ 8¢®AŸôE(!à?áÁ 'í À'1Öcï¯1;ÑcéŸþx>ÔÓ^?ÿïàn{xôzŒ÷ç•Gà³¼ö{óMÙøìö±æ«ã (Gp$�$48ÍãIÈ ÇU•Ò;f¯€�õ;×·üú´œß›~#!8/ápRcƒC„`Æm÷�àLPVI 2&à"ÓÇ›rwýñµe'Æëð6׉s¢è¿z+á²€ �>zf‰† ŸÅàe}_n|ÍŸd�â­¹�‚­~¦ýù¼Æ}Yÿ¹h¦6¾ ßþ�`Ÿ6ÅK!Ìr?ÿ·× WkºáøV×5θpÀÁOë™® £²•Ë`èú÷Õãõà ’…È•KíÚ<M³5:pgG´õÕÿ~%O ôOò¾Á"(BˈºTnäƒ÷ïðö°ö»}ˆ÷ì ºT~ô�…û÷øy¥>@ÍÒh /ߤ‚ ‚$$8ÅrøE_ûà™|.B"xD& 3‡"ƒ0ƒà€7ÆÆ-]„†34î¸ÜŸ½àc$=ÓË=ÿÿàKß5yêÏ 7:ç:YãOd§I…ÞÄâ­›è"šìßwƒîtë|À_£þ^ dìßuðDëj:zJÈ!Àluïñ€$�.ê" ¾Ã¶ÜïRÓà†×;‹ÿoøÍvj±_}ðL¾!<0 <|�Èñ!àÈà¨Á‡îH¾öøÖ/ÙgO?ïû}êcÚ“>pŸ8×cßçÂÖ-m~åùü80­­ß§ù¿RþðŽ¶’UýçñPTl6€�°\ÂÑkiÏð°9�W®[®Ëü_€i­¿à $}?»ï‚eôàˆž@!à?ß…Ò!?ÀÀ  @$@9…`ÅGI�Ll‘«úOáõ™žxƒ'}¯ µ©ÓêüÅp.rkݯÚõ:~oÌW¯ÏÂè—¸ˆ@£†‡Hþ´pî‘Ñ׈”ãûßüù§Þÿ ‘ž €Ñ†€°Q"ÄHDHXn¬Æø‡×_¤BC7ó³ÄM3ûïÚ†k_ã¼.ÓTç×þ¼„èH@ㆇ^ãÄpãø€O€kS§ÕÿðË)ª9ýð¹ àˆ x B""@l„ñ´è!<&¬ü=—�G1_všœ}ÝŸÞb¾öš£Ÿ_jß÷` ! Œ ,CCµ'm’µü|D3�íÿâ-9Ç÷¿Âä�œ<–$ (D€øÜ>‚ÀŰÒ~;5°QçïØ7ÿtQšÿî¿`¸Œ×ÿt s„C£ aÁ^Ï ˜}¦£�ï¿Áö¶¨ç×ÿ…É' °` Y I�‡ÐBx.˜!?<1Žb¾vµ:ß7æ+àšÚ£¯ç‚‚F„CáðÐà\»`CgµíÑÛxÄÓN ‹Ÿ'™þ{ÿ€VçÞÿ ’BqØD¡�ÈD@@@@>7Ã>ï§�ó¿Áw'°¸íøŒ×ÿu˜¯à -}ûðPH(|øhs/Þéë}†7`¹ÝNçö°€Lju9ýñ�?½þ$„à¸BÄ€™" ùÆO “¬ð‚ÃÖô`3_ýÑE®ý÷û£qšÿp»%Ê'ÃCÀk3¬ëšØü_G—…ÈÈNi¡È@“ˆÆáôž>2~A ”J+Šù‡­q\×Ìk2±ý1÷WÁ8]’³|@@4;â Ç1Ñ› €Ne¦¨éóÿðZe²£WÏÿÂäd'bBA@“À‚Ãp5 ?{?xõÌWðòwYŠþ„ŸŠÆŠÿÿß¡Ö�èÇÕIÏ–©Â …”tøÁB›XW·¿…þ0(!ö‡{{jt¦aô§Øn¿LæSÏ�ΖÿïÝF/Ÿ ÿ焃H@PÓy€ H’ ÔV>>þ`H’†½ÇÞ �ç\ÄŠ})¦<@UãbéS†ZnßÎûp' /˜¬zËô �¦Ux€…õÛ]×|"›Ì„;ÿ¾ð€@$Ð ²è’ˆ^ýà£ìF|(±›ZëÞàé¤Áó%À½ûÁC²ˆ² †0eW®ÎsÄæOJ'¦�á€ü_5-ûx`z«`³Pq? Ow|÷T|=f!‰ÿ×€âu‘hq¿þ.€Á> CI!@î ?tì”—õ}¼ƒI!ÀrŠ'îƒp/få”—ûsÏ™ôŸúŸQ´«Æ>ªNoKT &‚2�hõœOSéM?ì±FK ¦>À§ÒŸa²é€-�üÁTÉA"ûûÁ.¼ÃÓP!ŸûÞå%®C¤éïð¯GF�@²aà\/'"¹»8ðTI…“Ô@¯ûû |œÊQ- Ø $ÃȾ(_÷‚s˘iO¥SLjVŠÁ¿¬ßß@Ùïã'¨m·výß߻̄ÿßx9€Kð2{�wóx@p %À€ð<�"²Wk�£ÿxfW:ÎÕ`Š+¥Dˆ¶JP#ÈGþð)ù®rÀP¤¢%äK P(3Òi┇ =‡ª¦j oÁ¬+jn8ô>Œ ÷X —ÌQÎ7÷Ð#ÊÛZ©gÀH@�þ ³¨Ÿ^ýßà„¸ox*Ç& ŸD{ß òøBˆM½½ ±?ÿôÏ…_·üÿÌ”M=†Ì¼obS/ž7¿¼ ‚Uæ“!_žªñ±wE?ð…z:4ëoË ¿˜¬ è“ÿüXØ@ €4*>•Q)x©÷¼eÈæã{ß¼z•(Õ2}ïM¬À½ÖPo~ùç“̽àŒÊSLô¦8Ü$¼Ü¯�oPVî;¿ÿÚ¦d åÿ¶ðJ:Í@‰Bo»À{•YÍAÏø0€9ê (ßÎÊ"H|0±Uzìç‚i$¥¸€,àYÙFYEÃ�Buë[Ï ÒIN”P úv|üè”úE ú{®ùÚl9fªx×ÿ! ûÒƒýôš+6z©rã\ :’@ $Äöadå�«þ�ië9�¤-˜yÕëþ�ië9”‚Òp4© ŠÛßþ^™Óîpa }mSõà6~)?Nóþ W¤ýjß?Ÿ‚cô¦–0ëÌxXÀÆüðU^6*tíüÚôr4ÔÛêÃ"üVz$ÿß@Úï7;äÐ@xð¨@ €‚ ô�R!¯ûÁ–ÉX#È<ï@ô˜r)EWýØ!¶JÀbÊ?÷ÎÏ—Á€#ÀÀp€l0ð¸†ÇÚ—ˆJ«Ðg32žRî#&B: ÓÍ?ör×*¢ Æ1ÿùƒL¡/àTSû�™ñ¢&d­3E½ûù3óB'~�1”žŸI„v –¹×ÿÿïàÞ2Œd0Iÿmà”"¼NžÆo»À] ^Ÿî>r+÷¦ÐŒá~è¤%ù9ëï:öœó¡}ë;À0h`Àk¦¸å¼‚Ò„yâ"“§x‡aF‚`^ u²ãK>�iÁ äÖ"w�>ñ‹ƒÐ“áæïÿKà4Mƒ@è6¸ `â(ë¤RF͆™à°EqÂE‘C®tÎ]ÛžfJHF."wøj?ŽHÜaSûЀØÙ"xñ]Ö†ãX†!H€‘ë È¢ So¢ÃÊ©n¦5_àéûèmà'Œ�N%ö ó«z‚2Uø ¾ƒnä‚6) æ}í·³m¿Ã a’/Å;ì?Õ"¶4ü?‚sðP)Až7E(!à,—‚g™€dfЛ+ª'Ÿ¡¿Fþè á°ŒJÐvÒìÏlcûþÌYµ³•< ß°af§àYÁ²»){ðÕ: œ¤R }x&~#Dàä(l‡AZ DX0@µ's´×þŒy´�{ÀUàɪÈ:£Yÿø°$¼eL÷ÝWêl}´‡7•Þü |¼m8øï¿üËQßÅ R:>G·ÿ€½OUïjúðL&;†‚a�æOû÷Ð ‚9ëºïû¿ceݯû¯Âä‘àH@~X€¶qað€µëÝCPà,§tsÞÔ5À�ûó�ïsíànš\8G±ÿù¾d9y?Žï¶)\sßàß/'Ê–fu©óuð¼éë½>¼„pˆ|` à'BèHhEš‘ßg¿ÀøÁG‡­ß÷ÝÛ±ðq?%Àâ:$3âB �°lPy=Ñð8TOàúXÚRáÙ³¿¯ªõÏp^N]$äËÿ=üœJÚreÿžðP"“‰‰„A7h 8‡† 9c>ýÒüûÛæq¦§¯§¡ýd7h!ðÑ/¦,¬ÎöÆ&&™£•BVú÷æÜçmí‰1¬äà ÷ÏûóÁœù÷²àu‚t0Ð\È×oøÿë‡ø7ðÿü.Jàà ÀÂØOo¸FÎÙ!´$ìæ€ýkº^ÿö¼ú›ˆ²íðײÓyý(#œ¬£½×þ²«o# A¸hP»%ßF_xú–LóÀRð¤Ôá'zL7þX½ÎMãÙø·öxŸ—wïÂîG\‡p³úïï¿ÿïàà"KÃ2T—qú› O²"Ë-äÏáùvÿ�;©k¾ é"x€Œç‚0�á1‡¢¯økÈÒ4d�Ôq\O»xc:9l ¼cÕq\ð™|ežûíçGDo/È ýÓÖ™¨íÿTÜ÷^äIû÷W„©5GóÕ (OÓûyà�|.F‰Ä�p ¢°'�Ø  2Êg Õ€Ì"Å:(ÿ¾'Ï>Çí¼ÅŽJP—ž²ôHÀ8Æò~<R]î@þ“ñàõJNõ_Ëä�ÀY< ð"�øIýÒ+´î}¬|Ö‹Íæ·µãp4ˆ!†ÂÃ~ŸçNIï›ÁÇ þ¸ÿßaƒÝ<'²ÑSÊoØ„My<‡çºæ¼¿žÿÎý×ï‚p»#&"B#L†H@ÛÁ¾·°ÿÛÆŽMg& ít¶ÿø¼Êþ¼øMÀ/9eb™ß»oüú1NI‘íñ,�½ÿ ‚\6ã™À;ZI„‚~|¦HÇž7ØÎ~¢è¿¯ÿïø�ȶå^ ÿÐ+�4é£GïÛù&D$�õþ¢¶}Féÿð?÷Or¾ÿÿ€@`¶žX@!à,—‚gát™8=˜? `1€S¸¯¤È†FîÿÿïðAÇ¥Èz®+ž,S&�&ÊøçRÉW¶(îYÊYJý«¡'H' À‚80Ó€'Oò#6?î�ålzÍT츿9æŽÎ~©_�‚@ ÂêR¯|µ74¬æ÷ßà ©6Tz '§¦ûéèùÆvO×_ç¯ÏÂé2pˆ|* °'ƒÌ·cé �·þË%•Ñàö9²dÇÕ¿À H°•k0�Xb–­ëÕïÌדùÀS×ê÷-×ñÛù¯è‘ ˜+&‡wmöø€L‹ÒÖöÞš?ë~""ø:Âa  €Np°@B?÷îx|ÇJË]H“<óm¿ôÐciî)¢öüx‹�6•`rœåúºJÉ,”|�_ÍðP!+âgàfL@ÿ üÂ.nßóçÿ¢&ÏטS¡1îÜ&£F%e||bc€&S)#Ûí!Éçkž��"¶Ëm!dãµÙ|  „üϻ㼀ߘ|Ù?öÿ†‹øÙ?6ðP#Dà€ÁøAâAð �ò,•Þ{ü‘¯�$ÿÎͯS�¿ë¯ùZÏr-Ÿ–˜ð©´=]î¿‚®ëPpBDP0 „CBƒ‰–iãA_ ú×*S½à—ð ×=w½öiÐz®k¯{a)¬ô~|,ÀÚI˜ëÅW—„£×:ugzï_€œédŸ÷_ønt±jÿ¦ðP"Dðˆ|@õ0’…�¨Á€žXïq¶wºƒÁnµ£ªSHÞÒA6f—{î¹ÐE«½3£]骱·€h*Ú¶ö r4½Ö«x‚VÕ7·ßá†ðb C؄֒‰³�þûÿήP­f¾ ¬ì×ãx;�ƒ0°Ÿ¦‰”áÕáD{.¨¯ê¿À†eË–²óÑ�ÉgÂ7ã^%œ¯…ÈDO‡Á Âðrðø) S:E¹wÁ·Sny„—c«xhß{zCã®ÏP�§6&¹»ã�Àפ¡ÃÚuõT²Ö ¶¸/ç~ë|°(�p�<Ûð¹®$" À– M Xžc©Ç§Þ!-9Ž$cõöë·Óó½t7ÉÝjÄ!é éd«ÓyΜvŠžˆ$Å¥é¼áãGh«ºø'á™phP‡Êöûê•pÜK<¼ô ­³ÌÙ«'.dµæ¿þK™Lû~ø AšZ4¤Àx Þ ¤ÛI&ÚOÿøV1qN1ìäˆà€m-û½Àåºl‰žÿ¾Q»Ñ:iÓž Èÿ§m7ÿ^ÐûC¬„‚À°,4Ÿ! c½ûÃÀÊTdòç„â°w/W÷ŸÐã¸kf*ޏT _ì÷Ž�‹÷·g€X[zîñÐi=RÙïçŸ/†@Ÿ‰Að� Ôˆ‰ôáÆNq7®Â9�ã}mwøÅ—V|*[âZEw‹M Ûþ ô @ó„@ŽBòL$!ªð3'Ô4t0·þ𬜌�‘üîüz5v&9W¾c�@KI{ §’È _RÈ+'§à(XBAq`’j€Êf‡ýô],2õu¯«ÏötŽõߺZ8¶Ð÷�6()ÐòÕ–þµëÌÏ~ϘU¿¾ Ä._H |�� °@$$8UF ¯¯1Y’f�♉ЬsÖÑw<àÈ&{åÀ9Ž×³Þi›k00XwºîöŠ™Þ³ÁA|pp$ ¥@`Âǘêkn¡cd@p›zN�V™Œ·™§ßúõUé¼t¨µ^›Âw>‚‚þ˜0P]‹� Z«uŸxÍnKÀŠ­­6æÁ²Bˆ�®àñX¼7áw¾ŠJ`𱤮L~˜¹Ð#r)#Þ |…%9DRÀXcÒ‚¨„öк“m5°¼ û}ÛA‘jÏÏîtUaH©þ€~é}¯ý]02 «=›î`=aÄùø ùçGF]7OøBõ¦åüÀmÀ–Aš}=!à,�çòÿ€ã€Ë  a ez’† %Ê ”"þ!J”nГ‘ÒT¶ü µ$Œ.åŸyø,Ëä�0]Ï—yü �-àûœH>é_«{X¼¶`BñÙ€Nõ½üŸÇ¿µüŸÎðP=ëZø( ¿ÿðô³u½üzõðP=ë{üïç|ïãÞµ¯ÏÁ8÷­ïó¾~¼ÉƒÞµ¯ÃÿøG¸døì¾eÞ`þpæ$—à.!�lpÌMû`Ïõ÷ŸÀWuø Pè#ÿ@°Àå0üót ÑO™þø­3iãü 2 %`€èkPÓê/­¦Û9øü>ãû^p|çæÿ Ñà!3=¾Gø é¡ýÛŽuþ `9> ÇÉ>û ¤²�|¹ëûü �4 $J  .Œé<H‰ê¿åþ —ܾžD€�?�ëþc¿ÿðW¿‹{mæí.7�Â�A'B0àX�E*T“ïýàÅEI~ÿ€Û€!Až›E4H!à,Ÿ’Q€"†8z/àŠwyDÌ7ON ÿ(”pÝp!˜ü7ñ„t˜0ùq^ Ÿ…Ù0$†Åü\.€$µ¬�„ïî%ÌK0#å"×<Ô +'æÁi�«ï‚Ü`PMO<ëÓn+Á8W' ˆ/.€Ù.5În»L²‰ùƒKòðöÉs°NVü”àœ,íùãfþ¼— �´ƒð¸ð§æ6Öóе0 z‘ ™n²ÄQF•ïB²Ï@H²-¼’Ý–znp³øýàøgA™’£ÁY«"U7¾ö?*ûo=ô3k/q¼ðPă;P] H›}sÎ.ÿ˜ sI/7_PÚu1Q²µ˜#“mù YO˜"É›~` .FGŽ��V~ÐY-ÝZ^¨÷ŽÙäË\ˆ`{%K“~šóh'"6üÃkO˜!È�¿0d2Z—0PIx\„A^`>.»åu}èúk¤à2ÀF¶ûÏß nqÓðäÇO‚€¼’b€ Ba8]©Ðzœ$C±?ÜG�%™ÎΉf¾ZDŠf¬}¬±ðX}¬‡ÝÀPÄ€\ÁL.à„„ØŸ˜Û…Ñ€÷‡*%È…-¨—9päÛŠË�r[fV\¼6üôÛÿ$ÜÁ8aôŸÓ@³ž¿òQ7i0ÄÆÒhÓÏv„½“!ÄSœW#pÀL@€ ÂÂ;iõðIT¡Vëûx�$HB'L ½Ì=g‰D3O§vÊîdrÓ*»˜'_ ç  *.6@´VKžjîɸɉq ô÷ÿ.Ó³H¼óÅðAÓ~`ÞUߘ ?+·æ à³®üÁ:øW…ÁÓÂÎ$µÈ˜5©’ÈL‡ª[ÿcÿñX+ m~®o›€ó-¯ÁÕÍàÅ“_Ëá]…@€  jm3๠ê¥Ï8 L¿˜ aëž×îE{YîäTíeðNÁ'ÁÐO°¸°tRB85x ,#’È`\Åþ,q'ÁÁgÇ|�kZåá±úúd’²MÍn70NÅ2Âìiã߈/}µsLšãÕm&d§�/¼÷�/¼ô\<,ª›L羚ÇB$̹ðþ(jÞ[½P[^:üüßÐ[]­Ûì÷‚uÃLY“9eÜîfçrèB¸€�`öXXb)8hvŸÌ¢'æ†÷"YŠP˜ÑÍñ¯¹\œš¯=ÊÁ:øRAe6@.i°îžï˜X(JYÛõWÙ7¼Z�ø½±uÿ¶ ¹8eˆ‚>ÿÿ‚(  ¥ã€ˆpÅ“““““““““““““““““““““““““““““““““““““““®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºëßÿP.05!�Ø1ñÿa…dd-Àq‹·[T…µt£ï—…� @ BHÀ[€¨�%9 Y€¤Ë˜oÖÆR—ú›®«ù€##>& 0GRÃ1„�ÈXU8O`šñ.’±Ôm÷�‰NûタȄ€€¸N<Šô·Àé”ÖAÜKƒ…ˆóÎüŒãLAeá&z뮺뮺뮺뮺뮺뮺뮺뮺뮺÷üàÖ¹¹¸6QPNUß¿x*ú€Â0K¯½ðì› ; _tòú½Úk‚&€gõ3"‹x #„”XØ9à—<¹²&yæ vg�Ú69A.æ\ÌIß<Â` ¶g ëú‡ð*„qôE þý󡢈nX2 2s&�þÿÐ!Ó&/;Z�`Ü«UÖ`‡@ÑE(od,¢0Ÿð Ý & *ùÚÿ^ÑÜÍFÙX+Ük:î™yÿŠ¡�DþQ$þþÅÃK�™Ó¡„À z€l  ð èA§åÐ sZö�1A¦{ùids&Ö*‚r…t2‹°+KðIªˆ~“ùi1>Vý/õ\ì;ð´1q˘F©”ƒÏA]áÈÎ3E‡K~DÐÙ[<´0€@d    òà GL€rÏ€~kÓ“¸> R€Θ�âœ:iéÉ­.ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºëÞßý†ÄA)K+ó˜Þô&P8ˆë ýz‡ *ÚÚï—†¨¤CYVð@pP@Š@Â#¼¹¼€Q œ\÷†cy÷ƒŒdp\÷i‰¥ÑÌçþ�gAÑ'º –Z…ð |jÜŽž÷Þ‹ÊN;3zé°@ B0¯èZƒnµYa™hïþ‚Nýk2Á[ ¼wìþg<Á�ü§9”Ë.?7�¥õB™Š’÷ý€Ö@xá'âÿ»ÀQ11q�‡Ï7?¢Xú§<ŒýÞ€€ˆ�@ n €€ ™ �HTc³°|ê :¶bC&*ó¼§§Nà2Lt ¨JàQT :×bÁU&禞œšäõ×]u×]u×]u×]u×]u×]u×]u×]u×…�¾_°Ù€#âFz&gi®Eq +ÜAÑßx¿@J Xáù,û{Á…v8U uç„"Ž !Bм,Ÿ›×¼pÀýÖ{Å<e'Ýß [à&ùHûÞ™ë¯y¢þz@P󌛇 ¥·¯Á† (r"˜mJ 2ÿ¤9-– j40€@X „À A0Ý€†Xè$·ù#Yvëƒp 8pU¢* — C"ìùYU,>”O`·ß.dÌ.ÎÿÁЮšŠ.œ[ü@…s‚aÊ+üP}m�T œ�0ø ûÿƒ°aš u×]u×]u×]u×]u×]u×]u×]u×]{ÿãsÀÛI��Û Œ-ò£ÚçAª¤x¿·‚ ª»M©ƒ8Ey/OM᧦BMËo·ö�xÉJ G�l½ÿ¿t`i¹ k{ßóƒ^›Éa'@@@(0°n8h©ô÷G€¹Þû£ ÎÓ�#Öê÷ÇÄ\€#ßzë®—SéDñû1¢“£äñÀ¹a‘޼ð'`"PªŒÓûûаã:,µÝ|oh#”#ÓÞP ½ÓìH~¡ø’  À¡Ü€ “¨j£ –ÿ¼bvCݺìaËpVl<ÂÈ3Nà”=b öÿ»Ø��yIÕ1ƒ–àî0L“`—]u×]u×]u×]u×]u×]u×]u×]u׿ÿaÂBP((J—âϾ^‚aÙGÀD¹ýøÞ™ÙBÁdWûƒ²nÜùÀA¢HgïÁpòÁ à³PÇö }èx4€›^ëÐÇöb�Ͻ @i€›I7¯ e ý/ÜÈ[¹—¯ùîk®º÷¥>~òf[`ìQ–Hcð|¦ù n!;×W²ïaب�…™Y#äßëÒ’E\@4Éÿ‚ ‡œÀ`Ðm¢ Ê ãý¡0tÊÉXG�`жð�L oÚ”On€‡¹äõ×]u×]u×]u×]u×]u×]u×]u×]{íoâÆ¡Øa�j+«Éá3ÀU]°Å`SïÜ @Gôùç_ß¼²Ž(–=·Íû¡$CIDœþ³ÐÀø¥"�½ Ó–yBØ€iøÓâø®ºë¯ÿ°æŽ ôµÖþ½‡ÀIãVº‡ã J¥ÎÊþ˜0À�À¢&d ¢…»t° X‰@%gw@j¬´Áþyÿ¹ü÷5×]u×]u×]u×]u×]u×]u×]u×]{öÃ)sC´ÈŒ$w¨Ÿ/¸Hã0%÷ÞùÅiïƒÞA?á'zô,¥RX)µÿ釄 ( -« Œî§ÏøÇ1'=¤(Ëý4�Îê/>à #wž‚g®ºëßÿì9€ìŽyG çá<~»sÐjV‘óÝûô–ÄsÞó‚üažˆCàUHÚPÎ/ð¤z©¢Ñ %8é@B=P§úÿH¤+®ºë®ºë®ºë®ºë®ºë®ºë®ºë®º÷ý­ì6hË”BßvAP슦=÷ƒ´Ém0Ä%_Ô¡Ù `«ž!tY6„"…Ž ,A3‡¾«x�ÂîMk7€x71Aü÷˜Ð@7&O»;Lõ×]{ìßkA-z®vO,Ày©¨aÀEE$_éx0(Bx¦i glï€c@»ÿ�³5X€Y3Ûx@|ƒa€à¬w€aB¹…® è35pÆ,ÿèw` (w¬uà΀ƒwÚà` ,Lãa‘‘®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºáoÿügGóC�\ƒ/ÿa²FDŽÀ¢/¼0ÈJ µøÁËÞ@u9Wþ½à�õ€Ìû}Þ#‡ ‡0Tn¥=æp@@iuï-`s}*½î�ÆŒÏZ@�¯t“-u×^µ³µ¾A¸3Êš÷ÝÜ( Ÿ;r áŠ\g¾»ÿߥÿý¼8Jâî†ì"ÿý¥ 9h_Õìg÷´Lc¦¨Êª·çï†x` $‚ 6À cž`ˆÜÖš¼œ¨Þh…ؤ�¯4@AÿáX^ó(kób£°U"8’ð}ŒX]}È”ØO]u×]u×]u×]u×]u×]u×]u×]uï¤?áÌ ¿u²ò Šqñfõе–”RÊ¿ôÀ¥ 14.þüG4À€�q,@ ñ«4W¢;›~ôÎ5#E|„Fƃ=Ñ—ïMo·ö ÿ›§�ª`æmm3 `@–Aœs~•ŸyXspqŒwè¬û’° ¹+®º÷ÚÞÆE`Ì„8»Šï¸pï·€éFT[Š´›Ûp@È%|[ÏñCØ9zÈ�W|Ë…S<}Óþœj«ß{Ì2Ú§W_ý 'P„ 8@€ÌY†ãÃê@Ó€± «Œdq}Ó0tòX±ÇÐtÁÒ]‚ Y»–ȃ*¡„ĵ*FŒÀø±JË}H¿ÿ)€ÞÒ�qû$80 áå°‚Íò‘ˆ[6L~Q´fÈDd :뮺뮺뮺뮺뮺뮺뮺뮽ÿö ƒ‡¶²ëåáªiq˜ú·€-‰3ÉyÚkˆRÄÀUÜA¿á_x˜´È@P’pÒ‘ ÉÏ~zbB�7\û²¬8’Ò½ð@ »g݆ŸÆ°q:ë¯ù!àxŒb�ÄÝC‹*äÍB 'õç/!Îél€æD¶ŸÈOj¾°&N:bÚ›~`kÇN·ûTÇ�„ 4Àl 5€f¤fhM•Õ4ÆVÊIí(ƒH|ë~¨¡¦N€äF5ÌÙ]QèÃ9:–/ê(hƒà_¯MP;�'Nˆˆ¿þ àû1ƾréÎ7ö‚F‘OJ,úožBm£—(·w0€@ŒÀ,&€€ˆC¼ Þ4Ö×HYÐaøêœ{ó�Ru×]u×]u×]u×]u×]u×]u×]u×]{åþÖ0% Xáé,ë{Á…v Ê6ëÏáÚ`‡d® Çnÿß�§€@i¹±x@%à€€PIhá@ îãYåŒSàFz8 ÿIVxÐ |`[Z@G^Lð·ÿþî~;/…™wþ¯»°ïÐEfˆcZŒbaK¸’ ?¯'¾@�„g3„!mà 4Ê‚¢™¡¾;ÿU�Ë]ÿ«´€ @À�¼Œ‘If00€ [¥‰¶}ØŒED–iÎ ºxÐ5Óî¾Äîû–O€=+g¥ÿ`ÃìakõG8’T�(LL-<é€6ŒTš"`ùÖÁVÊÄ€0 $U„ùd¸AfW¦¹øàüjUwÁü Ž@@ ˜­€ÁHóÀAø-sE=»ñi p³›ø‡�§` ¿‚3`3Ó�-‰*öìaãòà h†þ p‰�Î?þ ü I÷ØM 5” àÅÏ_ßá�¡ PX]é<H ê¿åþÁ }ÍéàÊ@<¯ùÏÿþ ÷ñom¼Ý¥Æñø@ pY°€@T(#QJ•$ø?Þ TT—àþ–ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®¼™Aš$lÔþ~ Ïçó?¯´†€dfá6R«2ꘘ^¿)nz×ì`Ê,mÖa9¬,º‹Il˜F´Ÿ@tcø™7~´¨H�G€ ,p  p @ëo)Éýþʶz$Ý�§‚Ù$é¦â¬ç”äxK¥fmnÈSÀºY¾%œcÄè pT8À Q/ûH ub°�{©#Á#_‚¶‚¶¸2C:ǧÛp2XbÍħi–cö"û ˆ�ª[:�Éß°@l⣬G¦õá÷´ÑÏ_ï�SïÖ€P@€@À€•¡ÿØ—G‡íø ˜I¸½<&/‰�‡Ç—3Ø‹f2ä#€Xu®ƒ IY¼p˜!_y؉Šoë¦0¿ã–ôa'-ÿ`}"^[^1‹ù~ÿ`€Èš€€èâüä¬ÇP™DÈ~Áó‡·Å”ì xZž€&\ŠÌ:ÆÎ6AÚ¢ÛMÒbÎ=€“"LËa `™ö·±¶ÀÚ¯^~àd%Ž�ó€Áwÿ@² )ûkòÒHH�#€M 5³¹#ÿØ0€@@ñ@€@0àþ#½Á å•Àø,²ð©8*�—àü,¼;E¡3óš]‹ølÁ¿Q°n$c;Úþ<¶ˆ&ü=Æ1¯ÿpÿï¸H†—hqCð€�8 `( Ti»Nè‘ý¿×·ìöm°Á0m¹Ó¾43÷àúö~fٶÞx&3ØÛfŸ @À«ë@³‹(˸©R«ûÿË å³¤0ÓÖHpØf5ëT¤?¾9Ú¾9hÙ‹À‘LŠZþ?ßø÷‡ ÀäÃp 1 �z% h’7þAžtJÁ$oü(,ÀÈÂMçIb_/à"8±y’`‡ÊßøHb6Eùø%3ÿìÇ€€×ã0ðœ°â÷‚¬.ÌjЉSÿÀ�æ¬PªE»é�B`˜ž€º+3Ñ&ù_Ø 9ÃB~€º%™´I¾@n¼€$÷‡þý1¥òÿ ”à “Ço+Š'ÿÃH· Êd!€ @ P qBé “Âêé�ÃñAt¿‡XÓ5&|0óðLg¦xâCËùÀÅD…¢Ëab$šÉáƒSÞh@�äý÷ø€œvç¸ü°Y#ë¢1&ãyÿÜ�&aæ1ÿžÿ@ËÀ*@²›KÒ{µ˜4Áõ/yïý `Ø\š%ŽieDÚM¼LÀÓêK&DŸø0K@½8kˆyLÿS®«” ”Γ¨bc»©÷ ;ØDÚ˜S6VrÇÎAö"ƒ.d¢BQ%ÿ¹,à­Á1[ ;‚#€ ðXYÛ¾eo� $G´,òkï‚ÂÌànðuo� $³"Œ‹n¾™ÿ©û¼ôËi—7s´öß{ëzê+ —‘ ·]3ö (UT&Êù!ñwï‚»²‹þoð+­ÿç÷ÿñµüâ ¤ËR–¿ý R„“,¢M_Ü H_ßà7 lAžBx<«ðÃÿøÁw‹ä­õñq�`ý:dÓíL¸ˆ¿S¯}À²nõ9<¿ ´ˆ-‚ðWƒ@€*A,” ŽÔû& …¬z˜´_€à?%vŠF�?ðp¸Â§c=A`´â½Z¦oË‚¤ëÔA �˜©­›Ç€FËCô¡IR÷ô¸gEš…•Á0Ï0_7…ȉ±ahв›M]˜-ÅiŠcÿÖ( éœ#&ml÷œ¥Õ çR ‘µ�?Ÿ‡ÅˆøS™l!GˆœA8¶Hë\ðy§™ÌøñÁ3G^³ÁåM�g~>¸& ²2Ž87€¦¾€x\h{eØDS²±ÿ¡8¬wÖ³ÿ]Àè$Æf’Õð¸=#aý=ÿ¡€HÀ›K¬ôš+k ¬j‰ï]šÅÕ¿Ê B|Å­ÿ ë‚aák�@ ¡À˜2ôë€ÄC²¨_᥵…X HZG pKœãž¬ÞÍ™ÿ<€ˆqå¥?¿ÐÖ ,G9êù·ž£À(îÜiÖ×P®ÜwÖ×áy‚–á0è2 ¥àé/ŸD™_Ï ¬Ø‰ÐÞ "Bk5 Èyã°@3H¥¾žâ’ÁÉ;t€\R)|¿ò›·Š~·»"þ —…ÈÈ„ ð€0PðPëÚ#üèä,ïywÄm7nÂ&ÓT`ÏòPŠt;ß@ß[U?¸³ kÀ›` Ÿ.ÏŠŠ%ÖxÈü×g“¢DQëž ÆàÈ6 €Q…ÞoÙ1O£��­éúïþAik]àÍ‘wïar\4@`L,„¨Ï¥sÿ\ð[Ì�Ù‘G¦Òç%¿W¿ãQfg¼ —…Ñ3Áv @€!–ï´`�ÕëÐàͰúï÷Bø¼à ­×Òèr–4ßÇå�Ü‘üóMŠz'™õáj%{sUH„µ×®6 Owf¾ —…ÈÈ€” €¬¬`00ø0³Á E¡p„�™Æ¨²/¸x$tñ4 .ln¥)ÿ<Ø�Vü•/êƒÁĹù…­ÿÀcÀçíÆYŽmjþƒ«tï‚UáP8@ô,`RÀq °×Ã錟 Ý9ÂT Ç.P"OýpMy˜š+£c‡*"Rö"œÊàBEn‘ÔX8¡•¸„E p0ȳâK@�Ÿ|¶ÜÙ�꯷õìÿ‡_oëÙÿËÂè‰å@TƒÀ€(=¢?K\ ï ™†cŸé4m9È.9“)~þ‚Òc1¤#�‡€Ä!DÙu€þëÛ;ÿÿˆe�—Xa÷|s>ÿÿâú¡bº Ÿ½¿žatÕýa±€Ï5Çßð#,Rp/h…€›ÿâýÏx@@!$àïÒFý¿T¦(�~æðN?ˆ° ./,`7š'Ô‡!§EúËX\‰¬#ü4� #ƒ"9·ÖµÏ,¥ù3¦9/ùš¯ÒîátDOÂä0 àÈ.$d�Ï®Iª1�| S³AÃÝñŒFÜP­d"··åpND"ø"D*ÁAï7 z%�Ž_tÝ ¨¤iù¥p:ÛöºûjÚïÀé¶öºù´¶»õÁ3øW@�p7‡ L 0Õ �fݱËGÇ€³m¸­sÏ<h›~`ù¶ãdVüÁómÆ Â¸dBA€˜(XpxË!É86Üω4JÒìÏ„š#´a€�‰ ecS }5°EùµüC¿ïUÿáy$Ä 4  �)T—> ±/MɾP´ÄUÚ˦$õÖ€¹OЀÄÁˆŽ !l7�4Ÿé�EÀ_µŸdÔ¶³îÀ“…É' Â@YBËx(ûü ©#×<›LÅsó‘NÞ0]~×p¡Û~`¸Ÿ5Ì…pÀ`  Ø.ÀD­Ë\�/ˆì ©�Y߀HÂ<ßv[qî›H8Ën!�"aplqw’n@ÿà³Üü…q0p@€ì"�¸PÕ0‹û õžŠè=OÊ%!Q¶ü¹~Þ �ÑU¿ ï5ÀN¼.DEˆ" /…€ž�Ö Oú ;±ïÿÅè\–ÿÓSÿ›Y„y¿ü-›~`�Ž›À_;òAøH§bÂR?ùüz~\ ¿?Õ ðÀ�b(‘a‚>s º çVH‰Í^)ö{÷P`yo]gÜ"ž­ëžš£Šè&~zÒžc@!à-ŸàA> @q‚oþà¿äçÄ«øjÚBí5ø<B!`¨”ɸä¿Àˆ³=Z‡T|‡*&žÔÏ“��V·m÷ß¿Qê×Û}õØ¿ƒÏÂ舟ˆ’vz.HDàÊÿ�,ô+ï ÷ð�âKü—•n'H’ÀkîW½n& Ø /€-’f© ñÀ„ CÀ€…‚"ÝÈŸ <®^|{JÜ–¹ëƒ%7ÿ&R�ýsç¾×q6š*×:þñžÜ@ÿ/ï)Âý®jl$oø&_ ’N‚z€`p® †zé¾q î—þ¼ù¡Ï÷ÿu¥ßhŒZ]öˆÁ8^DN6á~G‡a'€Lþ׬þØ [²î8Ròî$á�\ î÷›hKøxŠ“î —Â¸@ƒ„`À !`®j•o‡ýþëànHe½ÿá›ðn.%,`Hü¼øÜ\x-”–0NÁ@@ Ì8Àt—¿ß–]`L©®ü¬é•ïÉ"AQ†BCG�zÏu#�¼ýüN¾^tÿ‚eð»! ø×‚|á.×!î�I`,Ùî—¿ÖôB]ÄÀq–ÜjË�°RÛ�á‡ÿÂྠˆoá ŠudþeË œÎHŸ‘ô0ª%õßÛÀ{2q‹¡s<˜Ï®ÿ·�ÆÞ9ŽŒðœ€’„�€0 gtqçYÇc¦d�½/|8b¿wð÷@ Ð.Ašg4¤Àx óùø&?ŸÏï�Ç ¸È¾•ûÿÿþ�…,×£W—ø—¿‚Nc"±˜‹áþ0rN®¿ ®pŒ X‹  ”þÿÿÆúgÈžE�€R1º™Îä#€5 M*\Åö˜÷Éá’oF~@àÖÁt¦?ð 郎%×ZÈ”JQ†çÿývpZ Ñ›ë/Ða "@à@pÀ, ËÙp-· .ÆUº�d{÷?mæ{6ÜYanbhv ‘n/E½!šVeû÷=³lͳmü2à _Â_�€“B„ 8BÙ„&gf̓/p¾Ö‡‡Å³02÷—¾xpHxŸðã(Z€{$•˜“\Ÿïà¼Y®«þ¾™Ó�ó™ò"""~I¸&Daº™ÎìË.–¦1ÔT‹ê@0?è‹jíŽxÀ&@DŽd9»5uâu¹[÷Ûƒµ´ÛïàÁø@DÀ4,ð‡&ßÿÜ‚£ù@ŽÛ�iÏ`à )‰è>‡•ÁA2!ëú…_€ðþŠû YÞý�ÀÙ*VÈÛ›7‚€™û¢J’ª¯ÿᇦþOØ&$rºÈçveQ†ru,OÒKíê°gXT€?þ�O0 4Ç À˜{½™àW„U»€à&··ÀBC,{Ó<ˆ¥’ncb©¬ã³ {ð³ã!íë“�yÛi !&Xî_ùXžfoÌ @HW'c4Kß@Ü?< P°€@mÀ ð¾©`e¤RßÿY„‹TZÍýmȼ µÁW GI«ú¥€ÏHc_ÿa„�ë€v´Om)³`3Ò�[ GX•€˜Ïÿ� :~ ‘×™J¨œ7à��‡s’<~à(l÷í nœåX[¦wð€@X ‹ ! 3~@,(3À*·ù€›|¿Ç€8d~·ù2EÊ_ãI"¬iÐU˜Qà¼p"%mï“”'~Á$ ±qÅŸóC^�"&9äö =©^Ãîî~`þ‚€‚€€0² ¶å\°š™ÏlÍ�ú•]ÆÇ ûd3`e†×Îm˜×”½Îîãc�‚c?üˆ°‡€¯w@̧�+œ0_€ƒ&ä³†î  {ð&�•)\òìæ‚H ±™qÅŸóI)_Ž»¿ì@ 8  À :Ð! m1¿B#¸ˆHÀÏH ÂËX„‚TÜèOJ‘ï@›!"”½ŒÎà|‘ üÀgjÔ S³>óeÝ0ˆ‹ÿà® HÃ+ ªIks›Ä�öŒ™1çöë‚*á ´–ü �ƒA€Ð�K &[•²&âiàaìlÒ²6ãkàaÁ1Ÿ“Ȳ,!€dÍÉMºÀ©ea‰%lb|oú¯ªµ °ôÞ�"‚¬¢ }à`Dêg§8Þul""qk_ý Î¶„U•i'€¡Â€'Xà7<ˆ^÷õ€#ßpy@»jEÀ«§Î{c^R÷;»ÀðH{ßÖgJ¸< .Û°;+sžÌn§/qR”Ì/ÿðV^ Fíñ(ÅHð '?®ï€€á`€@€ !–Ìa[k EÖÃÂI±¸+í¡é‘|0à—á<Ø*¬7­ü™^E·à[á¿ùŸ"‘¹&à"ú¯ïÿ¥� ÓR$|" ìÄǸ⊢,OgÖöm¶¨Œ7CT©IÐ^Àÿ5'wx¿ëF £"Ô‰ò{ð`ÿÐ$€<…°ÁþàGnËþºYú%Y ÒtW¿ &E,?ê�Šü€w¿,ý´Çiº›ïÀ2_ÿÁYðDP´§§9ßÿp@pæ}ÿÿÿþ~úÿþyø$ à€ .  ·nÙ³fÍÛ·lÙ¿ÿÃrþ ð 1C›ƒRã Â@€’AË+€›5¢¶\áR½póf´wÉG�›êg‘‘cÈ4œ<i= �‚è\7OÍç8¿ìL"#½-{늒¹JWÿö€Jø ¸Â¨éö}ocêªÇİà,ì H›*ï`ž(©ø��•îæ?0æxDó™ÇÂ7±Vw?7‚_Z©”žƒóoe‘³óxPO©š\Í.÷ÀdÈÙ:Õ°ôO‹�š—\ïÀ rÞihV¥‚à‡€ ÀÊBb“Š ébý¬NÁ]ªÄ“Àx7Ô]Ø#ã ]�œÄÇ7ùý¦B°r5ƒ|²à€ß‘»`ä Ý;çà˜Ï¯þ�gú£˜¹Äô£PÛàFm9­:áø›'hÒpåðt‰ˆ „<ð0=î®Bë˜2œ”ÿP`A³ÌŠÅ¾p†• Q¿¹ø _ ¸&s…!Y•?ä9‘“�û¹ÿÃ$&‰Uß ¡+y�ß_þ€ÁAž…E(!à7ß¾ Ÿ’D6@qàq™�¤¿‹ÀM_/ÿ\=¾|0¹…�p,€ Àd…@ G"ŒÖN¾Ç¿@„ìy!�Pÿ÷@Í|�5û†—q¦ˆÜBJ6¸U©óˆïÖ•©%.üýúðRÀX{¶‰ß,ÛiŸñCe¨ç°÷¢fû6-çòƒ¯¯0_ úd#ÁàBx› h„ì«{ûíÓŠ„HͲHì8¾Á¶ÿÀàģŮu“ˆ-ÿ J³zB%ñ°¶09ÂÒ�ŒTì—·ÃùôÄ%Q³Pݬ‹ÍúþC¯¯�ÂòàMHP\  %&ˆ4Ân<šT»Ø×U‘/ýÐÀü65”ÃGü=ßðtÍÈ‘¿ÿý ýz“×üÇÚV#¿ûÇð?�ÎÍ£~}Âx¸ýù2€vh¼›ó臄ë÷ÊÿCñS‚`»! Àx`À€¤ànÁ@`4 šöˆÍ?µƒ~áI�3Pµÿpyë8v”Ÿîx”Ï÷ümÃSöÛþðp„ö™Åo«Ã‰`-³ûÖz“8[¾¯*‚òÞ{}_ A0ÁG „è¡Éït­d cÚö9þ±ÛðòÜüÛ¯ÿÈ0à˜/h��ø¡ŒÂ8³z^ýÿáÈ?8Ãqoe؇þ»$’8± úC¯Ÿÿø0fBsè¾’æßŸ`*ê80 vIsoϰ¹+�¬UHdp´%£iÛµ" “1�›oÿ¼©7¢jE_ü!1B]³þx0á1Cyøÿž 8&ç`‡!]¬¾2`ŸA%ÈÇã¿7sÔY_)¿À"<¼@õ5xüÎ^ô¢O¿/ÂädãÁ…cÅ…„€ ˜†øVâߊ‚ÎÔ×x˜ÄoÀÓë‡9À/4¾÷ÐË€ y‘>ÎøFFØ&à ä8>üÀàGoOLtt #À É0úM~ûÀ Ч?i—ýì.DGÁ°Â`ø2@3áå×÷|ß­¼Péä© “žû5»0ÓíþÃ0*Tõ™2ûðuyìólϦkíOü£ã½ÇþÊMm¿Ÿ˜#Ê{þU~~{¿ÿå&³3|þ `•}sðl›rVÝp»!@^A`R5‡ÎáЮ÷¤Ý,¦êçÜüb’*E*ÿÿíÁĹ©Îo`À �_ÏäÐÔ½tðÓ|зãÇÅ:Ñ­û`qÌ\m~ù›w¢}($áøøTZ½Sš½á‚Uè.ˆGðÐ €D x�@ Ca@ô�&ñÂÂ÷9žþ, 9Ùò»¬8èÉ)¨ü¶{æ›ÈJö%?2ÆñÔ†%ù@?�Vòžþº\ë&cêXè+òžþºA•/1½q¯2øW4,€J˜0)ÂâÀˆÃð›G¥gø7ê�–¡ª)ßÌò+ÕªpíþÁÀ“oî2Þ�Â~þâ'8Kqz"ú€¢{…;ó¼zöeŸ¿ö€¢=Šý·‚�O3Ë?~йp?ˆŸÃ#ÓÙÍ”§ú|u�¿ûÿúðé÷~ð÷Á3^Ø „ ž¦@!àwávÙ œþ �p"9À×€ZE¿~uÀìÒ.{üú. �À.‚\d¨÷�¯Âú&À ¨;€q€) e4‡wüÀe¥,–ÃX/ýàÀ9G€©-ûó­0ÖÒ,ýùÔ�b@’ ,Àªóº�ûì²Z�jÿ˜2ü¯¨Øõ�À{%ï‹ÀÂaëÁ(^Ñ0`�pœàÀ8ÀÕn*`å+ÏÇ}óÁéG€²/÷çZ`V‘¿:’| õyÚóù–èù§ßû†_§ï‚P½¢b`ðÀq€§Ú˜�\Ef£tZÿƒ\œwk"ÿýu€‘úlí¬‹»ú„y°ˆ0lùOu‡yйn>hqöá‚`½¢t ÄBÆY£ò¡ˆôàô³§¶÷�ÂΤIî9Ò6Î×p¹âBކXZQösTïþ<§5Ø}ã�Ô ú½ñ~CøUÑ/÷çA‡Á{dƒ�,C¢B"‡ôPêÊÑ]ì¸Ãíœê?md[ÿ×\~md[ßë¨\�ठ¾`•6ÔýÐzY¾Zo¶ðjUvEûõëÀîÑþº 8& ÈÉà(I�„ÆN;€ôk @'Ë÷ÃÐz#�=,ÿ®?à•|o�BÁ xÂõ_ßÿàëí I${+Î’{ÿ…Þ˜%`l8'ÙÿWÂN2îH`2` ¦Ü€ÚZ'CøPÔ~—¾/,ëÉ~ø~;¿Á2ø\‰¼: EÁ€2Ã,,ÐúHáÞØÕ‘àwë6ÈLXùGð½Ϲ/ýx(|ZAš«4¤Àx$üÏçóðH?ŸÏçàŒþÁ¸ °ã›€ âäl¾ÿõâ{3ÿƒºˆÁ…¬X1û°› òÑÒ#äž þCL$D½ä)¦½°.³ðÂ"–A ,0 () Ì„Z!®º�ìÁX ›}`ìÑŠºê±€†‰€ ݾ¦»îús`9†d,‡™Ö€íˆ"2 ½=ð OAйl éçý×O`B¾ò}s·á$@4€³âMƒÁˆ S…!jõ-{^4€ …ŽØÈª@Ö"Vó¿ªØ{]@S雾à\úð-Äâ¤�–²…þØíj¶Ÿxòên qÒθxîšP¤0Q~Óëêö“�„‰_õÀ¨@l3#>Aû¯Ð&HuÀ.¼Vƒ .=J@e ~gàFÛ_ Q ;à ¶„(1À[‡¯Ä#?øîðéðˆ±âiN¨ßÈ�%Àp±ÎßÐ&ý0 «`*`ׄ&{ü÷ñ˜¯@@©´Q%Àk”öÁô€d7iD ß¿°�qfaÎ%`ä”@àWý3üîú�x `&"^ò°Ø‚# °[Ï¿÷ÀÊz UË`gI™\<°!¿ûü $RA@¦ƒ°˜\Òð=%?¨½$¶jÕ0˜J$N„Ãk|� ´pó[W©˜>îü¹ð¦‘X²\ÀZÄâ¤�µ²•þ€_nû´ ¨íj—=îâ‚K.1"Ó–„Å$€€¨æ�hFZ`”š´� * ’ü$’à4!–Œe.­¡±¥@Á»/Â@@Ép#3Þïÿ ÑA ¬FÀPñMï@A3bî¤Ñí"Sÿ7×ïºm�±Ör�®õ^ R%FíápcÀà€Ø  ¢%#uÔü˜ ¹™i 3"ÿ°ˆ•†ë÷ð:0Ù™y ±ü2*ýüÔñóÄבüÍÿø¼;à‘cCnuBûÔ}nšd-ã½ü‰\ŒüL‰d¢âÀ€�rÊ švAå£ÝtÐËAqW¬ƒÉ£ÝtàÏA�Cy«ÔÏýÝßAÓ 2 Q/k”¾ø˜¬ê6¸ñ-¦&ÂÝ�e¡7¾’ý:åòóà8a@¦�€8‘¬Ã2`׮ݰ’JpZ¿lÃ:&…£Õ¾­´¢€&ßÓ?ww˓Ƽ”a%‹{Ë›˜!UI`ׯ¯ÜgÀn<ÛTÒ_ôÒYÁ„ Ýle¿@Ý€ &`Óá`m›é°ír}W­úxí¥pž€¥€(v6j| ¦`å)~«¶}<;JPO?Â32.îåÿA¨~û´ ç …žHŸõÜ ypÎ…îîp™„µ%„­÷¿°3€…»IAY Ï€? €4 ´$&{·À]¨¢€Ö:¯[îú t‰�»õo�v ~Žm}^·3¹™�ãÿÃGÍrD#gÿùéçÇþx@ 0 H C~~o4ø?ïÓÇí{T 8"3ûùa`PÃ'� ƒ&aETž�¿¿ô›Öwý�êì;…˜î£ÿ�@rEY¡@G¨�ÿþ $� À@ ` àZp"®\aëíB†Â÷8o¿@ ˆvÌ!¾›†Šàâ¬Qi DßÓ7qw/ôÀ›÷ݼçÀ2�võ|ÇýÜž\ˆ@ ç†�%Ä4KýD�Rh[_{û7×ï¡p!àp@l…Ž2 ¨žh½÷à˜""RÙèþúÂP03"PHOËðL¨•€5¿�à÷ÙŽüøjøºÓ²IŸfI!4CýþŸ] >€ @3Ìç¶ŽIkÆþìb¶fI4öŒ1¿Œ”1ªç\ü™èŸIÞ£RJ挑 ÿ—_ì2È`kzC?ï * õSSíFßÙÕÝãþwH€@%�@ÁPìÈäÀ€"÷è±C,¡tQ™‚ üÌúà`½ú–83NF“?ÒŸ`¶ۨuªoëxTØTßÀq„ò8š–jÀ„a>ÅÒïRßÏçà”Ī|ô™ÇÀZ é�JΆù_ða€X13k]Î)¨ÖÈ%Ay*¿ísàH˜ùQkñ§óÀ€ecæRìY�Ñ�•½””Ç?à@x xP@ ‹1¬-Ak ÄÀ.¼Í“Œ´i;' UoTà»Ú€TXLð;÷�²hÉ£IØÀœªË*$ýí;À+0ºAžÉE(!à—Á;õ¨Â@&ä[|¯€ k„ží˜ö”^Ü!zoÈQ3±(ð5?s‚y¿p2Ÿÿ>3Íú¶lƒf /Ä9òƒdo!onõÁJ”¿¿ÝÿÀ…rWîðÁ#ò@š E€ ŠÀHw"™ ü59»D\X*ƒ@�@`ï«ÞÀ™½…sAPøÿ�¸¨”˜tägü« *ßÇBß�x%k˜È�ƒù@¨#õþ€>Êš?Ÿàµ×}±lRzŸ��ë6ár/ H à àªuæ�°%€U—�n5ÄB{ö:„D2LÝÿÿ°3úOÃí=ûžøOSßÛµoÂú&Pp0`%C™Ü*\ÅûÀOŸÒWa ®ü=ш#|üïÀ&‡+–+1›÷ÍnÛ ` �éà­qtøªóæØÛv\ë@ú%^|Ú!‚ÀˆŒ�˜eX\Ü�¡ñäõ„lrÍë°8�e‚Sù‚ôœ[5?ï: ,„Œ‰ÿ´?ÀÈ€²¹óìq~Åମ>}šýŸØÓ¥àˆvh`6A�x‚ž³ÿ|àåmràÝ)\EÀI kõŸ0¯wæ�Y®…EZ€6xSo{ç¿4Ùo|…ÑáòAB@È<¡×€Í1´ÈAù·þ?›x,ĵ1gÊðd¾ý挚è!U ÷ƒÅ!bœb�°À89ÄG17R?€xË ìøÂÍžÆþ¡€Q3TçðœÔÓÿ{â´ØÔoè:„a‘zö’6É%©ƒ(|ý?à‰}|w‚ð00 ‡€ L5!¡K+mþ€–qôø`ï@�ô€~vÜêå\í¸(®å�º!Î À â … ^4E&Ð`t1º¥1Mžwå±Ï�È?‡žÿ("+P&Ü�‚ÓSZÁÖ²´‹6™]ÝýÚ 7HE²c’¦: ò޽ïÎ-F9%lê¿÷�VXëÿÞ„à¨F ª‘dsÃÝ–€°28­5†³à �Na©ö >P9ÏTúLÁúú€�™à÷YêP½�˜ ! z`!À’�"À ¬ø­ß|" J ‹~ÈHË�¯QÏýàH «p.Äa%G*Ư»m"±©ì®HMþ�­›à~%€}¨¾Jf–Zü•mëžO7m òQž“&€! ¤O}ôv'ç7Ö…°4 ÀÀ°. ‹ 5ôcš˜6ÅBÿàcÌ^«Ž©ú7×ïôz"™"0uœÒ­Oð¾Wün$Lì÷Á±À÷Áø_K €¤àà H$ß³il§‚SXP…ÓýpvŒ‹æuè @#Ð=lj%ÒÛþ°X#1Y$‹Uß{üsö¿]þ¡¼à:2ª}íC’±À£ò2;þ Bõ'0“C‚@ éü;ý€ ‹ú²TXe@#À1ògÝÞp|ªn{x\„"àðƒ@B$!”K‡Ä\7×++høà$ž�.@ª7ƒ“�vÕŸñ0æßçÖOÑô¼ô¬â·Ã˜s[óþ&�½Sáß‚EáyN<0ˆ‰¨/xØPÃz°z”ÝÖkÐß„!¹Ý˜²ò¼ /ŽØØEÅŒ×yŽ“†X�–ZßÝ/Ô(øiÇÔÅzÿ‚Eãx�p B!ƃҊNÚÞÊŠ¸#¬o§€÷�¯P¹EØ(G(Œ„B1C¯ ¯ÙÆóÕgî¸@kÿP05Þ§ÎFKÕ%Ôñ|ÿ~`¨Étàûá÷ø%^L„ø R8� „ãÂ�à wzÇÏRsà‡= õ¹,fñÁÔú?q| Ôc×Þ¯ÀÒ³+ß e›óé4c×Þ¯ÀÒ™•ï†:4gçüM ‘> ÂÄá–©™éî¿}Vþÿ`j=“×þ‹5ÿ‚`»#~ �@)`œ€˜`lx#Þ ¿�õ€ºÍi�é8Ì@m­ûþ\v’)÷ü�U °ÈNbßÔ5øûÕð Ìžê@!àï‚WïÉ`03˜àçh´i½ÿÍ ¹ Àûဠ ‘æí&ïð<ì~2+½zn:_wZèèçNxuÀ ¯©žÿø5ê�÷ý ‘Ñ‚xסlxÅœ @0pn\lˆï�Õ6쉽ÿ¾|áNõß°>BD‘{¯ü¹�BpÏ�-¥šâ!¼,Bl Ø0Á{€ÂÜG�óïJ/¢å6�RàGúq‚51¿iÙïÈ”#,3»ì÷ð¹ø$„˜#†$…S€ Â1ÖÉÈwÃ:öØàpU}ž7P÷³ø77NOÍÀáÁ[; <zd+$mû´nW¬C+žÇðîN«¤…ÙNÇ�hEàÚŒ‚÷Üh jï;…±o$ÞÈè·ðér¸×ïþø>Œ¥È ¬|¥ˆ ¬|.Bx`2qcBÆ`ŒxÒMïôÁ¥ñ›çŒD´Üx÷ø³i�‘?ÎŽ€™)Ÿ„ ¹€!$G‰ø@�Ëþ¥q)@RSÎþL’ƒ ÿ|/…¼ Á 0ðà—’N1™ \  Ýe!߀U¢Åûð .,gžL„ø@6â`€n\¸áÈÚòôzöÈ‘\lï€ä£®üƒñüÿÿÅ#Ívô?ay‡aW­d+°=d±C «°"aš€)¸jܗƇ6fÀÔ~y…ZOÀ�67Ì/7X CA°§ …´Ñq¾ú§E~þG|oë¾”€<’üìtÙÿ—øzü¤{àăÀT+¡Ì-�e©I×SÎ’£à[ãá üƒ{ÔÉŸò¢¬ ô‚ ›üØ_Õ‚7ù¡r�‹Ð\Á €BøUÆ£@yæžø€Œy¥�Úý¡Ù~¾:ÈÛü�pÈi¾Ïa7ŽOóMøK@7‚`„*8R÷1Žo@k=G2i?˃ú›{¯9KÕg7g)x%xh@HhYŽC;7Ç‹¸ý`“ÿþ›ÿoüF;à�úð Ì_Ašî4¤Àxüþ?Ÿ‡ ÿçŽ x${°ÊÄ`÷'  p„@;B‚Þ Ày GÚ¡‰6¹l` —€ñ !DÖR$±ÈàÀ —¸…Ä.!q ˆRäB⸅Ä.gþ>¨x4YéÀ< £Ž4Ñá)0„ˆøV!ºã ?©\" ®‚3–JXÀá�߇öÑ;i´ 5æÄ˜>‰‹lÍ Ó0´òɊņÀ†%%0@€…öCÜò– Á”>?Y‘–WËr-D¹Ù·Ùá{!0üøB!lú()KÓùÍ]Ûðÿäóí[Ñžwü-‚àB0M�Æ£H?,öÜÌájeÏOä›lÜ`¾ç~ç9¢nsDØ[ Ã8È‚®dô¡ÿÈfvu¤Ì "íg¡Ç˜ÑwÂØVÐAð%AQ‰èßÇwÙaÂŽ1s<äNvüâÂX. †BP�Dˆ˯Ü@)x%ùþ4FÓÜ �RDÓy_>Á£ø¯Ÿbê@ à"Lj0˜ÙòÜ}[ÿ*^èÌè[ Àkb#c­B®gœˆS¬ì4ö‚ªLßûž;¢û,™c2Úg ì?åÂØ^“LFž „~é¯ð:$ÆRõD弤§‹skö8\„KÀH@ý☡Îæ§´ ãÿ›¤X£øwäïÎç{álˆÂ##®; ³)LóˆCa•qwÓÔ¾ËyPÀ8’Žù‚I3¾~Ä[=ó"ÝoŸÂÙ‡±‘!T=©ë7«¯É "'ùŒ…$¯óý^k<¿¹¬òüDF eà �'"ðþAKþ˜à=o€j Ÿ-@!àï‡×××ÇuP¹åEY´+—›B¹p¶^U?[÷ç&یͷNÃpGš‚®«¥ˆ×O6 ?ÓÌ칎„tšdÖjbfIª%Âä#\Ä�€@ƒäJ ¸äŽç™çlÛ;Á飲ÀC\­gýþ;,åycÿÿ p!áWp»gkþ¾ðýI¯9 ç¡Ôk†óÐê5ál°3Ĉ�*àÔc;ŽJ}aé5T×ðo–¦_ƒO,¥Ëø[0Á ¦ ¸QÜ€tEi`E!´úîÈ"’YžYžÁ ¤˜*�ÉfÍO_�£Ò7ç36�Ôn\|n[Œø[ÁfiB¨:èû™|2%<š[Ôušy¬uš¡p¤3’P¨MŒ°â‘œßŸç÷óˆ �éKðò)~ Âzý�ùçGGÓî}~ùçKõûÿžt¿_¿ç�/ÓïùçKõûü‘ÊA›/4¤ÀxÙâÀEaàÄ4û”\‰EßþÀ¢€ É–ƒ—DNÿë4Éd©ßú: ÚÀi›D›°aBœîg¿ËJ1ª6ÿàABÞOz™ÿŠê¬;Àm@�2áî¢[ÿHÄÜB™n¶éò”á'1‚XÌ5H(§ñö•ºa¨ ª‡Ž+¯´�u´æC†ØoáÙ¦ }½àY“"+ �MÌ`s†˜uǤ>˜p@¸`� EÖ¯u!ߟòø€œà|ªoðë OKdAœ%ƒl¦ýa!q&ÁÙ^è& SÓÇ:1LD:Ñ'†€Åkˆ~HÓÄŠÔ3ç|à }†í�Ô%ðÚ«ùâ÷ŸÕçN¾~°"ÑNµ—h'ù)±[`úÿ ¢Öp—z4s'œv0 Á£/'€= œÂäóm±�8@Ø�ÍîògÿªÝa€ ‰26›[¾��:xõ¬ãëü ¬žî* «LߟÀÇ2r2·ø(X@Ûa — ‡ê´3€Ç� M-Ôº¬n§¡Ô‹lŸ—Ía�Rå»úOBÐ?¢;ÐJæ¥ðXnÖ‡Á&™Dµ\ € .ÿLoЂB �⣬+O a�Ô!©b(ïŽIàÆ¬j”ÏýW[¥À&T¾è t’tÒÁ:‚uc«¢§y¶á#1XVt/¶Ùüþ?Ÿ‡<þË÷þ~^ƒOç÷_ÿò߃€Fgyÿ›pÀÄÄÁa†>ñ¿Á‚‰-ÑzP 2gBÐEîûü'xNð¹—‰Ýá‚6‚j«þoðá1V+¤ð`� ÞÊÿ›þ`î·ô0L SÁ9ëŽt´h ë›«óø ’þ†ÕK¹iJ8ÒqðÓ‹G÷Zþ‚ �!\ ·9ý<&Åa6GÇø€ŸœZ'ƒ0PâÝËàNh"•„á8²WÊW½ ¹¨RF;ã †.OHvKµÐätж©~q¨†í‰/2+æRtž~$¼Ä×ÌÓ!§Içó5çqþ?ÒuUàªGL­Ü àià ÊÛ«ü<²’sðP ¬Rsšoð€�æ¨ ,O.àFæDtðè°.—p#ñ¢©<* `>ÓÉŸõÕ\Pv$JZN½ €6ÁÜ´Öß¾pàOJ�gh#›Áˆ++oA e Œ€ñ.3¤ðAž€"ryˆ'ÓÓÁÒO@o'åða!�ð�!¦tC¢½pd�Á§/ûèÁšÏÀ)‡¿êçõ„´‚ZYŒÁÇ!¼šÛ]߯OŠOÀÆÜ¯ï¨¹bº?'à¤üþ-ä�ïÁøº¯A›S=!à`ßÿþLø•·KÊ[‡_¦Í×:À6Š6šÏýmþMë’;‰°Xð›ôÙÏw‘ýØ"0˜û™Ñý÷„` P  s`Úcs„òR(?vyM9>ýös9"²¼ÕŒp~:iÈäýs>Öö6í¡‚#~_ðÃÿ¾‡nÎ6–+".:>Z_PǧÖ#¿“餘‹í’K’å¡Ä†Y€� €@XÀz,…]^W„4èLè!M~!@)g€,èĵ}‰z$Ãá@äšüc€ x°fÿÓ µZÿ Õ>îÃ�3¾‡Èû¾E'0?|Á„‘ûà 0 €°!Å,x`¬%6à 4úÐ šž†£U‚¡"ßa9‘9™‡UÂ+ª‚ÇŠ<:íir>ßÀ*<дÝT‡ë?º€÷˜TK™F÷þ‚(tzƽƒ7Nºø ŽÞW T©äà´•Ûü`Z]h%Mü *Vú�²»€ Ò¦ð%mý3ÿSö Dü €�Aµ W�÷À1l‘¬š|s ŒÀ…øÊc¤êÁà,_Œá¶0%b- V<ÃX Û «¸ÂÓñ'_{ tƒ‘ðm+ß|øxaÊQ=VšQÞ — ýúÿ„?l/g2�OóD{#��ùê}àÀ 69Út3mþÇ‘ïa­ Šöx9ß:æ*ëÿA¯‚!βáù¾£°Agfm¤�Á†üW^Zñ ¯ø „€p0o—FFÅ#»PÚ¢¢@òßüšCe#Å3»bÙh«*@”ïÁüÍOÕuªÇ›cÛAUiÕ¿?@h^+{P—2­Gþ| £ /˜í÷„§N¾ ®9¹ ŠCß@€p(@ {FijƒºCbÊi. GŒÔ\§¨ Zþ=�Ô�ììñ�±¦4²áš�Ôö–¯éŸùý‚Ñ8Sºk‡Çýúòš_Ïó/»?÷~ ùüþZÿÿ—¼ÅX¥WëÇRìñýÀ!WØ'Ï«·ÿÀñúSru„Tÿ€NÔ(,J%6®ûä´oØá€€0 @` @¬nºJ ðµ V¾T’¸%ON´=�XE]% c�­.T’Ê`7ã|¿=ÌÏêµÔØvD+l¡.kQªÿÖQ‘–Ðq¶ðatr8ç¾¢à:Þ3§Mûa@€@8P€@  ]Â-)‹ÖßÀÙŒÞ`ê%À ¸G¡zœÐÖþ c adIؘ&?™ê¿ú�ðð í}¹pwÏ…i…iøä28‰�rV¨~ƒ3Œºò¶C˜¨«ªz­c„ïèß~Ï]þ›Ã7'XEßó} Sh”Ú¹í-y½Ú hkšÕj¶ïð€Ð)ãÀQ¤©ŽP~©€=n|{M‘È{§ã¢¼à�4Œ¦ ^©€ cŸÈØ–gEOÇD1.,G×KS?×\ìÀ›�kzGÞ Lè{.}àV@yl1b›ð@Tà Q Pœ©‚°¶þðàÊš�0wíoïž'?ŸÏÁ!•Õkþ�a�ÓšÓ£oà Àñ,d�O³¿~ø8ЀÀC>üf£\Ê·›ú¢>½ Ißï6®ºûàà*£_jŸT5®ï¿à£›¡Ì°üÂ'`ÀŽÀâµU¤pÕ>Þ/s»ìN è^¤x¶þŠ×rH -�·€Êí;¾²2‚õ) 5®ý3ÿ_°Z'àJ¢³6Òï˱Ù‚) �ù(d× ò©à~@g] jL¬_?Ÿ‚cu5_ô6=€�vœÝ=¯釦TÐyf§]þ~Ø|+ÔœæYªÿÀ€@ 8 L ƒ†À¨”Ïd­@*>Ô«ø±ß¿Á3‰ñ¤-¬ 'U¨?À q¿ækõZ�V<˜.bŽžBµ÷¼5iÓ�€ØWÞ³§3Û¿»HoØ]^eƒžc„uæ~¦÷@€BHЀI ²½:w�(²†iä`!aµú`jKøÊôéÚšå –pg‹KÃ'ûp-mý; fÃ(á1Úâg¿þÿýï‡ß|Ÿ[AŸqE4H!àeõàœ�Bü80nOÌYÝ qÚn[#óYblg™x·Qvé½ÿÀ¹e¦é¯ÿ ´GàO¦ @*a±ø-H)"4žý†$:¹~�Ï ï ‘vÓÏÅ ¹�6»Ì=_^ X7¶dn“‚>ÂÚï·üÑð/`Ù¸â­Ì3 É€ºáç‰B]oÀ²u.½…3†'ð¹× 6�(Ì$FƒÂ.Ú4‡><‘oÍÂv О*7þì¼^ŽÇø°gï|Áî{Â`Q „ã¥ʜڛÅwø�èë›ëÁ#é|-€Š`tÃá1AQ;Q³{´q» €<ó�ç"ø…<­þzÜ6Ï©1WNø¯}'"ý<œfc<ÿ0ëáðCŽ:¸ûçÿ ‘²l<‰‡caç†$ Õp¬�ro~ËE¦¸$°´½�e†Ó´TÞ¿ÈôÉÈÙþÑK¤üýY“¥Ý?`.§È[±P|0‡Pˆ-Sh3ýÿ*€\òÉG»Y©#ÿ˜@Vbÿœw1Ƙ_ÓÇŒf'Ò5Ó´¼6/{Úê{Ôö Ÿ¦|ÞA ½8Þää3£§èEé÷Í÷ß Û´ûæûð¶$�$�\>0ØTjx$2ÖU} €&Àyr/;üGvÐhÇu—Ä‹7›„F¡8dA¯Z»‡›CXçÁÐ1YFyþò45‰áP Vz3Çü.›&pR…�$@ÅÞ b92£]ú õlŽCï~(ªÍ’wÁáˆâ#Q zÀFˆ#ØÔ}e÷…l±+£Ÿly€l"ñîI·Í¬`+ç6¾{’mókÀ‹¯óÂX@ž @¢�à`Pzž¹Òÿíú=“{_}À°¸þ2°Ê¬Nûؤ‡­7ßé/�÷iQV|VÊÞOR}*xsŠöñÏná®wïß•Øï�<&àÒOSmºçÜÁ�g�þ¾ Õ_Gð9Éuóx\„o80A  pÆÂ£‰o )ZŸ��éÐýXã㇄ƒW›ƒ�²ìíÞúÐ:q›ælØxh›0Àit|.ä&lƒ@S�, ÀSð}âûû·ÂvAÚR¹o°Ž6T¦�/ðwÂÂÆmãßû®Ï6û,"Å á„ÃÀd�O ‹ŒgŸP±'‘? ‹†3Î>B8,�°ž ,=¢#ú§¼  ôÒ[µ`¸ Pñ½»Çà&XxÞ£¼x'/ù°Z¥ „x Âh=aQ!ÂÊ”Ÿÿßýf=;¿¶7O¼ÿ‚…àøƒŸ�tN^¸'b@……€‚ ’yM@Vóÿ�ÅÝóð†ï·ÂØÂ`Ìœ hAÁ¬ì˸7ÜšoÎü.‘ øð`0ãà¡s®£oá€ÂÃR¢© 9`t”/®X…³c×ÍáldÜLlƒeBæzJ3_ �‰`&àyøF¢.ÿÍý�gsë 0×Àh\ˆ„âB`Æ ¢a´>ó/„Ú{šGÂØvhË‹xKC“- �IBðÔ¹°Lþ¼-‚ˆ @¸Bǰ Jy¼ gÔÌïúuí�µð¶Ü�ô‹Žç霎CûGüîp¬Ë�½·ëèY÷ð¶ƒðb>Ø 77sž÷ð¶°b> A· ð«�ß/—Ýýø%^;‡€€@•þ"µ= `E‚(ú%I0$=NÞ¤¿ð µsÌ�BäM`¢L cÜAS7W3Õ�5ÜÑÔÞc~³vÿø(\ï4Ÿ’@!àeõàœL2 4Ínúþ?=»¶Â¹$°nùMÿ\üÚþS×? ‘2pÐ0Pà5p4 ÍÿÀ­Ë½lG ~¬ïÞÛî7æÎ~˜óMÃqŒ|e4|7–�H"-øN.€/€÷k;浇ßÒù×}ó�z€þà?!h[ @“a�Û Ù¿ëõxt¢¢¯ä"_^ É GHô×ár“ƒ#ˆ„Žx! ¶ŸáÈa§P¶’¸´EË3KÂeÀmk'�¾ ÉX! ^‰Ö,ÿ g D@x( ’��ï€/"”nþ|Ί@qÛ€Â4š‘üܽìã6-C<ܽ셲‰p 0€.EÄŒ·ÿ}Á/Í�¼¡zo`�|.B'Àž � † â¡@ºö¯¾¾OP0I¦`2�ḔØÞÿ€?K†“ív7¿Âé�œ�D(± ÀP}»I’ëÌøÓëã§&@wªÿ‘î°Ø0ax‡è×}ïJ7Á2ø\„k‰C…Ä”!…B'_.Ѥ^wçsçýx'j{Ž�3P }Œ�àWwÔ Sæ|�ü�à?`ð,àÓŒúʰv 6?ê ”çÀw·À*ô–³ì&X ·Ô¬÷‚…àøfA›•4¤Âœðø@GP0;#FØÀÍüf«ÿøXŽ‚)kƒtSNþYŠ�’Käði›�tYÛ€*üÂÖöÇeá�ÀÀHÜ~`"!-`´¿÷ûúÕ¡cß@ ýœ%s ™’ €~bU#¡IÑÑ ø ;˜©ó¨ÙYÕÎ�sÿè2p5Œ6wG¡¡jtñÃe”þ¥ ã_|½gtz=o¼pN³ŸÀ]¤°ó Ëá' `0ˆ u€h‹p«òë‡�š]±ƒd›×í旅½JboÐe‹ØtY³ï’J•B>¶ ÂÃTKã €4?B¿ôâRçäüYÓŸ¹À X–z,l¿A€²[úq)¿¿çÅçS®ð8 þ,l¯ô¿„“á§GF½9æ”O½àüf5wàˆ&kübU"£“£¦X¡‚ø~#”„7þ�ðÀ×õjS™�ÿwïðøÁü�Lœ mÎŒ“¤Ë±±õ¥Î� úgÕ®l2Ë!ï÷6N2i«ƒø Zž8ß°©… Nà X` d8�:YàN�&¶aBˆÿ€ì›|@è{ Kó<ég�9§ß¹±FÀs#`Bâ�Qe@M„,|ôú3Ìçþ�a+¿,Èߘ Z4÷²3qÿéá—ë27æŠÆ„ÜÿX�«<8X÷ºÿ�¬NÔ…‚†t™.¿34óÍ0�ü´nøVnöÞ~[Mù€�¥£O|*3köðËòÌ�ù¾…-saQ›_·„ < l$ɰBÒ�Q$OX�«<8X÷º²l´£TIdf.0OJxÄíHX(fÉ’èdf.0ORBÿã§-;!+¯Åôb™žg?ô ¾ï,Èߘ Þ†•.õûxeøª¢¿0@@)„ƒ”_�¬"“�¸PWK–ëþ™ŽNcáÀ��9׿ÓÏ4Ò<ß(�ð¨ÍÃóxeùm7æ(�ðˆÍƒóxeúÚoÍð@R‰ßŒÚ?7„ 6H@ ° vM‚> t=ÓÀÜŒ±<(‘®tdØ#âCÝæFÀ…¥¢Êž±;Vxp°’Ì�� Š=E•)ç³<óÿ@²~*ì¯Ì�oCJ“0èïÏß]>.¨¯Ì A! åàI�­y tÉgÀÓ1ÉÌ|8ó§:üÃÓÏ4Ò<ÁÁKDžõfÑÿíxeùm7æ(�ðˆÍ£óy…,ɶø )ÌÏ qù¼ °„ <ɰGćºx‘–'…5®€ì›|@è{ Èظ£ÔYSÀÖ'JBÀE�{ 9“`Bâ�Qe@JyìÏ<ÿÐ,†_Šª+ó -èi….a‘ß_]øª¢¿0@@)„ƒ”_�¦Bµ!ð ®—-×ü ŽNaiàGΜëóO<Âxò”nøTfáù¼2ü³&üÀH)hÓß ŒÚý¼2ü¶›ó| z$¡0¨ï�ÛÂ� €€&dØ´£ÔIÀÖ'jÏ5΀ì›”z‰"ÌŒÀ…Æ âªOX�©  é2]ÌŒÀBãñU$òÁ!û3Ìçþ�d2—Šªwæ@·¡&¹‡G~}wå™ ù‚H$ $¢ü 2¹t¹n¿ài˜äâ6ž|éο14óÍ4�0p@R‰ßŒÚ?7™RÚa0 )Dï„Fl›Ì©m0›à€µ3<$EÇæð€@À€lH“`�ˆtð5‘–'…-®€ì›|@è{ ÈÌ\Qê,©àk¥!aÀ"ǽГ`BÒ�QeIÙóðF~ÌóÏýÈe-‹»+ó [„”&a‘ß�¼5¶Tî`€€R  (¿L…nbáA.[¯ød+s O>tç_˜šyæšG›à€¥¾´~okdÆ(�ðˆÍ£óy•[L&ø -Äï„Fl›ÂPx@ 0 vM‚> t=ÓÀÖFXžH¶º´Ø#âA$@;#`BÒ�QeOX�) =î€ì›”z‹*O>~ÏÙžyÿ Y ¥±uE~` = 0¥Ì*;ë륱uE~`€€R  (¿L…nbáA.[¯ø2¸�§�:s¯ÌL<Îi¤yƒ‚ÂÜNôFn?™æ³&Ø sF{ w^aKkmð-ĘBf±Á„ 6ð€@,`�Úlñ ’'�¬Œ±<("ÚèÓ`�ˆ‘ì›”z‹*xÄéHXp±ïtdØ´£ÔYRv|üŸ£ÿ†‚?ªæ»®ð-èiã�ùçá`}Õ;»¯ÿø@Jc„9…ð7&²p'žw_ð5�ҳ€‰×_†ØŸ$šI6Úm—»à„BXæÖ–õ,Ðü?ä _¥ôý—ä¶“ôp D¼Ÿ´@!à?Â&�ËÀÔ8&‚êÜs….TU^Oྮ­UÀ¿”êÖ¯ï ÞVîîº.F‰ÀŽŒ8ÅD@fˆé_ÎÎ?¿ßà‚ŽÕWZ9`âÑcu©bþî<.�MMüß  š¤ŸŸ0÷€ø­�Œí³*Kõò¯ç€y…lkœö¤�$†Âø§iqOÞ  Á{Zn¯ÿµüi—ΗÿoÍh.Gpèdª"`ðP\²[�:‚ÙFG“Ž/Øðjò§„ŸmþSÑBjýgÔËíO�“#ËÍO?AßVSâô³r|J{îJ”†ÇÚ0ç_þ÷÷Ý€¸h­ì|–Ü«™Ø~Ïæ<ú=g‚€¶†A”Á8xO½{>ïÁãÌÖN¿Àök¬<KN°Öx=O¹Ìúø…ÖŽxNÎF&þ^ϬG ‰ð¦ŒApÚ � HšÇ¾Ÿà – |œ¶=pðKø E­,^ŸÀÞkK¦•oº¡Ò?O'[ϯËö‡`¬á*?¼/€DÕ'-ßðC‘‡/ ’Bpˆƒ‚cDÃï�½òð’fIæ7]x9«´^�ë=µ¿ÿþ�ʉö¬Üüb¯þ`/®‰,~Žˆ%õ¯Þxy×–<·V á¡xåBHcW6}|% Ÿ¯à”3Ÿ¯…ÈÈN Áx`Æ8’f BAñµ€³÷íŽH £µ¾9]ñ—³}_†êÓ[ß#æÉAêÏ)�IýqÀàŸ­Ï‹ÓíÃï®|c`'u|wÿŸ;ãqÀ=>z1í|°ì4×pI]u½ü½ár&NàF ‚‚ A0"ÀÀ(½J›~ןàb« ·>ìûàsôúG´º¹cÞ¦Î4ÌšGG@æ½?^ªþxù4­?03ž4Ÿï×P½Êx<åµ);·XL/�Tϩ۷ßárÀä1Ü=†Bã¡@ðPR1¤×”­ßØuð%÷aÿ~ŽÑÒ`NÛäÖÓ˜«RÚþ_äê‰  |=à^õú,—D†þ„sŸ½àß×~ë£à˜W l€ g‡"‹[džÿN¼ÿž¼_\iúúdú¾=wûï‚eð¹ÞQ!°BõëxèÒ}ÿû„ËÇÒýÏŠ@3”ó*CÌÁ¿{HV•y¸“í^ÿ͹¶þ3ùægßËâ”@OA›¶=!àø)¼ 3�`é€c]/Z=߸8xÝ$˜W:‹ÿðÉP»ìt¥»,7ïï`2ÙJ5/ïÿW�Jªü*Å£Žiï€&ð¦E —¾?°{Gòm vçŸáÓà¹' §î½õëkOÿ—Ê#8‘‚ã¿,›Ï©œhŒnŸÐÌžGò¦ú“Õ_ñýÚô¾7ç: °ô¼E€‚Àññ£Ÿ§ÅÆïðâ‘˯}‘Ýí¼ã:k€ 2G^ÀP_ØŸ óÉÆ Nb¾ÉÑ ¿ëŸÀTešÅØxcÕý_ã]'ªÒ#ïà˜j“×mëòün‚' …„þeWÊñàH«SqJDÃÐoìþ»Ìð$Òïá Î'ýØÆPï„W\®çlÿ‰€qTè‰~©s`PdsIüô€d’Â̳dÓƒ xJ¡¡:"½iÞÚÞ»»¿ý| | …|wT(«DÈÇë­.ÿ߀W® :Ó"ýu›±ûï� Ð4IÒÔcËÿÏ»( ̃Ü�|;÷~~–Юßðhý[B»x(/Î$o8yÇ%–ÌÕ·À¾ü›E]Þé-®Q°÷ì¯Æ:9í[90Ë¾ß �BZwsÿº3-Ò2öÙÎ7àæ‘ÜÓÿí�̾µ¯Îsßü6/À™œc¾Á_>½8V½¯ø\³óÿÁ9×/”YB7èÖG1*§Y5[Ó7ž 7èqgÃss•^kµt’ZO<üˆ8g•ØW~Äøú›Uÿ·EÖ?òð\ÊÖT+°#�¨Ì«ÿlfn·ý¯­Šêš6öð*œJþ�3ý/bÁ?åð` �PðV …>Í7‘N’ÿ¾þÀ•æU¢z\þm1þçüoøIªoÛ¿åõÜß² 1! ðüSà(¤#³_àŸC;§‹€J  A›Ú>L‡€²3ÿ†ïð߀ x 2’™Rfœ"ZÜÀgìk<ÿœq}Mqç�Š€8ׂ )€>DÆ·:Ü`Ç®·oï°§Ä.U¸ˆ¯I�…®ÒÀ¾)ô6ÿþÖLÅûtï?׃ù¶þæÛÁ.Apq€äÓ::£\?�¢ÜuOþ³¹j²\vòðÌ;.ÞÝ·,0¾TËnÚgÕÝܾO€ ñœdf€Ó!¦�þ�`° B(¬¼ÄÄ[”÷ö€ýñ×$õ¯ó‹1«ª¿’™ÞF”�QPŸy"Kg(Nç€(5ø@ 8À@ > =œTÏ9.€9Ð"â{ë}„¬j3á)·ïð�# FÐDã®ø…Úr +bUo°Ša¨:m!ë¿ïð„ Ìw)øk†óÿ3HûlJêØêýàmÞ°÷Â`€ð @ÕØb—sg–y xr�`b›³¢Õe0xrÌJ¥ Ølš!%ÝÁ‹Ö4ÿï9ƒd·‚ R(¶h=СNíÐ×>Z¬ë»k»ˆÉÄKYhn„@ P�@lßñŒsÚ0ûÞ A[Dþµ€sß¼¿Ã )”>÷ƒH+B%ð¿¿x%~ A`h[ε$Žøðœöpûóô}/Æ61ŸJi�ýDûUcíÚ=ÿîòb„÷KG�ùSää÷?nZ`¦¤ eân¼q¿^€ÂŒ„à0ø:r\ÀT,>÷…˜híU°èäsG8ô·RÀEÿ¼]Á¡I\„tr9£€˜ñ9üÉéO¤Ò`%V �°‹ÿxØ) ¨ÆZÂ\ü­ò@Æ™‡ É3³ H/*„Zåâ?ø D  *ðvÿ-aÇÞœ�  =g‚ôÿ�h8ûЧðiÀ@2÷ÀNxœÏ¥>š`.Øfn° 5†Yÿ÷I€+D!^ŠªÚÏèƒ7¸©­Þ°ð@XC! 8Ç`µl€ Q 0ûÐqp÷j¸äÚY€s‹ãï@¹v‡»UÄÌ>Tû¿Œ^NN¦íøx Y9@džÉv;ûÂ"D¸ûÆrØ*ø[‹î>ö‡-è‚a¥ Æñlc˜”ú'°Ø€73%˜;š%ïÞðH_)Á*ìj�šz|v¤JÉLç5!ÊoêAäª"GÛ°{ÿÁ ƒPP @·|R�½á!Ú`¡2ÉEïÞ Eí¨øç/ýá íÐùlÂ÷ïç‰Ì¥>sJq3DƒH•�!ò¾@a¬Ò+ú¿‚žîîàa@–UõãˆëÀ¸šýV5"¯ýá�0ÆB 0C¡“ŒCX‘GÞïÀ  Ã.Q`v¬1�bÇ{¿þ BŸ 8@ ÏJ|R�ä l aq ´…¹ëÀÇôƒ5°ì6©ró¾Z¨…uP�%ææø%ãç–þà�aX t´”&¤*ýC–¶ä�Öý�[MCjÀ)ïÐT9-lBÀuÿOæ})óÐsfxÁR3•ØŠm §ñ–,Ýìæûp€•œ ÈX*)`¬,‰iÊë*°+�*^)]a0?¥>Ãdø—œŠæ¬êûùÂñ+†$.ÈðÇÿƒÓd§y’ð³OÞ#¹Á0åÕnï{øÎŒaŸéŽÞóÎïoð_¿`¤þxûß<°NbP½žÃb„Ÿ«‘iC0”VJ"ÞéQ-z�j¤P †Èïÿyf�VC¥a/~ … ‚��Éx çÊT½ûÁ»¯€@dÇûöðr/>VÅïÞ Ç¥”TÝ>÷ÏŸ‚c)=3Ҙ។f a×éàP/*„NëÅ÷�à6×äXõŽ_÷�Þ…†*ÒÃùáCB Æ4ãF*¨`&î4Žd}àêÿZÁGî—pòµVs¾GŠƒÁy?À€F¬ ?tìN?¦zM>”‡çô†OaY=W­^þ] v`rÚPWþ`?ƒmm×4ÑßþìŠÌW ×£ÖØ a‚PÁ äüøP^ýÅ…vôŸ€Š°à ½ú2&¹ ŽÞ†ØŸÿü=¿¿_Ü}þãî?¦})þƒ˜9òiÍj÷*ø#Þ J`åÇe¸ç]ƒ—–ã–n\§Ðì6n’òs)MX®œ%Ú¨G…Ûø-ÿàðÇ$KÂJ|þTiø ŠH[b�0áàáÅ­¿ƒa— !^¢Ÿ)ÙöÎØrl$Å|€Ù"e`3çŸ?ÞFyüvÿµWø ¸ÿAŸøE8!à,—ׂWázdð1�[dPøG ÄÊØt²×kõ_!ßEÚW-ަùÞ l2 ³Ì'`‡r8É+ÀIÜõyìáPÒw<¯<—‚Wá8?  `o†‚@%wwzwdGè© ú š>0$á�Gì¦4àéÔ«Å&÷߃a·xâBQøü.ä $‚qaP³+ý¼M±ô™çÿü �hº×Ñ‘[?ï¡ÿ íÝh:°Á<ŽÖ©‹_þ¦‰€ý/S5!†á!Il³Þ Ô‘�L_ž"$ÞÃç¾¼? ‘2pà_rˆ!@)0@(ƒÃx À–Èrpª®™¦œx8"†º‹ôÔÏüð«yTiBâ.ÿ › X¹Ea¨¦@½ŸóÙ:JÛñ@³:;ùøp.ÞJˆÉ¨VïaFu/~ø(O%4ú¦ï«Á:‰@hxæÓI¦çC`/Ó;[ø6™Ú߅Ȉø8Q‚`ð@/…†ß�õIMÉÿíá)�-�Ù«où猿LŒ|&a& ºÈá ÏáXVÝ.Ýùð�¾(sC\øMà£YzN›ZÇÁ°Ý€�3ÉÚ9#ÿßAĺY­Þû˜…}Ûÿ÷ý< æÖQÉøñz@ ‡Zik7¶ü¥p:tm®€T-h‘3=½¾�/î™¶Ôä¬"DB@l  Œ4¼Ê‹-{ôfýž¬á¬À?;¦ïWwwj÷͸*î³Ý~æSºÏrô|,;aPØà^€U@Mƒ¬�Lyà/ üάý ÿû«^G#M@ ›øõñVÓoø‡í¥˜õ}x'_ ‘"x5lÊ‚P%�ÀÀ"i²ÚdÐ]$Œ¼Ñ‡‚‚.7@²ÎY @ab¡ŸU�Gütλw$dlŸÑÿxa0ƒÅ¯ÊïÝý€,6Mo®‚­¯ò»gö¨n‰íë·Á8G7x4 ø-�¤�ª[¿ž�˜Ì/ ‘ÐÅÿé`ÊzVY÷ïÅfvtkßÂé‘á9ày áa>Å®"0ÕžÏÁêÀA—7Ê|Ñuúÿ�ª-D>ó�Ö+ë¦ÿµêÈ.†Úä›þ«Î5¨=qßËáv‘8>àV$><< šÚ‹J?ñìgL¶M<˜k]磾¾ð0Ì+ÛÉ:;à¯È²ûÙàe€^Ä vÍ%÷ð”PI¯W™Æò¾ÏÞ:T–^§©›ïÆQK­÷„§¤T–o+C¾ Wßþ�`½;tuè[å*¼0J'®�Lx€ „í%f^ï]àè±a|ÆÿmúD-( ¶>¹ ÿ¾�¶N$ýB–õï†Ë_uïþ€”!€h €4-· y¡‡îð.:Òºtš9ü ¾ë¸øÅ¿ a,š?w�pëJèTW$þ³zëºø%^…pàJ °7„ ©‚»–ýàb< %e÷„ýÞÀ_‚ö¨ã3¿Uïð̨ú3;¯ß€Àžtaó t–g¢M¨ÉxÞ A€*áÅûøi/æ ßÂäH˜�Œ” ÀÄ aX 7Çù�6µõ�†*3w/tÿeWt"m$`%WªG·€!“ÓGyÐ\»Wooðí'”ýojà�d2 �êÊÊ6iõÑŒ%Hé×ÿï? R9Üÿî¿ ‘`HA�€�ˆÀs²‚*¶»ÜõEáê÷€úêoªv�npÅ>9_ºVÇ+ñžtJáÌH …Дo—¿?À#2ø?iiõî`?(ž+zÊ'ŠÞ�áÈ@ÂÛ—f<)?øoæ½ÿüùë÷Ï‚€¹ÉâB Ä œ.  X8p0 ñ/8*e'Füýþ?dç‘æï|))‹k?½l%¬òFùfYÚß^¶›µùËÚJÔwºUA;•�.)¹Üãýÿ…É'„¨ ¼,©j§›€DßOï6÷œ^_¯ݯ;ðì¿^\íy÷Á@^JÏ‚ …Þ禰à:a¢üþ­|9|ÿ…a¿ÆÂ „ƒ°À:  Frù¿Ï`W+^…&VŽ÷Oञ)j7·x( �ˆžp@Ä„D€H€ë Ä.!Åg„Õh/àuØSŸ:¸Œôv—çWL:zß÷Z �$6Áô™â¸¼ÍÖ›à~få‘¿^ ×…ÈÑ8�ˆ( ˆ¤ƒ€ñ§‡ÂŒË½‡íþÿ�ZÁyi}Àfut8­ÿì… ´ ’Þ^$È$„†·“¤ºü„aC€É€†‡€�îZçPbàæ‘ÙüþþÖ‡:_ÿü.FGâB"’ÄŽD‹†Ä5�Ÿ~…O½oùÿAØ"4o‰ü�HìtûðL¼.ˆ‰à@ à@ �P€°UÛ¿ŸáÊZ��·þ¼üž»µçLJÒÅ€¥Î¾÷hü"ĉcáÔZM*uJˆ½(ef€Ó™[„Äéâ/à> UpÔïøuOü„ž@!à,˜‚ᇭ› ¹´O�¥¿ÿÿ}r  ‚Ê^~Ên2§×ׂgãàÌó`À‡‚„ååÙù[2È5å£üÿ¿ÐNb!ÇZÏ!Fµjîÿ(^+yÛ¡20 –ß®¾´kv{éf U<Þ×>Éž6ó*jW‚gá8@H‚à˜hx:ÃýÖê>}¨4S=@ì^Š@ÍÀ•GJŸ¿U*~ü.äp¸ ¥€(6€ ²“do^%ènêïy¿_€ž^£÷üä½Q÷‚wár"> øa� ‚‚EÆ N™Ï€ +P²å¾©Êž=n SXö¾“{Ì&«âµÏ;¶¸AþωkŸ˜8&Ü”à4ŒßÕ®w²ýx' ²2`I@bø˜] ¿ )’K0=R~H‹?Èìý.|Àv7‰Rc\°uòTŒk¯ê Æ…Âar6ŠAò:¡“�–êÚVíæìõ¦¥QA-­5þ.84ˆYý ÖÀÇŠ‡¶—‚2»ì÷ÞœÜTº^‰�ZÕ©à .DGÄÁp},1Ómþ`6BHµÏ ¼ésÔI¦bÌ EsY‚S‰¿4®k0J8›ó^<�œL0 9ü¡ ZùZäAÀ¿X—0»IWA3&üÃBó,S7æ �,Ésrx��e‰¸÷;ÃüøæÑ{ßS0$@ň«}}ù9Á@W `À2 Âè¤ý,Á,mIøÀf£°žóÚ<BØàHNá8¯‚8XC°ò¡Ô•(ÞD2cƒ˜‡‰L`1Ì¿ ‘‘ðè�å…œBÉ’ HŸˆfàþ©øeÐgüÑz€Q¼n„Xfñ®¹‚€¹Ba”~~`tªÿ�T U²3d³2äíŒ�ÜÊ…ÓÞñíðT [ûr$O ‡Â)@@³Cì0EþAÀb*d¯7L†Iˆj¯LÈÈÌ­¾?ŠÛx÷\Îa ª)ƒ'Ò~`uƒ,Éù€õ�I]àÜ.IÜN)NÁ¤î­ºv òI„€�bátGÖô¹éÉ2~1Š÷œ* R~}`ÍrBF²¥rA#X ¸ N„ax_+fÀv¼@àÉŽ+ (Çă¿hêIA(Ò©FÁÌF šÈàæ/‚p®@ awÁ_Óò ÿü‘ßüŸ 1�×\“#\ºäXf²ø' É&à . à¶`0 ÜK Ћ¿ â�&APräüþ97ÁRÛøruȨU¿¼6ÔS<÷¯¹Ü�¹‚pŽ¿ƒ]ŒW¼À6ÛÒy´Ó£Õ¶™Ëð—<›³Ñp ¬,©$™ÈÒk@©o0÷82éi½«Ñt*›Zô\—*ôÞ ×#éž yA²ÂÀ+ê~` ?ÔüÂp:«­sÑʤ³ÈÜJ¹üI\Á:ø‚¬2(¥î2DùÝèIRT”‡ü ÔŠEPaÀn@ }eˆ„ûÿþ¢€‚—Ž QÀKNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®½ÿÃw‡xoÀ¼IL©3N ‘ì„Æ`3v 5ŸÎ6Ì EüjFËÜ£H@[ |‰†·qÑ�‹‘%sߨSâ*ÜDWÖ£�•r`0ݯðïAؑà •õ©_fÔØ4kF;Õb”•;¾x(5;æwçø%ìÊA4 ¹Ò=~ê!€ åÂp@ÆB¹xx¶ð�5sf5åp`¬Öç¹x€?oÖÐ ™˜ÕJP0#w¸’뮺뮺뮺뮺뮺뮺뮺뮺ë¯þOØ&$s•†f$•©^!~:xŒ�ªp±} *oè+%Cx;û€âÚãÞàA`鼺 2·»€À©¼º$gÝ}]Ýòäàèüg 4Èi£ÿ�#Ø,PŠ+7±1å=ýÀ?E|uÄ”GÖ¦cU_wš4Îðz4¤‚Š…{ûÉ[9@Êw8a„ €ý`  ðQèD¢¦yÈ)t΀ß[ì%cPéŸ M¿„€ 6‚'wÄ ¦Èe@VÀJÛëH¦ƒ¦Ò¼_xBFgrÿüðÞ`/ð1¯·c}[<þ 0€@XÁ€€@€@)v¥ÜÙåžHü{Ý�«)€ÃŸñ}ùÞ 4?®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®½ÿù`†ç ŒZp� ¸$t! ʲñG$»ïQš#m%° pÒ�€` Óœ<+TH*øp©߸x+\Ê;x(|·ïÔ‹äEøÜð2­ož|ŠA�píÁ©)šl!ûE±Ò•_ÿ9a‘JèÚZßÿ‰$UV5_ÿàÄ4€€°`  𗇟¥Ø€Eß~ç¶o3Ù¶óïÿÿÿðÀÁ:‚ߌjîH:Kß¹í›ÌÛ6Þ}ÿÿÿþZë§®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ÿÿÀiŸ¶éyKpìµU›-�‹È7ˆåøBÓRâd: íœy¼£‚‰·þàÔïí6Ö‡3ƒÆvÝépQ…üO‹JÆ`x¡«ˆå¼ÿž !¾€€@¬Ѐ¶‚�xò`u Õ÷<ÆëÊzâúDtA&âdI8“dH0È{Âȃ–/wÜó «Î,¥éXí@F {WcLÄË`� au×]=u×]u×]u×]u×]u×]u×]u×]u×]{íockzÐÁsòÿ†O÷ÁÛ³�ƒ¥ŠÄ‹Ž�€Bì\áÎ=>±þo¾aà$Ä_l’\“/ ü@ (4À  �!º¼¯iÐ�ÿ „5ð…¥ž±ÿ  VÕöxSÑ&…�5øÅ9âÁ›ÿGÿþ í÷ûoHFc?Ïa×]uÓ×]u×]u×]u×]u×]u×]u×]u×]u׸4¾\ƒp=÷^ȇ> (û«/ýí25@•pc�]ÛÕp)u S°HðŽåÊ€‰z&}ø "ö@n@ÊKKHhà »?y\õÀgØ1ö€‹<ûÆ¥Àܤ´ôÂBBŒ³¶ó¹é@1þ~` æ‹ø€À®'®ºë§®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë¯‡öÁ8vEÇ)‡�oxdȉa09cyŒL4î=ΘL @�p0� fÖ¯=ÒP€å§”„€G׿=Ú É疺뮸çÿþ`Íu×]u×]u×]u×]u×]u×]u×]u×]{ü?°Þª&ÉG¢¶® PÞ‚@­¨ÖÑ =Þ„�P�dgôúð@ð€"H¶ZYÏxfË]ñTŽÎÏzz뮺z뮺뮺뮺뮺뮺뮺뮺뮺÷O�Sø9= +Vw¬ßô•Œ}"Iµvú÷Læ&þ¶|Ÿ Ó'”²‡EüDÞ&)?,_ÿÁ„ 0ˆGð QÈð«÷´†"¸‚’Q‹´P*‡lðÄülûÚBo–;ÁM(CBZІDõ×]tõ×]u×]u×]u×]u×]u×]u×]u×]uïI§ÒpçÎ I;ÃÆH„×_ìZ `kzB¿ï°$µMÏ0ßÿõø+³ÿ»ÂàH €@'C³ “‹ß prÅ ²…ÑFf Aù˜õÀÀûô,pfœ>Œ;/ô§Ø-‡@ql,AwMý릦þŒ'‘ÄÔ³P�þ# ö(ž—z–뮺z뮺뮺뮺뮺뮺뮺뮺뮺írU>zLãà,€ÇtÏúÄC¹Sþ n àÔbnÖ:¹œSI�Ǩ/%W½~|-E¬ƘWÁé ‰1év,Çÿ€ +{,¤¦9ÂÀS‚`Y«`9P@ÚÂe�}¿x›'hÒv0NªÞ©Áwµ¨0.°™à*wï32hmó¢{‚UY`EDŸ½“×]tõ×]u×]u×]u×]u×]u×]u×]u×]u×¼õÿ@³ôpËd÷iMowÂ�‚!H›*G°N’U|0-•îå—?~ºžqð̦*ίÍàHÔ$­PÈOWìÚ»,ŒÏÍàhIA¾&W3Iû>昗¢ÉuÿùàHÂ|\ìÐüºç~H‹óZ�R?Á€Qð 0 2�˜¤â‚ºX¿^¬NÁ]ªÄ“Àx7Ô]Ø#ã \™ÌLsqÀ�œ;×�i�¬�`ß,¸¤ü�Ý&jºë§®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®º÷×ÿ@³ýQ‰L\âzQ¨mð#6œÖ�pÿœÙ8ËF“‡)ªê«ÇxY•mDR7ÿ—§ð6p¤N~D2ÞÆ¿ žqª$µg?—¿ÂûØ‘ý”ü~ €0: € 㪘7/¿€…£ ‡éÒÆþìƒ0~œ=Åi€UYý”àaö C1¢|ƹÿ»�zpZK&_êbº¬;Ar™Óð�Ã÷Ï^q§{h›Se',wüàD5fPeÌ”HJ$¿÷¥ã�ç£`a¬x¸ƒ‚ÂÈ &ÝðC+|i = Ág“_…™À&ÝàêßH!fEÝ}ê¾Áo}6µ›oo7s´óÚòúÞžºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®–_ü5Ý{?úÿ_ûï8àú A R¦$á¤PTÀfH ž`Ï*ïïð?Ã…ÄP#þ"ûàœ@«�¶›î¿Â`X`@\CÁ�ÐF\¼Á[퀠*ïÇø¬#I÷nð7 Zp`²{Þßý¡d?fq#¡À᳿?ÀFÐ!Úgd²>ô0áàá „€pÆÄ@çRyà"�x„8ÀðžC¿Ä@÷r,ð0T¢è;ýu×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×]u×^ Aš$lÏ•}À4 AžBx<ÌÀ žatæ`žc@!àfAšh4¤Àx ?*û€.h Až†E(!àf ž¥tæ`ž§@!àfPAš¬4¤" ÀJ”8Ïů»»þ-wÞðKá%ßwÀëê™�+{�CÛŽÒÌw�+ÔÔù�Üù�ÜøMw»¿ðFÝÜÈÈú‘ 7 :AžÊE(!à,¦p³"ŸbÆ}Šî+|«»¸%„q_|Qüdâ�–ÅÅᯱPI+®(ø£â�Ã2០øeˆ/pˆ¥îïrûww†eÃ>ðÏ‚!ׯ‚($÷J¡�CÿðVPNÿoá!‹×Ã>ðφ|±ý¢¢ðE»»»»Ã18gÃ>ðφX‡Q›®+Ã18gÃ>ðφX¢ðE JëŠ>ëŠ>ŠÔ3>ðφ|3á– ¼E.îîîðˆ|3០øeˆ/R {ã%|P~ðφ|3á– ¼Ç·»»»½á–' øgÃ>b Á$zîîîîïxf' øgÃ,Ax%‘wwÂ’Š>(ø…‹ÅJ¡‡þ§ì— y&×»�§‚]ÎÓ†X‡ôs§»çüì:žétÉzõÁ"õÚà�r®×*à‰s.—2à‰s.—2à‰s.—2à�r®×*à•v¸'^¸ Ø,žë@!à,ââuŠ?‹]ÝÞ“ ¸¯È+â¾+øRqGX£â�Š>Žá¥Ã¬ÒO�£wwwwwwwtw ¡8gÃ>x#„BÝîîîïÄÑŠ†|3០¼A Eî•C‡ÿà¬HNým^HFÆo_ øgÃ>ðËÁ"îï{»»ÃLNðφ|3á—‚(ȇ\WÝq_†Xœ3០øgÃ/Qñ¸¯Šü2Ä០øgÃ>x"Š www¼2Å០øgÃ>x"�kß N(>(>(×G3០øgÃ/qAîîîðˆ|3០¼APC½ÝÝÝî�C,Nðφ^ b÷{áLWÅ}ïÞ�Cý~ÁhœòÚà—s´ðK¹ÚpËÁ<@Öû¿µß»Ašð4¤h `7AP�P`æ`9AŸE(!à, ¡ç…‚¡L:,b@<5 ☹‘– d~8,–Á%°r׿f@’ �;X�j”p¤…ÕÄ€âŠp YåE~ ̰¤¡@*³*Xø _Åþ>? x!b,ÿPGÁkâá`5ã}øSÎ…™fÙà_å€ øø/†¼3០¼Â’ xŸ\°#á<§|â€?È/†eÃ>ðφ^ œHQY`ÜP‡–Já�CÿðD[�Œ"„@ˆ™!Ÿ øgÃ>x"…$s|°#âG‰=q@Ÿšl3.ðφ|3á—‚(S$Åþ( HÞ(ññøgÃ>ðφ|2ðE  Ä‡â€?`Ô»–üniaÃ2០øgÃ>x"'°TÂ{"l°(ŠÄòÀ<ù` TØeðφ|3០¼A:x°„pÒŠÏ)› ?ÒŸ`·ÀÅ\ÿ‡{«ÀpæNŒ]IÙOÀLŽŒ.¤ìáÃ>ðφ|:Í%q²»r,3Øb€_NðtáO>„ëóL? øgÃ>ðËÁŒ,¯Â˜èÕâ€_/W? >/9„@œ~ðφ|2ðK!`T°3á@b€Ï Áט'ôrð¦(ñ@¯ƒ ða� óÀ�6 䈊ê~Áo.W2>dxeàž Sáó.¡ÓÎtà•@}Ç_é Ø$Ÿ-tÎ`‹¼ “ wp½K,�FÝåE@¦—|éÕ:»ëŸ/@!à,¦nl<æ ´)ŠfÃafY•`¡6 áfY§'Šá÷�H-âOJ°Y ƒŽ(Ù ÷à€`�áIÂà{q@ñ@â€Çà†W,Þ÷ðG3,bøS VËq@?,â€|`(‡§‹ì3០¼“»ÅøT]ÅøÃ3០øgÁ 7µüA;7‹›–få™`׆ÿþ�K…"Ai øgÃ>ðBÍíPYŠõóüP‰8“Ã3០øgÃ>x"…&öx =ðφ|3à…›Þ¾¡L\@óVP‹ˆ~(ÆŸ øgÃ>ðBÍÖxYx"‚¦oŠb˜¦)Šb™¹fX(pË>ðφ|3á—‚('nég‰>o–\0ÿJ}‚Òàžø¾ˆýÕà ¸k:1~Ýü9“£RvS០øgÃ>Y½½üÁVyŠ5lPˆÝ`¡�s†Yðφ|3à…›Þ¾ä3/‚©ŽÝb€|(¹€À  pË>ðÏ‚n¶V^ `©ÁN�¦)‹Šrf c˜ â8¼P:¸( !‡þ¿`¨ë–œ4ÀŽY½¿àž…Ø ¯s£¬¤ß{?N€nÀ1A›44¤8ˆ:YP 9`x /TËê™}S Kê˜ëêš =R © {õH€!1AŸRE(!à, ¡æŽÆ¥#òQÁF(Ú, õÅÓ, ñà26E€2À€µÇ_™˜A$8áÇ�òQþ7ØSÿ¦$Å,8P ¬ò¢€28S 5™RÀ¿Åþ(ññøepBÉ_­A PãÇ€Ó,)„¨fe›e€–üP/à¼~ðφ|2ðG `ëâ}s€¯ð^ø^^œ°/à¼ðφ|3á—‚(]‚×Ñãåå€0dÔ°¿wx`ßÿàŠôž T9Nðφ|3à…ë_Á)ÍòÀ­q#ÁÑ|P/æ‚êðφ|3០¼B˜ 1!ø _ʼnÛÅþhü3០øgÃ>x"�ʼnË\P/⃢îXñyaøgÃ>ðφ|2ðE[ƒ«š¸ _Ä€>#ËóGXgÃ>ðφ|2ðEéx dÊñM†áO°[àãö‚o*\é–ËL~:e²i�†|3០øeàŽ7|Á€£:”P/ònðjò@ø·‚pO¯ßü3០øgÁ ׯ‚9h{áD°(fȰ(ŠÅx+}ËŠÃOÁ8†|3áŸ/[ßÁ, v–ðÐð  PL –ððÑÊÊð¦(ñ@¿ž„Á@x�'ÔQ©ü¿ÿÛºÇðËÁJ8RGUptxq™²BHV ŽðËÁ.Â"ɉ„4‚.pð<ù` ”o ¸ºáø å8gÃ>ðφ^¡IÍòÀ�ŠÁÒëŠüD¦Ã2០øgÃ>x" !ÿû àp&ÙÞ=4¡¿¨�<<5OÀ� ±±µXp*[˜‹±ç¼€ Os†|3០øgÃ/Q² H~(ñF$üP'â"›rÃøf\3០øgÃ/PU!xŸ/Å>$N_ˆ*l3.ðφ|3á—‚(]¡`᥼9‰ŒWü0ÿ }‚ß[¹øçW€(àUmiœü„µ²Óyðφ|3០¼ÁT‡€Å Ó ø5ã¼·‚LØe— øgÃ>x#‘ƒm~6AÐÅ~�àÔ¤±“²‚ ïï†eÃ>ðËÁ,,ŸŠ5�KŸ0úЏÜP'â€?„À¸1˜�y°W#†WâÞ Yø%õ�á—‚x%_�?£Žž§šE…Pb?%e¼•ï‘|)gð°,A›u4¤9àbT @à Á xÕ"�ê’ =S (ú ˆ }S *z¤@7`wA›™=!à,�çóðH?ŸÏçàŒþ?ŸÏçàŒþ?ŸÏçàˆGŸÏçóùüüÏçóùüþ~�çóùüþ?Góùüþ?Ÿ‚#ùüþ?ŸÏÁüþ?ŸÏçàŒþ?ŸÏçàŒþ?ŸÏçà�þ?ŸÏÁ9üüìXAŸ·E4H!à,¦p`©EQzî.:àbN[ÒÆµ— 6ƒU/m°ÃÿÎvÚ ´t-.f™{ô�Çå’nIœ“ Ж~÷ÿøÍ`_59¡à‚§ƒùoƒ€‚ sýÁƒ˜î;ÿÿt‚fvp°T:úþü2¸!p☿ÝÁ0߆ó< Sa„Š\¿Ã{¸ÉÞþB%@Çï„þ¿ øgÃ>\8¦/Ö Œ0´‹´¾@ß„ÁÁiÂ]„šeª‡ßIj£ú—Ã>ðφ|¸qL_½Áaö·±¶Ð‡¤Æ‰chUéì!‘ôœqôdÃ>ðφ|¸qL_­Aaí-­ñ¾œñE?ÞØ6�±@Ö!‡…›œ3០øgÃ>\8¦/Þàˆ0‡ÿì7±xÚ þ“íbV0φ|3០ø!p☿Z‚ Zà!~£Ý~±6=}‹ øgÃ>ðÏ‚)‹÷¸" )ž”Á†M„LxlŒÎÀËŸÐ%´ÞZheðφ|3áŸ.SëPDziôÐrߟÃÒ;À¥¦¤Ê É…oA+ÿølûøgÃ>ðÏ‚)‹÷¸# )óÒixõ;áeŒðXPx)fPφ|3áŸ.SëPG#ûØ|ó™Ã¸ÀÉøkv,Z ¡žÖòžío Y”3០ø!p☿{‚PÁB¹üW†¯òÜ&Œ °g1ðaÌxMù ?×]V£øo,µ¨‘–-›r‡0b|!Ÿ.SëPOÁ›Ü¡^*Kß…R¤^+¼ÀÉ|F¨Uÿ»?ŸÖtÒV{Á$þñÎýE1ŠÔqŽ=GãÁ:÷½ãXÖ *ãÔqŽ=GãÔqŽ<¯{Þ5�`Ž �ŒzŽ1ǨãzŽ1Ǩãx'^÷¼kÁQOQÆ8õc�QÆ8õc�QÆ8ðN½ïxÖ5‚*Œc£Œqê8Ç£Œqê8Ç£Œqà�{Þñ¬kUÆ=GãÔqŽ=GãÔqŽ=GãÁ:÷½ãXÖª1Ž=GãÔqŽ=GãÔqŽ=GãÁ:÷½ãXÖª1‚¨ãzŽ1ǨãzŽ1Ǩãx'^÷¼kÁGãÔqŽ=GãÔqŽ=GãÁ:÷½ãXÖê-Ž=GãÔqŽ=GãÔqŽ<¯{Þ5�`’¢ØãÔqŽ=GãÔqŽ<¯{Þ5�`–¨·¨ãQÆ8ðN½ïxÖ5€A`:ŸØ@!à,Ÿ¿|?}¾ß+à�ò¾Ÿ3à‰ó>Ÿ3à‰ó>Ÿ3à‰ó>ß+à�ò¾ #à�ûà7` A›Ý4¤Àx™€ AŸûE(!àf žtæ`ž@!àfÓeˆ‚>ÿÿ‚(  ¥ã€ˆpÅ“““““““““““““““““““““““““““““““““““““““®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºë®ºëÀ Aš$læ` AžBx<ÌÀ žatæ`žc@!àf Aše4¤Àx™€pallets-quart-6b318bd/examples/video/src/video/templates/000077500000000000000000000000001524460054700235515ustar00rootroot00000000000000pallets-quart-6b318bd/examples/video/src/video/templates/index.html000066400000000000000000000001241524460054700255430ustar00rootroot00000000000000 pallets-quart-6b318bd/examples/video/tests/000077500000000000000000000000001524460054700210205ustar00rootroot00000000000000pallets-quart-6b318bd/examples/video/tests/__init__.py000066400000000000000000000000001524460054700231170ustar00rootroot00000000000000pallets-quart-6b318bd/examples/video/tests/test_video.py000066400000000000000000000005631524460054700235430ustar00rootroot00000000000000from video import app async def test_auto_video() -> None: test_client = app.test_client() response = await test_client.get("/video.mp4") data = await response.get_data() assert len(data) == 255_849 response = await test_client.get("/video.mp4", headers={"Range": "bytes=200-1000"}) data = await response.get_data() assert len(data) == 801 pallets-quart-6b318bd/pyproject.toml000066400000000000000000000122121524460054700176440ustar00rootroot00000000000000[project] name = "Quart" version = "0.23.1" description = "A Python ASGI web framework with the same API as Flask" readme = "README.md" license = "MIT" license-files = ["LICENSE.txt"] authors = [{ name = "pgjones", email = "philip.graham.jones@googlemail.com" }] maintainers = [{ name = "Pallets", email = "contact@palletsprojects.com" }] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Application Frameworks", "Typing :: Typed", ] requires-python = ">=3.13" dependencies = [ "aiofiles", "blinker>=1.6", "click>=8.0", "flask>=3.0", "hypercorn>=0.11.2", "itsdangerous", "jinja2", "markupsafe", "werkzeug>=3.0", ] [project.urls] Donate = "https://palletsprojects.com/donate" Documentation = "https://quart.palletsprojects.com" Changes = "https://quart.palletsprojects.com/page/changes/" Source = "https://github.com/pallets/quart/" Chat = "https://discord.gg/pallets" [project.optional-dependencies] dotenv = ["python-dotenv"] [project.scripts] quart = "quart.cli:main" [dependency-groups] dev = [ "ruff", "tox", "tox-uv", ] docs = [ "myst-parser", "pydata-sphinx-theme", "sphinx", ] docs-auto = [ "sphinx-autobuild", ] gha-update = [ "gha-update ; python_full_version >= '3.12'", ] pre-commit = [ "pre-commit", "pre-commit-uv", ] tests = [ "hypothesis", "pytest", "pytest-asyncio", "pytest-cov", "pytest-sugar", "python-dotenv", ] typing = [ "mypy", "pyright", "pytest", "types-aiofiles", ] [build-system] requires = ["flit-core<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "quart" [tool.uv] default-groups = ["dev", "pre-commit", "tests", "typing"] [tool.pytest.ini_options] addopts = "--no-cov-on-fail --showlocals --strict-markers" asyncio_default_fixture_loop_scope = "session" asyncio_mode = "auto" testpaths = ["tests"] filterwarnings = [ "error", ] [tool.coverage.run] branch = true source = ["quart", "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.13" files = ["src", "tests"] show_error_codes = true pretty = true strict = true # TODO fully satisfy strict mode and remove these customizations allow_redefinition = true disallow_any_generics = false disallow_untyped_calls = false implicit_reexport = true no_implicit_optional = true strict_optional = false warn_return_any = false [tool.pyright] pythonVersion = "3.13" include = ["src", "tests"] 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 "FA", # flake8-future-annotations "I", # isort "N", # pep8-naming "T20", # flake8-print "UP", # pyupgrade "W", # pycodestyle warning ] ignore = [ "UP038", # keep isinstance tuple ] [tool.ruff.lint.isort] force-single-line = true order-by-type = false [tool.tox] env_list = [ "py3.14", "py3.13", "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"] commands = [[ "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", "--cov=quart", {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 = ["tests", "typing"] commands = [ ["mypy"], ] [tool.tox.env.docs] description = "build docs" dependency_groups = ["docs"] # TODO fix warnings and add -W commands = [["sphinx-build", "-E", "-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"] # TODO fix warnings and add -W commands = [["sphinx-autobuild", "-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}]] pallets-quart-6b318bd/src/000077500000000000000000000000001524460054700155215ustar00rootroot00000000000000pallets-quart-6b318bd/src/quart/000077500000000000000000000000001524460054700166555ustar00rootroot00000000000000pallets-quart-6b318bd/src/quart/__init__.py000066400000000000000000000055421524460054700207740ustar00rootroot00000000000000from __future__ import annotations from markupsafe import escape as escape from markupsafe import Markup as Markup from .app import Quart as Quart 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_app_context as copy_current_app_context from .ctx import copy_current_request_context as copy_current_request_context from .ctx import copy_current_websocket_context as copy_current_websocket_context from .ctx import has_app_context as has_app_context from .ctx import has_request_context as has_request_context from .ctx import has_websocket_context as has_websocket_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 .globals import websocket as websocket 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_push_promise as make_push_promise 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 got_websocket_exception as got_websocket_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 signals_available as signals_available from .signals import template_rendered as template_rendered from .signals import websocket_finished as websocket_finished from .signals import websocket_started as websocket_started from .signals import websocket_tearing_down as websocket_tearing_down 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 .typing import ResponseReturnValue as ResponseReturnValue from .wrappers import Request as Request from .wrappers import Response as Response from .wrappers import Websocket as Websocket pallets-quart-6b318bd/src/quart/__main__.py000066400000000000000000000001451524460054700207470ustar00rootroot00000000000000from __future__ import annotations if __name__ == "__main__": from .cli import main main() pallets-quart-6b318bd/src/quart/app.py000066400000000000000000002020251524460054700200100ustar00rootroot00000000000000from __future__ import annotations import asyncio import os import signal import sys import warnings from collections import defaultdict from collections.abc import AsyncGenerator from collections.abc import Awaitable from collections.abc import Callable from collections.abc import Coroutine from datetime import timedelta from inspect import isasyncgen from inspect import iscoroutinefunction from inspect import isgenerator from types import TracebackType from typing import Any from typing import cast from typing import NoReturn from typing import overload from typing import ParamSpec from typing import TypeVar from urllib.parse import quote from aiofiles import open as async_open from aiofiles.base import AiofilesContextManager from flask.app import Flask from flask.sansio.app import App from flask.sansio.scaffold import setupmethod from hypercorn.asyncio import serve from hypercorn.config import Config as HyperConfig from hypercorn.typing import ASGIReceiveCallable from hypercorn.typing import ASGISendCallable from hypercorn.typing import Scope from werkzeug.datastructures import Authorization from werkzeug.datastructures import Headers from werkzeug.datastructures import ImmutableDict from werkzeug.exceptions import Aborter 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 RoutingException from werkzeug.wrappers import Response as WerkzeugResponse from .asgi import ASGIHTTPConnection from .asgi import ASGILifespan from .asgi import ASGIWebsocketConnection from .cli import AppGroup from .config import Config from .ctx import _AppCtxGlobals from .ctx import AppContext from .ctx import has_request_context from .ctx import has_websocket_context from .globals import _cv_app from .globals import g from .globals import request from .globals import session from .helpers import _CollectErrors from .helpers import get_debug_flag from .helpers import get_flashed_messages from .helpers import send_from_directory from .routing import QuartMap from .routing import QuartRule from .sessions import SecureCookieSessionInterface from .signals import appcontext_tearing_down from .signals import got_background_exception from .signals import got_request_exception from .signals import got_serving_exception from .signals import got_websocket_exception from .signals import request_finished from .signals import request_started from .signals import request_tearing_down from .signals import websocket_finished from .signals import websocket_started from .signals import websocket_tearing_down from .templating import _default_template_ctx_processor from .templating import Environment from .testing import make_test_body_with_headers from .testing import make_test_headers_path_and_query_string from .testing import make_test_scope from .testing import no_op_push from .testing import QuartClient from .testing import QuartCliRunner from .testing import sentinel from .testing import TestApp from .typing import AfterServingCallable from .typing import AfterWebsocketCallable from .typing import ASGIHTTPProtocol from .typing import ASGILifespanProtocol from .typing import ASGIWebsocketProtocol from .typing import BeforeServingCallable from .typing import BeforeWebsocketCallable from .typing import Event from .typing import FilePath from .typing import HeadersValue from .typing import ResponseReturnValue from .typing import ResponseTypes from .typing import ShellContextProcessorCallable from .typing import StatusCode from .typing import TeardownCallable from .typing import TemplateFilterCallable from .typing import TemplateGlobalCallable from .typing import TemplateTestCallable from .typing import TestAppProtocol from .typing import TestClientProtocol from .typing import WebsocketCallable from .typing import WhileServingCallable from .utils import cancel_tasks from .utils import file_path_to_path from .utils import MustReloadError from .utils import observe_changes from .utils import run_reloader from .utils import run_sync from .wrappers import BaseRequestWebsocket from .wrappers import Request from .wrappers import Response from .wrappers import Websocket AppOrBlueprintKey = str | None # The App key is None, whereas blueprints are named T_after_serving = TypeVar("T_after_serving", bound=AfterServingCallable) T_after_websocket = TypeVar("T_after_websocket", bound=AfterWebsocketCallable) T_before_serving = TypeVar("T_before_serving", bound=BeforeServingCallable) T_before_websocket = TypeVar("T_before_websocket", bound=BeforeWebsocketCallable) T_shell_context_processor = TypeVar( "T_shell_context_processor", bound=ShellContextProcessorCallable ) T_teardown = TypeVar("T_teardown", bound=TeardownCallable) T_template_filter = TypeVar("T_template_filter", bound=TemplateFilterCallable) T_template_global = TypeVar("T_template_global", bound=TemplateGlobalCallable) T_template_test = TypeVar("T_template_test", bound=TemplateTestCallable) T_websocket = TypeVar("T_websocket", bound=WebsocketCallable) T_while_serving = TypeVar("T_while_serving", bound=WhileServingCallable) T = TypeVar("T", bound=Any) P = ParamSpec("P") def _make_timedelta(value: timedelta | int | None) -> timedelta | None: if value is None or isinstance(value, timedelta): return value return timedelta(seconds=value) class Quart(App): """The web framework class, handles requests and returns responses. The primary method from a serving viewpoint is :meth:`~quart.app.Quart.handle_request`, from an application viewpoint all the other methods are vital. This can be extended in many ways, with most methods designed with this in mind. Additionally any of the classes listed as attributes can be replaced. Attributes: aborter_class: The class to use to raise HTTP error via the abort helper function. app_ctx_globals_class: The class to use for the ``g`` object asgi_http_class: The class to use to handle the ASGI HTTP protocol. asgi_lifespan_class: The class to use to handle the ASGI lifespan protocol. asgi_websocket_class: The class to use to handle the ASGI websocket protocol. config_class: The class to use for the configuration. env: The name of the environment the app is running on. event_class: The class to use to signal an event in an async manner. debug: Wrapper around configuration DEBUG value, in many places this will result in more output if True. If unset, debug mode will be activated if environ is set to 'development'. jinja_environment: The class to use for the jinja environment. jinja_options: The default options to set when creating the jinja environment. permanent_session_lifetime: Wrapper around configuration PERMANENT_SESSION_LIFETIME value. Specifies how long the session data should survive. request_class: The class to use for requests. response_class: The class to user for responses. secret_key: Warpper around configuration SECRET_KEY value. The app secret for signing sessions. session_interface: The class to use as the session interface. shutdown_event: This event is set when the app starts to shutdown allowing waiting tasks to know when to stop. url_map_class: The class to map rules to endpoints. url_rule_class: The class to use for URL rules. websocket_class: The class to use for websockets. """ asgi_http_class: type[ASGIHTTPProtocol] asgi_lifespan_class: type[ASGILifespanProtocol] asgi_websocket_class: type[ASGIWebsocketProtocol] shutdown_event: Event test_app_class: type[TestAppProtocol] test_client_class: type[TestClientProtocol] # type: ignore[assignment] aborter_class = Aborter app_ctx_globals_class = _AppCtxGlobals asgi_http_class = ASGIHTTPConnection asgi_lifespan_class = ASGILifespan asgi_websocket_class = ASGIWebsocketConnection config_class = Config event_class = asyncio.Event jinja_environment = Environment # type: ignore[assignment] lock_class = asyncio.Lock request_class = Request response_class = Response session_interface = SecureCookieSessionInterface() test_app_class = TestApp test_client_class = QuartClient # type: ignore[assignment] test_cli_runner_class = QuartCliRunner # type: ignore url_map_class = QuartMap url_rule_class = QuartRule # type: ignore[assignment] websocket_class = Websocket default_config = ImmutableDict( Flask.default_config | { "APPLICATION_ROOT": "/", "BACKGROUND_TASK_SHUTDOWN_TIMEOUT": 5, # Second "BODY_TIMEOUT": 60, # Second "DEBUG": None, "ENV": None, "EXPLAIN_TEMPLATE_LOADING": False, "MAX_CONTENT_LENGTH": 16 * 1024 * 1024, # 16 MB Limit "MAX_COOKIE_SIZE": 4093, "MAX_FORM_MEMORY_SIZE": 500_000, "MAX_FORM_PARTS": 1_000, "PERMANENT_SESSION_LIFETIME": timedelta(days=31), # Replaces PREFERRED_URL_SCHEME to allow for WebSocket scheme "PREFER_SECURE_URLS": False, "PRESERVE_CONTEXT_ON_EXCEPTION": None, "PROPAGATE_EXCEPTIONS": None, "PROVIDE_AUTOMATIC_OPTIONS": True, "RESPONSE_TIMEOUT": 60, # Second "SECRET_KEY": None, "SECRET_KEY_FALLBACKS": None, "SEND_FILE_MAX_AGE_DEFAULT": timedelta(hours=12), "SERVER_NAME": None, "SESSION_COOKIE_DOMAIN": None, "SESSION_COOKIE_HTTPONLY": True, "SESSION_COOKIE_NAME": "session", "SESSION_COOKIE_PARTITIONED": False, "SESSION_COOKIE_PATH": None, "SESSION_COOKIE_SAMESITE": None, "SESSION_COOKIE_SECURE": False, "SESSION_REFRESH_EACH_REQUEST": True, "TEMPLATES_AUTO_RELOAD": None, "TESTING": False, "TRAP_BAD_REQUEST_ERRORS": None, "TRAP_HTTP_EXCEPTIONS": False, } ) def __init__( self, import_name: str, static_url_path: str | None = None, static_folder: 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: """Construct a Quart web application. Use to create a new web application to which requests should be handled, as specified by the various attached url rules. See also :class:`~quart.static.PackageStatic` for additional constructor arguments. Arguments: import_name: The name at import of the application, use ``__name__`` unless there is a specific issue. host_matching: Optionally choose to match the host to the configured host on request (404 if no match). instance_path: Optional path to an instance folder, for deployment specific settings and files. instance_relative_config: If True load the config from a path relative to the instance path. Attributes: after_request_funcs: The functions to execute after a request has been handled. after_websocket_funcs: The functions to execute after a websocket has been handled. before_request_funcs: The functions to execute before handling a request. before_websocket_funcs: The functions to execute before handling a websocket. """ super().__init__( import_name, static_url_path, static_folder, static_host, host_matching, subdomain_matching, template_folder, instance_path, instance_relative_config, root_path, ) self.after_serving_funcs: list[Callable[[], Awaitable[None]]] = [] self.after_websocket_funcs: dict[ AppOrBlueprintKey, list[AfterWebsocketCallable] ] = defaultdict(list) self.background_tasks: set[asyncio.Task] = set() self.before_serving_funcs: list[Callable[[], Awaitable[None]]] = [] self.before_websocket_funcs: dict[ AppOrBlueprintKey, list[BeforeWebsocketCallable] ] = defaultdict(list) self.teardown_websocket_funcs: dict[ AppOrBlueprintKey, list[TeardownCallable] ] = defaultdict(list) self.while_serving_gens: list[AsyncGenerator[None]] = [] self.template_context_processors[None] = [_default_template_ctx_processor] self.cli = AppGroup() self.cli.name = self.name if self.has_static_folder: assert bool(static_host) == host_matching, ( "Invalid static_host/host_matching combination" ) self.add_url_rule( f"{self.static_url_path}/", "static", self.send_static_file, host=static_host, ) 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 Quart class. """ value = self.config["SEND_FILE_MAX_AGE_DEFAULT"] if value is None: return None if isinstance(value, timedelta): return int(value.total_seconds()) return value return None async def send_static_file(self, filename: str) -> Response: if not self.has_static_folder: raise RuntimeError("No static folder for this object") return await send_from_directory(self.static_folder, filename) async def open_resource( self, path: FilePath, mode: str = "rb", ) -> AiofilesContextManager: """Open a file for reading. Use as .. code-block:: python async with await app.open_resource(path) as file_: await file_.read() """ if mode not in {"r", "rb", "rt"}: raise ValueError("Files can only be opened for reading") return async_open(os.path.join(self.root_path, path), mode) # type: ignore async def open_instance_resource( self, path: FilePath, mode: str = "rb" ) -> AiofilesContextManager: """Open a file for reading. Use as .. code-block:: python async with await app.open_instance_resource(path) as file_: await file_.read() """ return async_open(self.instance_path / file_path_to_path(path), mode) # type: ignore def create_jinja_environment(self) -> Environment: # type: ignore """Create and return the jinja environment. This will create the environment based on the :attr:`jinja_options` and configuration settings. The environment will include the Quart globals by default. """ options = dict(self.jinja_options) if "autoescape" not in options: options["autoescape"] = self.select_jinja_autoescape if "auto_reload" not in options: options["auto_reload"] = self.config["TEMPLATES_AUTO_RELOAD"] jinja_env = self.jinja_environment(self, **options) jinja_env.globals.update( { "config": self.config, "g": g, "get_flashed_messages": get_flashed_messages, "request": request, "session": session, "url_for": self.url_for, } ) jinja_env.policies["json.dumps_function"] = self.json.dumps return jinja_env async def update_template_context(self, ctx: AppContext, context: dict) -> None: """Update the provided template context. This adds additional context from the various template context processors. Arguments: context: The context to update (mutate). """ names = [None] if has_request_context(): names.extend(reversed(ctx.request.blueprints)) # type: ignore elif has_websocket_context(): names.extend(reversed(ctx.websocket.blueprints)) # type: ignore extra_context: dict[str, Any] = {} for name in names: for processor in self.template_context_processors[name]: extra_context.update(await self.ensure_async(processor)()) # type: ignore[call-overload] original = context.copy() context.update(extra_context) context.update(original) @setupmethod def before_serving( self, func: T_before_serving, ) -> T_before_serving: """Add a before serving function. This will allow the function provided to be called once before anything is served (before any byte is received). This is designed to be used as a decorator, if used to decorate a synchronous function, the function will be wrapped in :func:`~quart.utils.run_sync` and run in a thread executor (with the wrapped function returned). An example usage, .. code-block:: python @app.before_serving async def func(): ... Arguments: func: The function itself. """ self.before_serving_funcs.append(func) return func @setupmethod def while_serving( self, func: T_while_serving, ) -> T_while_serving: """Add a while serving generator function. This will allow the generator provided to be invoked at startup and then again at shutdown. This is designed to be used as a decorator. An example usage, .. code-block:: python @app.while_serving async def func(): ... # Startup yield ... # Shutdown Arguments: func: The function itself. """ self.while_serving_gens.append(func()) return func @setupmethod def after_serving( self, func: T_after_serving, ) -> T_after_serving: """Add a after serving function. This will allow the function provided to be called once after anything is served (after last byte is sent). This is designed to be used as a decorator, if used to decorate a synchronous function, the function will be wrapped in :func:`~quart.utils.run_sync` and run in a thread executor (with the wrapped function returned). An example usage, .. code-block:: python @app.after_serving async def func(): ... Arguments: func: The function itself. """ self.after_serving_funcs.append(func) return func def create_url_adapter( self, request: BaseRequestWebsocket | None ) -> MapAdapter | None: """Create and return a URL adapter. This will create the adapter based on the request if present otherwise the app configuration. """ if request is not None: subdomain = ( (self.url_map.default_subdomain or None) if not self.subdomain_matching else None ) return self.url_map.bind_to_request( # type: ignore[attr-defined] request, subdomain, self.config["SERVER_NAME"] ) if self.config["SERVER_NAME"] is not None: scheme = "https" if self.config["PREFER_SECURE_URLS"] else "http" return self.url_map.bind(self.config["SERVER_NAME"], url_scheme=scheme) return None def websocket( self, rule: str, **options: Any, ) -> Callable[[T_websocket], T_websocket]: """Add a websocket to the application. This is designed to be used as a decorator, if used to decorate a synchronous function, the function will be wrapped in :func:`~quart.utils.run_sync` and run in a thread executor (with the wrapped function returned). An example usage, .. code-block:: python @app.websocket('/') async def websocket_route(): ... Arguments: rule: The path to route on, should start with a ``/``. endpoint: Optional endpoint name, if not present the function name is used. defaults: A dictionary of variables to provide automatically, use to provide a simpler default path for a route, e.g. to allow for ``/book`` rather than ``/book/0``, .. code-block:: python @app.websocket('/book', defaults={'page': 0}) @app.websocket('/book/') def book(page): ... host: The full host name for this route (should include subdomain if needed) - cannot be used with subdomain. subdomain: A subdomain for this specific route. strict_slashes: Strictly match the trailing slash present in the path. Will redirect a leaf (no slash) to a branch (with slash). """ def decorator(func: T_websocket) -> T_websocket: endpoint = options.pop("endpoint", None) self.add_websocket( rule, endpoint, func, **options, ) return func return decorator def add_websocket( self, rule: str, endpoint: str | None = None, view_func: WebsocketCallable | None = None, **options: Any, ) -> None: """Add a websocket url rule to the application. This is designed to be used on the application directly. An example usage, .. code-block:: python def websocket_route(): ... app.add_websocket('/', view_func=websocket_route) Arguments: rule: The path to route on, should start with a ``/``. endpoint: Optional endpoint name, if not present the function name is used. view_func: Callable that returns a response. defaults: A dictionary of variables to provide automatically, use to provide a simpler default path for a route, e.g. to allow for ``/book`` rather than ``/book/0``, .. code-block:: python @app.websocket('/book', defaults={'page': 0}) @app.websocket('/book/') def book(page): ... host: The full host name for this route (should include subdomain if needed) - cannot be used with subdomain. subdomain: A subdomain for this specific route. strict_slashes: Strictly match the trailing slash present in the path. Will redirect a leaf (no slash) to a branch (with slash). """ return self.add_url_rule( rule, endpoint, view_func, methods={"GET"}, websocket=True, **options, ) def url_for( self, endpoint: str, *, _anchor: str | None = None, _external: bool | None = None, _method: str | None = None, _scheme: str | None = None, **values: Any, ) -> str: """Return the url for a specific endpoint. This is most useful in templates and redirects to create a URL that can be used in the browser. Arguments: endpoint: The endpoint to build a url for, if prefixed with ``.`` it targets endpoint's in the current blueprint. _anchor: Additional anchor text to append (i.e. #text). _external: Return an absolute url for external (to app) usage. _method: The method to consider alongside the endpoint. _scheme: A specific scheme to use. values: The values to build into the URL, as specified in the endpoint rule. """ app_context = _cv_app.get(None) if has_request_context(): url_adapter = app_context.url_adapter if endpoint.startswith("."): if app_context.request.blueprint is not None: endpoint = app_context.request.blueprint + endpoint else: endpoint = endpoint[1:] if _external is None: _external = _scheme is not None elif has_websocket_context(): url_adapter = app_context.url_adapter if endpoint.startswith("."): if app_context.websocket.blueprint is not None: endpoint = app_context.websocket.blueprint + endpoint else: endpoint = endpoint[1:] if _external is None: _external = _scheme is not None elif app_context is not None: url_adapter = app_context.url_adapter if _external is None: _external = True else: url_adapter = self.create_url_adapter(None) if _external is None: _external = True if url_adapter is None: raise RuntimeError( "Unable to create a url adapter, try setting the SERVER_NAME" " config variable." ) if _scheme is not None and not _external: raise ValueError("External must be True for scheme usage") self.inject_url_defaults(endpoint, values) old_scheme = None if _scheme is not None: old_scheme = url_adapter.url_scheme url_adapter.url_scheme = _scheme try: url = url_adapter.build( endpoint, values, method=_method, force_external=_external ) except BuildError as error: return self.handle_url_build_error(error, endpoint, values) finally: if old_scheme is not None: url_adapter.url_scheme = old_scheme if _anchor is not None: quoted_anchor = quote(_anchor, safe="%!#$&'()*+,/:;=?@") url = f"{url}#{quoted_anchor}" return url def make_shell_context(self) -> dict: """Create a context for interactive shell usage. The :attr:`shell_context_processors` can be used to add additional context. """ context = {"app": self, "g": g} for processor in self.shell_context_processors: context.update(processor()) return context def run( self, host: str | None = None, port: int | None = None, debug: bool | None = None, use_reloader: bool = True, loop: asyncio.AbstractEventLoop | None = None, ca_certs: str | None = None, certfile: str | None = None, keyfile: str | None = None, **kwargs: Any, ) -> None: """Run this application. This is best used for development only, see Hypercorn for production servers. Arguments: host: Hostname to listen on. By default this is loopback only, use 0.0.0.0 to have the server listen externally. port: Port number to listen on. debug: If set enable (or disable) debug mode and debug output. use_reloader: Automatically reload on code changes. loop: Asyncio loop to create the server in, if None, take default one. If specified it is the caller's responsibility to close and cleanup the loop. ca_certs: Path to the SSL CA certificate file. certfile: Path to the SSL certificate file. keyfile: Path to the SSL key file. """ if kwargs: warnings.warn( f"Additional arguments, {','.join(kwargs.keys())}, are not supported.\n" "They may be supported by Hypercorn, which is the ASGI server Quart " "uses by default. This method is meant for development and debugging.", stacklevel=2, ) if use_reloader and os.environ.get("QUART_RUN_MAIN") != "true": run_reloader() return if loop is None: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) if "QUART_DEBUG" in os.environ: self.debug = get_debug_flag() if debug is not None: self.debug = debug loop.set_debug(self.debug) shutdown_event = asyncio.Event() def _signal_handler(*_: Any) -> None: shutdown_event.set() for signal_name in {"SIGINT", "SIGTERM", "SIGBREAK"}: if hasattr(signal, signal_name): try: loop.add_signal_handler( getattr(signal, signal_name), _signal_handler ) except NotImplementedError: # Add signal handler may not be implemented on Windows signal.signal(getattr(signal, signal_name), _signal_handler) server_name = self.config.get("SERVER_NAME") sn_host = None sn_port = None if server_name is not None: sn_host, _, sn_port = server_name.partition(":") if host is None: host = sn_host or "127.0.0.1" if port is None: port = int(sn_port or "5000") task = self.run_task( host, port, debug, ca_certs, certfile, keyfile, shutdown_trigger=shutdown_event.wait, # type: ignore ) tasks = [loop.create_task(task)] if os.environ.get("QUART_RUN_MAIN") == "true": tasks.append( loop.create_task(observe_changes(asyncio.sleep, shutdown_event)) ) print(f" * Serving Quart app '{self.name}'") # noqa: T201 print(f" * Debug mode: {self.debug or False}") # noqa: T201 print(" * Please use an ASGI server (e.g. Hypercorn) directly in production") # noqa: T201 scheme = "https" if certfile is not None and keyfile is not None else "http" print(f" * Running on {scheme}://{host}:{port} (CTRL + C to quit)") # noqa: T201 try: loop.run_until_complete(asyncio.gather(*tasks)) except MustReloadError: pass finally: try: _cancel_all_tasks(loop) loop.run_until_complete(loop.shutdown_asyncgens()) loop.run_until_complete(loop.shutdown_default_executor()) finally: asyncio.set_event_loop(None) loop.close() if shutdown_event.is_set(): sys.exit(3) def run_task( self, host: str = "127.0.0.1", port: int = 5000, debug: bool | None = None, ca_certs: str | None = None, certfile: str | None = None, keyfile: str | None = None, shutdown_trigger: Callable[..., Awaitable[None]] | None = None, ) -> Coroutine[None, None, None]: """Return a task that when awaited runs this application. This is best used for development only, see Hypercorn for production servers. Arguments: host: Hostname to listen on. By default this is loopback only, use 0.0.0.0 to have the server listen externally. port: Port number to listen on. debug: If set enable (or disable) debug mode and debug output. ca_certs: Path to the SSL CA certificate file. certfile: Path to the SSL certificate file. keyfile: Path to the SSL key file. """ config = HyperConfig() config.access_log_format = "%(h)s %(r)s %(s)s %(b)s %(D)s" config.accesslog = self.logger config.errorlog = self.logger if host.startswith("unix:") or host.startswith("fd:"): config.bind = [f"{host}"] else: config.bind = [f"{host}:{port}"] config.ca_certs = ca_certs config.certfile = certfile if debug is not None: self.debug = debug config.keyfile = keyfile return serve(self, config, shutdown_trigger=shutdown_trigger) def test_client( self, use_cookies: bool = True, **kwargs: Any ) -> TestClientProtocol: """Creates and returns a test client.""" return self.test_client_class(self, use_cookies=use_cookies, **kwargs) def test_cli_runner(self, **kwargs: Any) -> QuartCliRunner: """Creates and returns a CLI test runner.""" return self.test_cli_runner_class(self, **kwargs) @setupmethod def before_websocket( self, func: T_before_websocket, ) -> T_before_websocket: """Add a before websocket function. This is designed to be used as a decorator, if used to decorate a synchronous function, the function will be wrapped in :func:`~quart.utils.run_sync` and run in a thread executor (with the wrapped function returned). An example usage, .. code-block:: python @app.before_websocket async def func(): ... Arguments: func: The before websocket function itself. """ self.before_websocket_funcs[None].append(func) return func @setupmethod def after_websocket( self, func: T_after_websocket, ) -> T_after_websocket: """Add an after websocket function. This is designed to be used as a decorator, if used to decorate a synchronous function, the function will be wrapped in :func:`~quart.utils.run_sync` and run in a thread executor (with the wrapped function returned). An example usage, .. code-block:: python @app.after_websocket async def func(response): return response Arguments: func: The after websocket function itself. """ self.after_websocket_funcs[None].append(func) return func @setupmethod def teardown_websocket( self, func: T_teardown, ) -> T_teardown: """Add a teardown websocket function. This is designed to be used as a decorator, if used to decorate a synchronous function, the function will be wrapped in :func:`~quart.utils.run_sync` and run in a thread executor (with the wrapped function returned). An example usage, .. code-block:: python @app.teardown_websocket async def func(): ... Arguments: func: The teardown websocket function itself. """ self.teardown_websocket_funcs[None].append(func) return func async def handle_http_exception( self, ctx: AppContext, error: HTTPException ) -> HTTPException | ResponseReturnValue: """Handle a HTTPException subclass error. This will attempt to find a handler for the error and if fails will fall back to the error response. """ if error.code is None: return error if isinstance(error, RoutingException): return error blueprints = [] if has_request_context(): blueprints = ctx.request.blueprints elif has_websocket_context(): blueprints = ctx.websocket.blueprints handler = self._find_error_handler(error, blueprints) if handler is None: return error else: return await self.ensure_async(handler)(error) # type: ignore[return-value] async def handle_user_exception( self, ctx: AppContext, error: Exception ) -> HTTPException | ResponseReturnValue: """Handle an exception that has been raised. This should forward :class:`~quart.exception.HTTPException` to :meth:`handle_http_exception`, then attempt to handle the error. If it cannot it should reraise the error. """ if isinstance(error, BadRequestKeyError) and ( self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] ): error.show_exception = True if isinstance(error, HTTPException) and not self.trap_http_exception(error): return await self.handle_http_exception(ctx, error) blueprints = [] if has_request_context(): blueprints = ctx.request.blueprints elif has_websocket_context(): blueprints = ctx.websocket.blueprints handler = self._find_error_handler(error, blueprints) if handler is None: raise error return await self.ensure_async(handler)(error) # type: ignore[return-value] async def handle_exception( self, ctx: AppContext, error: Exception ) -> ResponseTypes: """Handle an uncaught exception. By default this switches the error response to a 500 internal server error. """ exc_info = sys.exc_info() await got_request_exception.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore[arg-type] exception=error, ) 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 error: raise raise error self.log_exception(ctx, exc_info) server_error: InternalServerError | ResponseReturnValue server_error = InternalServerError(original_exception=error) handler = self._find_error_handler(server_error, ctx.request.blueprints) if handler is not None: server_error = await self.ensure_async(handler)(server_error) # type: ignore[assignment] return await self.finalize_request(ctx, server_error, from_error_handler=True) async def handle_websocket_exception( self, ctx: AppContext, error: Exception ) -> ResponseTypes | None: """Handle an uncaught exception. By default this logs the exception and then re-raises it. """ exc_info = sys.exc_info() await got_websocket_exception.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore[arg-type] exception=error, ) 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 error: raise raise error self.log_exception(ctx, exc_info) server_error: InternalServerError | ResponseReturnValue server_error = InternalServerError(original_exception=error) handler = self._find_error_handler(server_error, ctx.websocket.blueprints) if handler is not None: server_error = await self.ensure_async(handler)(server_error) # type: ignore[assignment] return await self.finalize_websocket(ctx, server_error, from_error_handler=True) def log_exception( self, ctx: AppContext, exception_info: ( tuple[type, BaseException, TracebackType] | tuple[None, None, None] ), ) -> None: """Log a exception to the :attr:`logger`. By default this is only invoked for unhandled exceptions. """ if has_request_context(): request_ = ctx.request self.logger.error( f"Exception on request {request_.method} {request_.path}", exc_info=exception_info, ) elif has_websocket_context(): websocket_ = ctx.websocket self.logger.error( f"Exception on websocket {websocket_.path}", exc_info=exception_info ) else: self.logger.error("Exception", exc_info=exception_info) @overload def ensure_async( self, func: Callable[P, Awaitable[T]] ) -> Callable[P, Awaitable[T]]: ... @overload def ensure_async(self, func: Callable[P, T]) -> Callable[P, Awaitable[T]]: ... def ensure_async( self, func: Callable[P, Awaitable[T]] | Callable[P, T] ) -> Callable[P, Awaitable[T]]: """Ensure that the returned func is async and calls the func. .. versionadded:: 0.11 Override if you wish to change how synchronous functions are run. Before Quart 0.11 this did not run the synchronous code in an executor. """ if iscoroutinefunction(func): return func else: return self.sync_to_async(cast(Callable[P, T], func)) def sync_to_async(self, func: Callable[P, T]) -> Callable[P, Awaitable[T]]: """Return a async function that will run the synchronous function *func*. This can be used as so,:: result = await app.sync_to_async(func)(*args, **kwargs) Override this method to change how the app converts sync code to be asynchronously callable. """ return run_sync(func) async def do_teardown_request( self, ctx: AppContext, exc: BaseException | None = None ) -> None: """Teardown the request, calling the teardown functions. Arguments: exc: Any exception not handled that has caused the request to teardown. request_context: The request context, optional as Flask omits this argument. """ collect_errors = _CollectErrors() names = [*ctx._request_websocket.blueprints, None] for name in names: for function in reversed(self.teardown_request_funcs[name]): with collect_errors: await self.ensure_async(function)(exc) with collect_errors: await request_tearing_down.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore[arg-type] exc=exc, ) collect_errors.raise_any("Errors during request teardown") async def do_teardown_websocket( self, ctx: AppContext, exc: BaseException | None = None, ) -> None: """Teardown the websocket, calling the teardown functions. Arguments: exc: Any exception not handled that has caused the websocket to teardown. websocket_context: The websocket context, optional as Flask omits this argument. """ collect_errors = _CollectErrors() names = [*ctx._request_websocket.blueprints, None] for name in names: for function in reversed(self.teardown_websocket_funcs[name]): with collect_errors: await self.ensure_async(function)(exc) with collect_errors: await websocket_tearing_down.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore[arg-type] exc=exc, ) collect_errors.raise_any("Errors during websocket teardown") async def do_teardown_appcontext( self, ctx: AppContext, exc: BaseException | None = None, ) -> None: """Teardown the app (context), calling the teardown functions.""" collect_errors = _CollectErrors() for function in self.teardown_appcontext_funcs: with collect_errors: await self.ensure_async(function)(exc) with collect_errors: await appcontext_tearing_down.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore[arg-type] exc=exc, ) collect_errors.raise_any("Errors during app teardown") def app_context(self) -> AppContext: """Create and return an app context. This is best used within a context, i.e. .. code-block:: python async with app.app_context(): ... """ return AppContext(self) def request_context(self, request: Request) -> AppContext: """Create and return a request context. Use the :meth:`test_request_context` whilst testing. This is best used within a context, i.e. .. code-block:: python async with app.request_context(request): ... Arguments: request: A request to build a context around. """ return AppContext(self, request=request) def websocket_context(self, websocket: Websocket) -> AppContext: """Create and return a websocket context. Use the :meth:`test_websocket_context` whilst testing. This is best used within a context, i.e. .. code-block:: python async with app.websocket_context(websocket): ... Arguments: websocket: A websocket to build a context around. """ return AppContext(self, websocket=websocket) def test_app(self) -> TestAppProtocol: return self.test_app_class(self) def test_request_context( self, path: str, *, method: str = "GET", headers: dict | Headers | None = None, query_string: dict | None = None, scheme: str = "http", send_push_promise: Callable[[str, Headers], Awaitable[None]] = no_op_push, data: str | bytes | None = None, form: dict | None = None, json: Any = sentinel, root_path: str = "", http_version: str = "1.1", scope_base: dict | None = None, auth: Authorization | tuple[str, str] | None = None, subdomain: str | None = None, ) -> AppContext: """Create a request context for testing purposes. This is best used for testing code within request contexts. It is a simplified wrapper of :meth:`request_context`. It is best used in a with block, i.e. .. code-block:: python async with app.test_request_context("/", method="GET"): ... Arguments: path: Request path. method: HTTP verb headers: Headers to include in the request. query_string: To send as a dictionary, alternatively the query_string can be determined from the path. scheme: Scheme for the request, default http. """ headers, path, query_string_bytes = make_test_headers_path_and_query_string( self, path, headers, query_string, auth, subdomain, ) request_body, body_headers = make_test_body_with_headers( data=data, form=form, json=json ) headers.update(**body_headers) scope = make_test_scope( "http", path, method, headers, query_string_bytes, scheme, root_path, http_version, scope_base, ) request = self.request_class( method, scheme, path, query_string_bytes, headers, root_path, http_version, send_push_promise=send_push_promise, scope=scope, ) request.body.set_result(request_body) return self.request_context(request) def add_background_task(self, func: Callable, *args: Any, **kwargs: Any) -> None: async def _wrapper() -> None: async with self.app_context() as ctx: try: await self.ensure_async(func)(*args, **kwargs) except Exception as error: await self.handle_background_exception(ctx, error) task = asyncio.get_event_loop().create_task(_wrapper()) self.background_tasks.add(task) task.add_done_callback(self.background_tasks.discard) async def handle_background_exception( self, ctx: AppContext, error: Exception ) -> None: await got_background_exception.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore[arg-type] exception=error, ) self.log_exception(ctx, sys.exc_info()) async def make_default_options_response(self, ctx: AppContext) -> Response: """This is the default route function for OPTIONS requests.""" methods = ctx.url_adapter.allowed_methods() return self.response_class("", headers={"Allow": ", ".join(methods)}) async def make_response( self, result: ResponseReturnValue | HTTPException ) -> ResponseTypes: """Make a Response from the result of the route handler. The result itself can either be: - A Response object (or subclass). - A tuple of a ResponseValue and a header dictionary. - A tuple of a ResponseValue, status code and a header dictionary. A ResponseValue is either a Response object (or subclass) or a str. """ headers: HeadersValue | None = None status: StatusCode | None = None if isinstance(result, tuple): if len(result) == 3: value, status, headers = result elif len(result) == 2: value, status_or_headers = result if isinstance(status_or_headers, (Headers, dict, list)): headers = status_or_headers status = None elif status_or_headers is not None: status = status_or_headers # type: ignore[assignment] else: raise TypeError( """The response value returned must be either (body, status), (body, headers), or (body, status, headers)""" ) else: value = result if value is None: raise TypeError( "The response value returned by the view function cannot be None" ) response: ResponseTypes if isinstance(value, HTTPException): response = value.get_response() elif not isinstance(value, (Response, WerkzeugResponse)): if ( isinstance(value, (str, bytes, bytearray)) or isgenerator(value) or isasyncgen(value) ): response = self.response_class(value) elif isinstance(value, (list, dict)): response = self.json.response(value) # type: ignore[assignment] else: raise TypeError( f"The response value type ({type(value).__name__}) is not valid" ) else: response = value if status is not None: response.status_code = int(status) if headers is not None: response.headers.update(headers) return response async def handle_request(self, request: Request) -> ResponseTypes: async with self.request_context(request) as ctx: try: return await self.full_dispatch_request(ctx) except asyncio.CancelledError: raise # CancelledErrors should be handled by serving code. except Exception as error: return await self.handle_exception(ctx, error) finally: if request.scope.get("_quart._preserve_context", False): self._preserved_context = ctx.copy() async def handle_websocket(self, websocket: Websocket) -> ResponseTypes | None: async with self.websocket_context(websocket) as ctx: try: return await self.full_dispatch_websocket(ctx) except asyncio.CancelledError: raise # CancelledErrors should be handled by serving code. except Exception as error: return await self.handle_websocket_exception(ctx, error) finally: if websocket.scope.get("_quart._preserve_context", False): self._preserved_context = ctx.copy() async def full_dispatch_request(self, ctx: AppContext) -> ResponseTypes: """Adds pre and post processing to the request dispatching. Arguments: request_context: The request context, optional as Flask omits this argument. """ try: await request_started.send_async(self, _sync_wrapper=self.ensure_async) # type: ignore result: ResponseReturnValue | HTTPException | None result = await self.preprocess_request(ctx) if result is None: result = await self.dispatch_request(ctx) except Exception as error: result = await self.handle_user_exception(ctx, error) return await self.finalize_request(ctx, result) async def full_dispatch_websocket(self, ctx: AppContext) -> ResponseTypes | None: """Adds pre and post processing to the websocket dispatching. Arguments: websocket_context: The websocket context, optional to match the Flask convention. """ try: await websocket_started.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore ) result: ResponseReturnValue | HTTPException | None result = await self.preprocess_websocket(ctx) if result is None: result = await self.dispatch_websocket(ctx) except Exception as error: result = await self.handle_user_exception(ctx, error) return await self.finalize_websocket(ctx, result) async def preprocess_request(self, ctx: AppContext) -> ResponseReturnValue | None: """Preprocess the request i.e. call before_request functions. Arguments: request_context: The request context, optional as Flask omits this argument. """ names = [None, *reversed(ctx.request.blueprints)] for name in names: for processor in self.url_value_preprocessors[name]: processor(ctx.request.endpoint, ctx.request.view_args) for name in names: for function in self.before_request_funcs[name]: result = await self.ensure_async(function)() if result is not None: return result # type: ignore[return-value] return None async def preprocess_websocket(self, ctx: AppContext) -> ResponseReturnValue | None: """Preprocess the websocket i.e. call before_websocket functions. Arguments: websocket_context: The websocket context, optional as Flask omits this argument. """ names = [ None, *reversed(ctx.websocket.blueprints), ] for name in names: for processor in self.url_value_preprocessors[name]: processor(ctx.websocket.endpoint, ctx.websocket.view_args) for name in names: for function in self.before_websocket_funcs[name]: result = await self.ensure_async(function)() if result is not None: return result # type: ignore[return-value] return None def raise_routing_exception(self, request: BaseRequestWebsocket) -> NoReturn: raise request.routing_exception async def dispatch_request(self, ctx: AppContext) -> ResponseReturnValue: """Dispatch the request to the view function. Arguments: request_context: The request context, optional as Flask omits this argument. """ request_ = ctx.request if request_.routing_exception is not None: self.raise_routing_exception(request_) if request_.method == "OPTIONS" and request_.url_rule.provide_automatic_options: return await self.make_default_options_response(ctx) handler = self.view_functions[request_.url_rule.endpoint] return await self.ensure_async(handler)(**request_.view_args) # type: ignore[return-value] async def dispatch_websocket(self, ctx: AppContext) -> ResponseReturnValue | None: """Dispatch the websocket to the view function. Arguments: websocket_context: The websocket context, optional to match the Flask convention. """ websocket_ = ctx.websocket if websocket_.routing_exception is not None: self.raise_routing_exception(websocket_) handler = self.view_functions[websocket_.url_rule.endpoint] return await self.ensure_async(handler)(**websocket_.view_args) # type: ignore[return-value] async def finalize_request( self, ctx: AppContext, result: ResponseReturnValue | HTTPException, from_error_handler: bool = False, ) -> ResponseTypes: """Turns the view response return value into a response. Arguments: result: The result of the request to finalize into a response. request_context: The request context, optional as Flask omits this argument. """ response = await self.make_response(result) try: response = await self.process_response(ctx, response) await request_finished.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore[arg-type] response=response, ) except Exception: if not from_error_handler: raise self.logger.exception("Request finalizing errored") return response async def finalize_websocket( self, ctx: AppContext, result: ResponseReturnValue | HTTPException, from_error_handler: bool = False, ) -> ResponseTypes | None: """Turns the view response return value into a response. Arguments: result: The result of the websocket to finalize into a response. websocket_context: The websocket context, optional as Flask omits this argument. """ if result is not None: response = await self.make_response(result) else: response = None try: response = await self.postprocess_websocket(ctx, response) await websocket_finished.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore[arg-type] response=response, ) except Exception: if not from_error_handler: raise self.logger.exception("Websocket finalizing errored") return response async def process_response( self, ctx: AppContext, response: ResponseTypes, ) -> ResponseTypes: """Postprocess the request acting on the response. Arguments: response: The response after the request is finalized. request_context: The request context, optional as Flask omits this argument. """ names = [*ctx.request.blueprints, None] for function in ctx._after_request_functions: response = cast(ResponseTypes, await self.ensure_async(function)(response)) for name in names: for function in reversed(self.after_request_funcs[name]): response = cast( ResponseTypes, await self.ensure_async(function)(response) ) session_ = ctx.session if not self.session_interface.is_null_session(session_): await self.ensure_async(self.session_interface.save_session)( self, session_, response ) return response async def postprocess_websocket( self, ctx: AppContext, response: ResponseTypes | None, ) -> ResponseTypes: """Postprocess the websocket acting on the response. Arguments: response: The response after the websocket is finalized. websocket_context: The websocket context, optional as Flask omits this argument. """ names = [*ctx.websocket.blueprints, None] for function in ctx._after_websocket_functions: response = cast(ResponseTypes, await self.ensure_async(function)(response)) for name in names: for function in reversed(self.after_websocket_funcs[name]): response = cast( ResponseTypes, await self.ensure_async(function)(response) ) session_ = ctx.session if not self.session_interface.is_null_session(session_): await self.session_interface.save_session(self, session_, response) return response async def __call__( self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: """Called by ASGI servers. The related :meth:`~quart.app.Quart.asgi_app` is called, allowing for middleware usage whilst keeping the top level app a :class:`~quart.app.Quart` instance. """ await self.asgi_app(scope, receive, send) async def asgi_app( self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: """This handles ASGI calls, it can be wrapped in middleware. When using middleware with Quart it is preferable to wrap this method rather than the app itself. This is to ensure that the app is an instance of this class - which allows the quart cli to work correctly. To use this feature simply do, .. code-block:: python app.asgi_app = middleware(app.asgi_app) """ asgi_handler: ASGIHTTPProtocol | ASGILifespanProtocol | ASGIWebsocketProtocol if scope["type"] == "http": asgi_handler = self.asgi_http_class(self, scope) elif scope["type"] == "websocket": asgi_handler = self.asgi_websocket_class(self, scope) elif scope["type"] == "lifespan": asgi_handler = self.asgi_lifespan_class(self, scope) else: raise RuntimeError("ASGI Scope type is unknown") await asgi_handler(receive, send) async def startup(self) -> None: self.shutdown_event = self.event_class() async with self.app_context() as ctx: try: for func in self.before_serving_funcs: await self.ensure_async(func)() for gen in self.while_serving_gens: await gen.__anext__() except Exception as error: await got_serving_exception.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore[arg-type] exception=error, ) self.log_exception(ctx, sys.exc_info()) raise async def shutdown(self) -> None: self.shutdown_event.set() try: await asyncio.wait_for( asyncio.gather(*self.background_tasks), timeout=self.config["BACKGROUND_TASK_SHUTDOWN_TIMEOUT"], ) except TimeoutError: await cancel_tasks(self.background_tasks) async with self.app_context() as ctx: try: for gen in reversed(self.while_serving_gens): try: await gen.__anext__() except StopAsyncIteration: pass else: raise RuntimeError("While serving generator didn't terminate") for func in self.after_serving_funcs: await self.ensure_async(func)() except Exception as error: await got_serving_exception.send_async( self, _sync_wrapper=self.ensure_async, # type: ignore[arg-type] exception=error, ) self.log_exception(ctx, sys.exc_info()) raise def _cancel_all_tasks(loop: asyncio.AbstractEventLoop) -> None: tasks = [task for task in asyncio.all_tasks(loop) if not task.done()] if not tasks: return for task in tasks: task.cancel() loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) for task in tasks: if not task.cancelled() and task.exception() is not None: loop.call_exception_handler( { "message": "unhandled exception during shutdown", "exception": task.exception(), "task": task, } ) pallets-quart-6b318bd/src/quart/asgi.py000066400000000000000000000401041524460054700201510ustar00rootroot00000000000000from __future__ import annotations import asyncio import warnings from functools import partial from functools import wraps from typing import cast from typing import TYPE_CHECKING from urllib.parse import urlparse from hypercorn.typing import ASGIReceiveCallable from hypercorn.typing import ASGISendCallable from hypercorn.typing import ASGISendEvent from hypercorn.typing import HTTPResponseBodyEvent from hypercorn.typing import HTTPResponseStartEvent from hypercorn.typing import HTTPScope from hypercorn.typing import LifespanScope from hypercorn.typing import LifespanShutdownCompleteEvent from hypercorn.typing import LifespanShutdownFailedEvent from hypercorn.typing import LifespanStartupCompleteEvent from hypercorn.typing import LifespanStartupFailedEvent from hypercorn.typing import WebsocketAcceptEvent from hypercorn.typing import WebsocketCloseEvent from hypercorn.typing import WebsocketResponseBodyEvent from hypercorn.typing import WebsocketResponseStartEvent from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Headers from werkzeug.wrappers import Response as WerkzeugResponse from .debug import traceback_response from .signals import websocket_received from .signals import websocket_sent from .typing import ResponseTypes from .utils import encode_headers from .wrappers import Request # noqa: F401 from .wrappers import Response # noqa: F401 from .wrappers import Websocket # noqa: F401 from .wrappers.base import ClientDisconnectedError if TYPE_CHECKING: from .app import Quart # noqa: F401 class ASGIHTTPConnection: def __init__(self, app: Quart, scope: HTTPScope) -> None: self.app = app self.scope = scope self._disconnected = False async def __call__( self, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: send = _convert_os_error(send) request = self._create_request_from_scope(send) async with asyncio.TaskGroup() as task_group: request_task = task_group.create_task(self.handle_request(request, send)) task_group.create_task(self.handle_messages(request, receive, request_task)) async def handle_messages( self, request: Request, receive: ASGIReceiveCallable, request_task: asyncio.Task ) -> None: while True: message = await receive() if message["type"] == "http.request": await request.body.put(message.get("body", b"")) if not message.get("more_body", False): request.body.set_complete() elif message["type"] == "http.disconnect": self._disconnected = True request.body.disconnect() request_task.cancel() return def _create_request_from_scope(self, send: ASGISendCallable) -> Request: headers = Headers() headers["Remote-Addr"] = (self.scope.get("client") or [""])[0] for name, value in self.scope["headers"]: headers.add(name.decode("latin1").title(), value.decode("latin1")) if self.scope["http_version"] < "1.1": headers.setdefault("Host", self.app.config["SERVER_NAME"] or "") path = self.scope["path"] path = path if path[0] == "/" else urlparse(path).path root_path = self.scope.get("root_path", "") if root_path != "": path = _normalise_path(path, root_path) return self.app.request_class( self.scope["method"], self.scope["scheme"], path, self.scope["query_string"], headers, self.scope.get("root_path", ""), self.scope["http_version"], max_content_length=self.app.config["MAX_CONTENT_LENGTH"], body_timeout=self.app.config["BODY_TIMEOUT"], send_push_promise=partial(self._send_push_promise, send), scope=self.scope, ) async def handle_request(self, request: Request, send: ASGISendCallable) -> None: try: response = await self.app.handle_request(request) except ClientDisconnectedError: return except Exception as error: response = await _handle_exception(self.app, error) if isinstance(response, Response) and response.timeout != Ellipsis: timeout = cast(float | None, response.timeout) else: timeout = self.app.config["RESPONSE_TIMEOUT"] try: await asyncio.wait_for(self._send_response(send, response), timeout=timeout) except TimeoutError: pass async def _send_response( self, send: ASGISendCallable, response: ResponseTypes ) -> None: if self._disconnected: raise ClientDisconnectedError() await send( cast( HTTPResponseStartEvent, { "type": "http.response.start", "status": response.status_code, "headers": encode_headers(response.headers), }, ) ) if isinstance(response, WerkzeugResponse): for data in response.response: body = data.encode() if isinstance(data, str) else data await send( cast( HTTPResponseBodyEvent, {"type": "http.response.body", "body": body, "more_body": True}, ) ) else: async with response.response as response_body: async for data in response_body: body = data.encode() if isinstance(data, str) else data await send( cast( HTTPResponseBodyEvent, { "type": "http.response.body", "body": body, "more_body": True, }, ) ) await send( cast( HTTPResponseBodyEvent, {"type": "http.response.body", "body": b"", "more_body": False}, ) ) async def _send_push_promise( self, send: ASGISendCallable, path: str, headers: Headers ) -> None: if self._disconnected: raise ClientDisconnectedError() extensions = self.scope.get("extensions", {}) or {} if "http.response.push" in extensions: await send( { "type": "http.response.push", "path": path, "headers": encode_headers(headers), } ) class ASGIWebsocketConnection: def __init__(self, app: Quart, scope: WebsocketScope) -> None: self.app = app self.scope = scope self._accepted = False self._closed = False self._disconnected = False async def __call__( self, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: send = _convert_os_error(send) websocket = self._create_websocket_from_scope(send) async with asyncio.TaskGroup() as task_group: websocket_task = task_group.create_task( self.handle_websocket(websocket, send) ) task_group.create_task( self.handle_messages(websocket, receive, websocket_task) ) async def handle_messages( self, websocket: Websocket, receive: ASGIReceiveCallable, websocket_task: asyncio.Task, ) -> None: while True: event = await receive() if event["type"] == "websocket.receive": message = event.get("bytes") or event["text"] await websocket_received.send_async(message) await websocket.buffer.put(message) elif event["type"] == "websocket.disconnect": self._disconnected = True websocket.buffer.disconnect() websocket_task.cancel() return def _create_websocket_from_scope(self, send: ASGISendCallable) -> Websocket: headers = Headers() headers["Remote-Addr"] = (self.scope.get("client") or [""])[0] for name, value in self.scope["headers"]: headers.add(name.decode("latin1").title(), value.decode("latin1")) path = self.scope["path"] path = path if path[0] == "/" else urlparse(path).path root_path = self.scope.get("root_path", "") if root_path != "": path = _normalise_path(path, root_path) return self.app.websocket_class( path, self.scope["query_string"], self.scope["scheme"], headers, self.scope.get("root_path", ""), self.scope.get("http_version", "1.1"), list(self.scope.get("subprotocols", [])), partial(self.send_data, send), partial(self.accept_connection, send), partial(self.close_connection, send), scope=self.scope, ) async def handle_websocket( self, websocket: Websocket, send: ASGISendCallable ) -> None: try: response = await self.app.handle_websocket(websocket) except ClientDisconnectedError: return except Exception as error: response = await _handle_exception(self.app, error) if response is not None and not self._accepted: extensions = self.scope.get("extensions", {}) or {} if "websocket.http.response" in extensions: headers = [ (key.lower().encode(), value.encode()) for key, value in response.headers.items() ] await send( cast( WebsocketResponseStartEvent, { "type": "websocket.http.response.start", "status": response.status_code, "headers": headers, }, ) ) if isinstance(response, WerkzeugResponse): for data in response.response: await send( cast( WebsocketResponseBodyEvent, { "type": "websocket.http.response.body", "body": data, "more_body": True, }, ) ) elif isinstance(response, Response): async with response.response as body: async for data in body: await send( cast( WebsocketResponseBodyEvent, { "type": "websocket.http.response.body", "body": data, "more_body": True, }, ) ) await send( cast( WebsocketResponseBodyEvent, { "type": "websocket.http.response.body", "body": b"", "more_body": False, }, ) ) elif not self._closed: await send( cast(WebsocketCloseEvent, {"type": "websocket.close", "code": 1000}) ) elif self._accepted and not self._closed: await send( cast(WebsocketCloseEvent, {"type": "websocket.close", "code": 1000}) ) async def send_data(self, send: ASGISendCallable, data: str | bytes) -> None: if self._disconnected: raise ClientDisconnectedError() if isinstance(data, str): await send({"type": "websocket.send", "bytes": None, "text": data}) else: await send({"type": "websocket.send", "bytes": data, "text": None}) await websocket_sent.send_async(data) async def accept_connection( self, send: ASGISendCallable, headers: Headers, subprotocol: str | None ) -> None: if not self._accepted: message: WebsocketAcceptEvent = { "headers": [], "subprotocol": subprotocol, "type": "websocket.accept", } spec_version = _convert_version( self.scope.get("asgi", {}).get("spec_version", "2.0") ) if spec_version > [2, 0]: message["headers"] = encode_headers(headers) elif headers: warnings.warn( "The ASGI Server does not support accept headers, headers not sent", stacklevel=1, ) self._accepted = True await send(message) async def close_connection( self, send: ASGISendCallable, code: int, reason: str ) -> None: if self._closed: raise RuntimeError("Cannot close websocket multiple times") spec_version = _convert_version( self.scope.get("asgi", {}).get("spec_version", "2.0") ) if spec_version >= [2, 3]: await send({"type": "websocket.close", "code": code, "reason": reason}) else: await send({"type": "websocket.close", "code": code}) # type: ignore self._closed = True class ASGILifespan: def __init__(self, app: Quart, scope: LifespanScope) -> None: self.app = app async def __call__( self, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: while True: event = await receive() if event["type"] == "lifespan.startup": try: await self.app.startup() except Exception as error: await send( cast( LifespanStartupFailedEvent, {"type": "lifespan.startup.failed", "message": str(error)}, ), ) else: await send( cast( LifespanStartupCompleteEvent, {"type": "lifespan.startup.complete"}, ) ) elif event["type"] == "lifespan.shutdown": try: await self.app.shutdown() except Exception as error: await send( cast( LifespanShutdownFailedEvent, {"type": "lifespan.shutdown.failed", "message": str(error)}, ), ) else: await send( cast( LifespanShutdownCompleteEvent, {"type": "lifespan.shutdown.complete"}, ), ) break def _convert_version(raw: str) -> list[int]: return list(map(int, raw.split("."))) async def _handle_exception(app: Quart, error: Exception) -> Response: show_error = app.debug or app.testing if show_error: return await traceback_response(error) else: raise error def _normalise_path(path: str, root_path: str) -> str: if path == root_path or not path.startswith(root_path): return " " # Invalid in paths, hence will result in 404 return path.removeprefix(root_path) def _convert_os_error(send: ASGISendCallable) -> ASGISendCallable: @wraps(send) async def new_send(message: ASGISendEvent) -> None: try: await send(message) except OSError: raise ClientDisconnectedError() from None return new_send pallets-quart-6b318bd/src/quart/blueprints.py000066400000000000000000000344001524460054700214170ustar00rootroot00000000000000from __future__ import annotations import os import typing as t from collections import defaultdict from datetime import timedelta from aiofiles import open as async_open from aiofiles.base import AiofilesContextManager from flask.sansio.app import App from flask.sansio.blueprints import Blueprint as SansioBlueprint # noqa from flask.sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa from flask.sansio.scaffold import setupmethod from .cli import AppGroup from .globals import current_app from .helpers import send_from_directory from .typing import AfterServingCallable from .typing import AfterWebsocketCallable from .typing import AppOrBlueprintKey from .typing import BeforeServingCallable from .typing import BeforeWebsocketCallable from .typing import FilePath from .typing import TeardownCallable from .typing import WebsocketCallable from .typing import WhileServingCallable if t.TYPE_CHECKING: from .wrappers import Response T_after_serving = t.TypeVar("T_after_serving", bound=AfterServingCallable) T_after_websocket = t.TypeVar("T_after_websocket", bound=AfterWebsocketCallable) T_before_serving = t.TypeVar("T_before_serving", bound=BeforeServingCallable) T_before_websocket = t.TypeVar("T_before_websocket", bound=BeforeWebsocketCallable) T_teardown = t.TypeVar("T_teardown", bound=TeardownCallable) T_websocket = t.TypeVar("T_websocket", bound=WebsocketCallable) T_while_serving = t.TypeVar("T_while_serving", bound=WhileServingCallable) class Blueprint(SansioBlueprint): """A blueprint is a collection of application properties. The application properties include routes, error handlers, and before and after request functions. It is useful to produce modular code as it allows the properties to be defined in a blueprint thereby deferring the addition of these properties to the app. """ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: super().__init__(*args, **kwargs) self.cli = AppGroup() self.cli.name = self.name self.after_websocket_funcs: dict[ AppOrBlueprintKey, list[AfterWebsocketCallable] ] = defaultdict(list) self.before_websocket_funcs: dict[ AppOrBlueprintKey, list[BeforeWebsocketCallable] ] = defaultdict(list) self.teardown_websocket_funcs: dict[ AppOrBlueprintKey, list[TeardownCallable] ] = defaultdict(list) 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 Quart class. """ 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 return None async def send_static_file(self, filename: str) -> Response: if not self.has_static_folder: raise RuntimeError("No static folder for this object") return await send_from_directory(self.static_folder, filename) async def open_resource( self, path: FilePath, mode: str = "rb", ) -> AiofilesContextManager: """Open a file for reading. Use as .. code-block:: python async with await app.open_resource(path) as file_: await file_.read() """ if mode not in {"r", "rb", "rt"}: raise ValueError("Files can only be opened for reading") return async_open(os.path.join(self.root_path, path), mode) # type: ignore def websocket( self, rule: str, **options: t.Any, ) -> t.Callable[[T_websocket], T_websocket]: """Add a websocket to the application. This is designed to be used as a decorator, if used to decorate a synchronous function, the function will be wrapped in :func:`~quart.utils.run_sync` and run in a thread executor (with the wrapped function returned). An example usage, .. code-block:: python @app.websocket('/') async def websocket_route(): ... Arguments: rule: The path to route on, should start with a ``/``. endpoint: Optional endpoint name, if not present the function name is used. defaults: A dictionary of variables to provide automatically, use to provide a simpler default path for a route, e.g. to allow for ``/book`` rather than ``/book/0``, .. code-block:: python @app.websocket('/book', defaults={'page': 0}) @app.websocket('/book/') def book(page): ... host: The full host name for this route (should include subdomain if needed) - cannot be used with subdomain. subdomain: A subdomain for this specific route. strict_slashes: Strictly match the trailing slash present in the path. Will redirect a leaf (no slash) to a branch (with slash). """ def decorator(func: T_websocket) -> T_websocket: endpoint = options.pop("endpoint", None) self.add_websocket( rule, endpoint, func, **options, ) return func return decorator def add_websocket( self, rule: str, endpoint: str | None = None, view_func: WebsocketCallable | None = None, **options: t.Any, ) -> None: """Add a websocket url rule to the application. This is designed to be used on the application directly. An example usage, .. code-block:: python def websocket_route(): ... app.add_websocket('/', websocket_route) Arguments: rule: The path to route on, should start with a ``/``. endpoint: Optional endpoint name, if not present the function name is used. view_func: Callable that returns a response. defaults: A dictionary of variables to provide automatically, use to provide a simpler default path for a route, e.g. to allow for ``/book`` rather than ``/book/0``, .. code-block:: python @app.websocket('/book', defaults={'page': 0}) @app.websocket('/book/') def book(page): ... host: The full host name for this route (should include subdomain if needed) - cannot be used with subdomain. subdomain: A subdomain for this specific route. strict_slashes: Strictly match the trailing slash present in the path. Will redirect a leaf (no slash) to a branch (with slash). """ return self.add_url_rule( rule, endpoint, view_func, methods={"GET"}, websocket=True, **options, ) @setupmethod def before_websocket( self, func: T_before_websocket, ) -> T_before_websocket: """Add a before websocket function. This is designed to be used as a decorator, if used to decorate a synchronous function, the function will be wrapped in :func:`~quart.utils.run_sync` and run in a thread executor (with the wrapped function returned). An example usage, .. code-block:: python @app.before_websocket async def func(): ... Arguments: func: The before websocket function itself. """ self.before_websocket_funcs[None].append(func) return func @setupmethod def after_websocket( self, func: T_after_websocket, ) -> T_after_websocket: """Add an after websocket function. This is designed to be used as a decorator, if used to decorate a synchronous function, the function will be wrapped in :func:`~quart.utils.run_sync` and run in a thread executor (with the wrapped function returned). An example usage, .. code-block:: python @app.after_websocket async def func(response): return response Arguments: func: The after websocket function itself. """ self.after_websocket_funcs[None].append(func) return func @setupmethod def teardown_websocket( self, func: T_teardown, ) -> T_teardown: """Add a teardown websocket function. This is designed to be used as a decorator, if used to decorate a synchronous function, the function will be wrapped in :func:`~quart.utils.run_sync` and run in a thread executor (with the wrapped function returned). An example usage, .. code-block:: python @app.teardown_websocket async def func(): ... Arguments: func: The teardown websocket function itself. name: Optional blueprint key name. """ self.teardown_websocket_funcs[None].append(func) return func @setupmethod def before_app_websocket(self, func: T_before_websocket) -> T_before_websocket: """Add a before websocket to the App. This is designed to be used as a decorator, and has the same arguments as :meth:`~quart.Quart.before_websocket`. It applies to all requests to the app this blueprint is registered on. An example usage, .. code-block:: python blueprint = Blueprint(__name__) @blueprint.before_app_websocket def before(): ... """ self.record_once(lambda state: state.app.before_websocket(func)) # type: ignore return func @setupmethod def before_app_serving(self, func: T_before_serving) -> T_before_serving: """Add a before serving to the App. This is designed to be used as a decorator, and has the same arguments as :meth:`~quart.Quart.before_serving`. An example usage, .. code-block:: python blueprint = Blueprint(__name__) @blueprint.before_app_serving def before(): ... """ self.record_once(lambda state: state.app.before_serving(func)) # type: ignore return func @setupmethod def after_app_websocket(self, func: T_after_websocket) -> T_after_websocket: """Add an after websocket function to the App. This is designed to be used as a decorator, and has the same arguments as :meth:`~quart.Quart.after_websocket`. It applies to all requests to the ppe this blueprint is registered on. An example usage, .. code-block:: python blueprint = Blueprint(__name__) @blueprint.after_app_websocket def after(): ... """ self.record_once(lambda state: state.app.after_websocket(func)) # type: ignore return func @setupmethod def after_app_serving(self, func: T_after_serving) -> T_after_serving: """Add an after serving function to the App. This is designed to be used as a decorator, and has the same arguments as :meth:`~quart.Quart.after_serving`. An example usage, .. code-block:: python blueprint = Blueprint(__name__) @blueprint.after_app_serving def after(): ... """ self.record_once(lambda state: state.app.after_serving(func)) # type: ignore[attr-defined] return func @setupmethod def while_app_serving(self, func: T_while_serving) -> T_while_serving: """Add a while serving function to the App. This is designed to be used as a decorator, and has the same arguments as :meth:`~quart.Quart.while_serving`. An example usage, .. code-block:: python @blueprint.while_serving async def func(): ... # Startup yield ... # Shutdown """ self.record_once(lambda state: state.app.while_serving(func)) # type: ignore[attr-defined] return func @setupmethod def teardown_app_websocket(self, func: T_teardown) -> T_teardown: """Add a teardown websocket function to the app. This is designed to be used as a decorator, and has the same arguments as :meth:`~quart.Quart.teardown_websocket`. It applies to all requests to the app this blueprint is registered on. An example usage, .. code-block:: python blueprint = Blueprint(__name__) @blueprint.teardown_app_websocket def teardown(): ... """ self.record_once(lambda state: state.app.teardown_websocket(func)) # type: ignore return func def _merge_blueprint_funcs(self, app: App, name: str) -> None: super()._merge_blueprint_funcs(app, name) def extend(bp_dict: dict, parent_dict: dict) -> 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 extend(self.before_websocket_funcs, app.before_websocket_funcs) # type: ignore extend(self.after_websocket_funcs, app.after_websocket_funcs) # type: ignore extend(self.teardown_websocket_funcs, app.teardown_websocket_funcs) # type: ignore pallets-quart-6b318bd/src/quart/cli.py000066400000000000000000000574351524460054700200140ustar00rootroot00000000000000from __future__ import annotations import ast import asyncio import code import functools import inspect import os import platform import re import sys import traceback from collections.abc import Callable from importlib import import_module from importlib.metadata import entry_points from operator import attrgetter from types import ModuleType from typing import Any from typing import TYPE_CHECKING import click from click.core import ParameterSource from .globals import current_app from .helpers import get_debug_flag from .helpers import get_load_dotenv try: from importlib.metadata import version except ModuleNotFoundError: from importlib_metadata import version # type: ignore if TYPE_CHECKING: from .app import Quart # noqa: F401 class NoAppException(click.UsageError): pass def _called_with_wrong_args(f: Callable) -> 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_best_app(module: ModuleType) -> Quart: from .app import Quart for attr_name in ("app", "application"): app = getattr(module, attr_name, None) if isinstance(app, Quart): return app matches = [value for value in module.__dict__.values() if isinstance(value, Quart)] if len(matches) == 1: return matches[0] elif len(matches) > 1: raise NoAppException( "Detected multiple Quart applications in module" f" '{module.__name__}'. Use '{module.__name__}:name'" " to specify the correct one." ) 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, Quart): return app except TypeError as error: 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 error raise NoAppException( "Failed to find Quart application or factory in module" f" '{module.__name__}'. Use '{module.__name__}:name'" " to specify one." ) def find_app_by_string(module: ModuleType, app_name: str) -> Quart: from .app import Quart 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} 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, Quart): return app raise NoAppException( "A valid Quart application was not obtained from" f" '{module.__name__}:{app_name}'." ) def locate_app(module_name: str, app_name: str) -> Quart | None: try: module = import_module(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: raise NoAppException( f"While importing {module_name!r}, an ImportError was" f" raised:\n\n{traceback.format_exc()}" ) from None else: raise NoAppException(f"Could not import {module_name!r}.") from None else: if app_name is None: return find_best_app(module) else: return find_app_by_string(module, 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]) class ScriptInfo: def __init__( self, app_import_path: str | None = None, create_app: Callable[..., Quart] | None = None, set_debug_flag: bool = True, ) -> None: self.app_import_path = app_import_path self.create_app = create_app self.data: dict[Any, Any] = {} self.set_debug_flag = set_debug_flag self._loaded_app: Quart | None = None def load_app(self) -> Quart: if self._loaded_app is not None: return self._loaded_app 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: import_name = prepare_import("app.py") app = locate_app(import_name, None) if not app: raise NoAppException( "Could not locate a Quart application. Use the" " 'quart --app' option, 'QUART_APP' environment" " variable, or an '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) def with_appcontext(fn: Callable | None = None) -> Callable: # decorator was used with parenthesis if fn is None: return with_appcontext @click.pass_context def decorator(__ctx: click.Context, *args: Any, **kwargs: Any) -> Any: async def _inner() -> Any: async with __ctx.ensure_object(ScriptInfo).load_app().app_context(): try: return __ctx.invoke(fn, *args, **kwargs) except RuntimeError as error: if ( error.args[0] == "Cannot run the event loop while another loop is running" ): click.echo( "The appcontext cannot be used with a command that" " runs an event loop. See quart#361 for more details" ) raise return asyncio.run(_inner()) return functools.update_wrapper(decorator, fn) 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:`QuartGroup`. """ def command(self, *args: Any, **kwargs: Any) -> Callable: # type: ignore """This works exactly like the method of the same name on a regular :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` if it's enabled by passing ``with_appcontext=True``. """ wrap_for_ctx = kwargs.pop("with_appcontext", False) def decorator(f: Callable) -> Callable: if wrap_for_ctx: f = with_appcontext(f) return click.Group.command(self, *args, **kwargs)(f) return decorator def group(self, *args: Any, **kwargs: Any) -> Callable: # type: ignore kwargs.setdefault("cls", AppGroup) return super().group(*args, **kwargs) def get_version(ctx: Any, param: Any, value: Any) -> None: if not value or ctx.resilient_parsing: return quart_version = version("quart") werkzeug_version = version("werkzeug") click.echo( f"Python {platform.python_version()}\n" f"Quart {quart_version}\n" f"Werkzeug {werkzeug_version}", color=ctx.color, ) ctx.exit() version_option = click.Option( ["--version"], help="Show the Quart version", expose_value=False, callback=get_version, is_flag=True, is_eager=True, ) 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 QUART_APP gets picked up. _app_option = click.Option( ["-A", "--app"], metavar="IMPORT", help=( "The QUART 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) 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["QUART_DEBUG"] = "1" if value else "0" return value _debug_option = click.Option( ["--debug/--no-debug"], help="Set 'app.debug' separately from '--env'.", expose_value=False, callback=_set_debug, ) def _env_file_callback( ctx: click.Context, param: click.Option, value: str | None ) -> str | None: if value is None: return None import importlib try: importlib.import_module("dotenv") except ImportError: raise click.BadParameter( "python-dotenv must be installed to load an env file.", ctx=ctx, param=param, ) from None # Don't check QUART_SKIP_DOTENV, that only disables automatically # loading .env and .quartenv files. load_dotenv(value) 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. python-dotenv must be installed.", is_eager=True, expose_value=False, callback=_env_file_callback, ) class QuartGroup(AppGroup): def __init__( self, add_default_commands: bool = True, create_app: Callable[..., Quart] | None = None, add_version_option: bool = True, load_dotenv: bool = True, set_debug_flag: bool = True, **extra: Any, ) -> None: params = 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", "QUART") 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 for point in entry_points(group="quart.commands"): self.add_command(point.load(), point.name) self._loaded_plugin_commands = True def get_command(self, ctx: click.Context, name: str) -> click.Command: self._load_plugin_commands() 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 return app.cli.get_command(ctx, name) def list_commands(self, ctx: click.Context) -> list[str]: self._load_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: Any, ) -> click.Context: # Attempt to load .env and .quartenv files. The --env-file # option can cause another file to be loaded. if get_load_dotenv(self.load_dotenv): load_dotenv() 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 ) 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: # 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 load_dotenv(path: str | os.PathLike | None = None) -> bool: """Load "dotenv" files in order of precedence to set environment variables. If an env var is already set it is not overwritten, so earlier files in the list are preferred over later files. 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 instead of searching. :return: ``True`` if a file was loaded. """ try: import dotenv except ImportError: if path or os.path.isfile(".env") or os.path.isfile(".quartenv"): click.secho( " * Tip: There are .env or .quartenv files present." ' Do "pip install python-dotenv" to use them.', fg="yellow", err=True, ) return False # Always return after attempting to load a given path, don't load # the default files. if path is not None: if os.path.isfile(path): return dotenv.load_dotenv(path, encoding="utf-8") return False loaded = False for name in (".env", ".quartenv"): path = dotenv.find_dotenv(name, usecwd=True) if not path: continue dotenv.load_dotenv(path, encoding="utf-8") loaded = True return loaded # True if at least one file was located and loaded. @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( "--certfile", "--cert", type=click.Path(exists=True, file_okay=True, dir_okay=False), help="Specify a certificate file to use HTTPS.", ) @click.option( "--keyfile", "--key", type=click.Path(exists=True, file_okay=True, dir_okay=False), help="The key file to use when specifying a certificate.", ) @click.option( "--reload/--no-reload", default=None, help="Enable or disable the reloader", ) @pass_script_info def run_command( info: ScriptInfo, host: str, port: int, reload: bool, keyfile: str, certfile: str, ) -> None: """Run a local development server.""" app = info.load_app() debug = get_debug_flag() if reload is None: reload = debug app.run( debug=debug, host=host, port=port, certfile=certfile, keyfile=keyfile, use_reloader=reload, ) 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 Quart 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. """ banner = ( f"Python {sys.version} on {sys.platform}\n" f"App: {current_app.import_name}\n" f"Instance: {current_app.instance_path}" ) ctx: dict = {} # 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 # quart.__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 Quart 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) if sort in ("endpoint", "rule", "domain"): rules = sorted(rules, key=attrgetter(sort)) elif sort == "methods": rules = sorted(rules, key=lambda rule: sorted(rule.methods)) headers = ["Endpoint", "Methods"] if has_domain: headers.append("Host" if host_matching else "Subdomain") headers.append("Rule") rows = [] for rule in rules: row = [rule.endpoint, ", ".join(sorted(rule.methods - ignored_methods))] if has_domain: row.append((rule.host if host_matching else rule.subdomain) or "") row.append(rule.rule) rows.append(row) 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 = QuartGroup( name="quart", help="""\ A general utility script for Quart applications. An application to load must be given with the '--app' option, 'QUART_APP' environment variable, or with an 'app.py' file in the current directory. """, ) def main() -> None: cli.main() if __name__ == "__main__": main() pallets-quart-6b318bd/src/quart/config.py000066400000000000000000000027751524460054700205070ustar00rootroot00000000000000from __future__ import annotations import json from collections.abc import Callable from typing import Any from flask.config import Config as FlaskConfig # noqa: F401 from flask.config import ConfigAttribute as ConfigAttribute # noqa: F401 class Config(FlaskConfig): def from_prefixed_env( self, prefix: str = "QUART", *, loads: Callable[[str], Any] = json.loads ) -> bool: """Load any environment variables that start with the prefix. The prefix (default ``QUART_``) is dropped 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. Arguments: prefix: Load env vars that start with this prefix, separated with an underscore (``_``). 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`. """ return super().from_prefixed_env(prefix, loads=loads) pallets-quart-6b318bd/src/quart/ctx.py000066400000000000000000000346071524460054700200370ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable from contextvars import Token from functools import update_wrapper from types import TracebackType from typing import Any from typing import Self from typing import TYPE_CHECKING from flask.ctx import _AppCtxGlobals as _AppCtxGlobals # noqa: F401 from werkzeug.exceptions import HTTPException from werkzeug.routing import MapAdapter from .globals import _cv_app from .helpers import _CollectErrors from .sessions import SessionMixin # noqa from .signals import appcontext_popped from .signals import appcontext_pushed from .typing import AfterRequestCallable from .typing import AfterWebsocketCallable from .wrappers import Request from .wrappers import Websocket if TYPE_CHECKING: from .app import Quart # noqa class AppContext: def __init__( self, app: Quart, *, request: Request | None = None, session: SessionMixin | None = None, websocket: Websocket | None = None, ) -> None: self.app = app self.g: _AppCtxGlobals = app.app_ctx_globals_class() self.url_adapter: MapAdapter | None = None self._request: Request | None = request self._session: SessionMixin | None = session self._websocket: Websocket | None = websocket self._flashes: list[tuple[str, str]] | None = None self._after_request_functions: list[AfterRequestCallable] = [] self._after_websocket_functions: list[AfterWebsocketCallable] = [] try: self.url_adapter = app.create_url_adapter(self._request_websocket) except HTTPException as error: self._request_websocket.routing_exception = error self._cv_token: Token[AppContext] | None = None self._push_count: int = 0 @property def has_request(self) -> bool: """True if this context was created with request data.""" return self._request is not None @property def has_websocket(self) -> bool: """True if this context was created with request data.""" return self._websocket is not None def copy(self) -> Self: """Create a new context with the same data objects as this context. See :func:`.copy_current_request_context`. """ return self.__class__( self.app, request=self._request, session=self._session, websocket=self._websocket, ) @property def request(self) -> Request: """The request object associated with this context. Accessed through :data:`.request`. Only available in request contexts, otherwise raises :exc:`RuntimeError`. """ if self._request is None: raise RuntimeError("There is no request in this context.") return self._request @property def websocket(self) -> Websocket: """The websocket object associated with this context. Accessed through :data:`.websocket`. Only available in websocket contexts, otherwise raises :exc:`RuntimeError`. """ if self._websocket is None: raise RuntimeError("There is no websocket in this context.") return self._websocket @property def _request_websocket(self) -> Request | Websocket: if self._request is not None: return self._request elif self._websocket is not None: return self._websocket else: return None async def _open_session(self) -> None: """Open the session if it is not already open for this request context.""" if self._request_websocket is None: return if self._session is None: interface = self.app.session_interface self._session = await self.app.ensure_async(interface.open_session)( self.app, self._request_websocket ) if self._session is None: self._session = await interface.make_null_session(self.app) @property def session(self) -> SessionMixin: """The session object associated with this context. Accessed through :data:`.session`. Only available in request contexts, otherwise raises :exc:`RuntimeError`. Accessing this sets :attr:`.SessionMixin.accessed`. """ self._session.accessed = True return self._session def match_request(self) -> None: """Apply routing to the current request, storing either the matched endpoint and args, or a routing exception. """ if self._request_websocket is None: raise RuntimeError("There is no request nor websocket in this context.") try: result = self.url_adapter.match(return_rule=True) except HTTPException as error: self._request_websocket.routing_exception = error else: self._request_websocket.url_rule, self._request_websocket.view_args = result # type: ignore[assignment] async def push(self) -> None: """Push this context so that it is the active context. If this is a request context, calls :meth:`match_request` to perform routing with the context active. Typically, this is not used directly. Instead, use a ``with`` block to manage the context. In some situations, such as streaming or testing, the context may be pushed multiple times. It will only trigger matching and signals if it is not currently pushed. """ self._push_count += 1 if self._cv_token is not None: return self._cv_token = _cv_app.set(self) await appcontext_pushed.send_async( self.app, _async_wrapper=self.app.ensure_async ) if self._request_websocket is not None: await self._open_session() if self.url_adapter is not None: self.match_request() async def pop(self, exc: BaseException | None = None) -> None: """Pop this context so that it is no longer the active context. Then call teardown functions and signals. Typically, this is not used directly. Instead, use a ``with`` block to manage the context. This context must currently be the active context, otherwise a :exc:`RuntimeError` is raised. In some situations, such as streaming or testing, the context may have been pushed multiple times. It will only trigger cleanup once it has been popped as many times as it was pushed. Until then, it will remain the active context. :param exc: An unhandled exception that was raised while the context was active. Passed to teardown functions. .. versionchanged:: 0.9 Added the ``exc`` argument. """ if self._cv_token is None: raise RuntimeError(f"Cannot pop this context ({self!r}), it is not pushed.") ctx = _cv_app.get(None) if ctx is None or self._cv_token is None: raise RuntimeError( f"Cannot pop this context ({self!r}), there is no active context." ) if ctx is not self: raise RuntimeError( f"Cannot pop this context ({self!r}), it is not the active" f" context ({ctx!r})." ) self._push_count -= 1 if self._push_count > 0: return collect_errors = _CollectErrors() if self._request is not None: with collect_errors: await self.app.do_teardown_request(self, exc) with collect_errors: await self._request.close() if self._websocket is not None: with collect_errors: await self.app.do_teardown_websocket(self, exc) with collect_errors: await self.app.do_teardown_appcontext(self, exc) _cv_app.reset(self._cv_token) self._cv_token = None with collect_errors: await appcontext_popped.send_async( self.app, _async_wrapper=self.app.ensure_async ) collect_errors.raise_any("Errors during context teardown") async def __aenter__(self) -> Self: await self.push() return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, tb: TracebackType | None, ) -> None: await self.pop(exc_value) def __repr__(self) -> str: if self._request is not None: return ( f"<{type(self).__name__} {id(self)} of {self.app.name}," f" {self.request.method} {self.request.url!r}>" ) return f"<{type(self).__name__} {id(self)} of {self.app.name}>" def after_this_request(func: AfterRequestCallable) -> AfterRequestCallable: """Schedule the func to be called after the current request. This is useful in situations whereby you want an after request function for a specific route or circumstance only, for example, .. code-block:: python def index(): @after_this_request def set_cookie(response): response.set_cookie('special', 'value') return response ... """ ctx = _cv_app.get(None) if ctx is None: raise RuntimeError("Not within a request context") ctx._after_request_functions.append(func) return func def after_this_websocket(func: AfterWebsocketCallable) -> AfterWebsocketCallable: """Schedule the func to be called after the current websocket. This is useful in situations whereby you want an after websocket function for a specific route or circumstance only, for example, .. note:: The response is an optional argument, and will only be passed if the websocket was not active (i.e. there was an error). .. code-block:: python def index(): @after_this_websocket def set_cookie(response: Optional[Response]): response.set_cookie('special', 'value') return response ... """ ctx = _cv_app.get(None) if ctx is None: raise RuntimeError("Not within a websocket context") ctx._after_websocket_functions.append(func) return func def copy_current_app_context(func: Callable) -> Callable: """Share the current app context with the function decorated. The app context is local per task and hence will not be available in any other task. This decorator can be used to make the context available, .. code-block:: python @copy_current_app_context async def within_context() -> None: name = current_app.name ... """ original = _cv_app.get(None) if original is None: raise RuntimeError( "'copy_current_app_context' can only be used when a" " context is active, such as in a view function." ) async def wrapper(*args: Any, **kwargs: Any) -> Any: # Copy the context before pushing, so each worker acts independently. async with original.copy() as ctx: return await ctx.app.ensure_async(func)(*args, **kwargs) return update_wrapper(wrapper, func) def copy_current_request_context(func: Callable) -> Callable: """Share the current request context with the function decorated. The request context is local per task and hence will not be available in any other task. This decorator can be used to make the context available, .. code-block:: python @copy_current_request_context async def within_context() -> None: method = request.method ... """ original = _cv_app.get(None) if original is None: raise RuntimeError( "'copy_current_request_context' can only be used when a" " request context is active, such as in a view function." ) async def wrapper(*args: Any, **kwargs: Any) -> Any: # Copy the context before pushing, so each worker acts independently. async with original.copy() as ctx: return await ctx.app.ensure_async(func)(*args, **kwargs) return update_wrapper(wrapper, func) def copy_current_websocket_context(func: Callable) -> Callable: """Share the current websocket context with the function decorated. The websocket context is local per task and hence will not be available in any other task. This decorator can be used to make the context available, .. code-block:: python @copy_current_websocket_context async def within_context() -> None: method = websocket.method ... """ original = _cv_app.get(None) if original is None: raise RuntimeError( "'copy_current_websocket_context' can only be used when a" " websocket context is active, such as in a view function." ) async def wrapper(*args: Any, **kwargs: Any) -> Any: # Copy the context before pushing, so each worker acts independently. async with original.copy() as ctx: return await ctx.app.ensure_async(func)(*args, **kwargs) return update_wrapper(wrapper, func) def has_app_context() -> bool: """Check if execution is within an app context. This allows a controlled way to act if there is an app context available, or silently not act if not. For example, .. code-block:: python if has_app_context(): log.info("Executing in %s context", current_app.name) See also :func:`has_request_context` """ return _cv_app.get(None) is not None def has_request_context() -> bool: """Check if execution is within a request context. This allows a controlled way to act if there is a request context available, or silently not act if not. For example, .. code-block:: python if has_request_context(): log.info("Request endpoint %s", request.endpoint) See also :func:`has_app_context`. """ return (ctx := _cv_app.get(None)) is not None and ctx.has_request def has_websocket_context() -> bool: """Check if execution is within a websocket context. This allows a controlled way to act if there is a websocket context available, or silently not act if not. For example, .. code-block:: python if has_websocket_context(): log.info("Websocket endpoint %s", websocket.endpoint) See also :func:`has_app_context`. """ return (ctx := _cv_app.get(None)) is not None and ctx.has_websocket pallets-quart-6b318bd/src/quart/datastructures.py000066400000000000000000000031271524460054700223070ustar00rootroot00000000000000from __future__ import annotations from os import PathLike from pathlib import Path from typing import IO from aiofiles import open as async_open from werkzeug.datastructures import FileStorage as WerkzeugFileStorage from werkzeug.datastructures import Headers class FileStorage(WerkzeugFileStorage): """A thin wrapper over incoming files.""" def __init__( self, stream: IO[bytes] | None = None, filename: str | None = None, name: str | None = None, content_type: str | None = None, content_length: int | None = None, headers: Headers | None = None, ) -> None: super().__init__(stream, filename, name, content_type, content_length, headers) async def save(self, destination: PathLike, buffer_size: int = 16384) -> None: # type: ignore """Save the file to the destination. Arguments: destination: A filename (str) or file object to write to. buffer_size: Buffer size to keep in memory. """ async with async_open(destination, "wb") as file_: data = self.stream.read(buffer_size) while data != b"": await file_.write(data) data = self.stream.read(buffer_size) async def load(self, source: PathLike, buffer_size: int = 16384) -> None: path = Path(source) self.filename = path.name async with async_open(path, "rb") as file_: data = await file_.read(buffer_size) while data != b"": self.stream.write(data) data = await file_.read(buffer_size) pallets-quart-6b318bd/src/quart/debug.py000066400000000000000000000042621524460054700203210ustar00rootroot00000000000000from __future__ import annotations import inspect from jinja2 import Template from .wrappers import Response TEMPLATE = """

      {{ name }} {{ value }}

        {% for frame in frames %}
      • File {{ frame.file }}, line {{ frame.line }}, in
        {% for line in frame.code[0] %}
        {{ loop.index + frame.code[1] }}
        {{ line }}
        {% endfor %}
        {% for name, repr in frame.locals.items() %}
        {{ name }}
        {{ repr }}
        {% endfor %}
      • {% endfor %}
      """ async def traceback_response(error: Exception) -> Response: type_ = type(error) tb = error.__traceback__ frames = [] while tb: frame = tb.tb_frame try: code = inspect.getsourcelines(frame) except OSError: code = None frames.append( { "file": inspect.getfile(frame), "line": frame.f_lineno, "locals": frame.f_locals, "code": code, } ) tb = tb.tb_next name = type_.__name__ template = Template(TEMPLATE) html = template.render(frames=reversed(frames), name=name, value=error) return Response(html, 500) pallets-quart-6b318bd/src/quart/formparser.py000066400000000000000000000200501524460054700214040ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Awaitable from collections.abc import Callable from typing import Any from typing import cast from typing import IO from typing import NoReturn from typing import TYPE_CHECKING from urllib.parse import parse_qsl from werkzeug.datastructures import Headers from werkzeug.datastructures import MultiDict from werkzeug.exceptions import RequestEntityTooLarge from werkzeug.formparser import default_stream_factory from werkzeug.http import parse_options_header from werkzeug.sansio.multipart import Data from werkzeug.sansio.multipart import Epilogue from werkzeug.sansio.multipart import Field from werkzeug.sansio.multipart import File from werkzeug.sansio.multipart import MultipartDecoder from werkzeug.sansio.multipart import NeedData from .datastructures import FileStorage if TYPE_CHECKING: from .wrappers.request import Body StreamFactory = Callable[ [int | None, str | None, str | None, int | None], IO[bytes], ] ParserFunc = Callable[ ["FormDataParser", "Body", str, int | None, dict[str, str]], Awaitable[tuple[MultiDict, MultiDict]], ] class FormDataParser: file_storage_class = FileStorage def __init__( self, *, cls: type[MultiDict] | None = MultiDict, max_content_length: int | None = None, max_form_memory_size: int | None = None, max_form_parts: int | None = None, silent: bool = True, stream_factory: StreamFactory = default_stream_factory, ) -> None: self.cls = cls self.max_content_length = max_content_length self.max_form_memory_size = max_form_memory_size self.max_form_parts = max_form_parts self.silent = silent self.stream_factory = stream_factory def get_parse_func( self, mimetype: str, options: dict[str, str] ) -> ParserFunc | None: return self.parse_functions.get(mimetype) async def parse( self, body: Body, mimetype: str, content_length: int | None, options: dict[str, str] | None = None, ) -> tuple[MultiDict, MultiDict]: if options is None: options = {} parse_func = self.get_parse_func(mimetype, options) if parse_func is not None: try: return await parse_func(self, body, mimetype, content_length, options) except ValueError: if not self.silent: raise return self.cls(), self.cls() async def _parse_multipart( self, body: Body, mimetype: str, content_length: int | None, options: dict[str, str], ) -> tuple[MultiDict, MultiDict]: parser = MultiPartParser( cls=self.cls, file_storage_cls=self.file_storage_class, max_content_length=self.max_content_length, max_form_memory_size=self.max_form_memory_size, max_form_parts=self.max_form_parts, stream_factory=self.stream_factory, ) boundary = options.get("boundary", "").encode("ascii") if not boundary: raise ValueError("Missing boundary") return await parser.parse(body, boundary, content_length) async def _parse_urlencoded( self, body: Body, mimetype: str, content_length: int | None, options: dict[str, str], ) -> tuple[MultiDict, MultiDict]: form = parse_qsl( (await body).decode(), keep_blank_values=True, ) return self.cls(form), self.cls() parse_functions: dict[str, ParserFunc] = { "multipart/form-data": _parse_multipart, "application/x-www-form-urlencoded": _parse_urlencoded, "application/x-url-encoded": _parse_urlencoded, } class MultiPartParser: def __init__( self, *, buffer_size: int = 64 * 1024, cls: type[MultiDict] = MultiDict, file_storage_cls: type[FileStorage] = FileStorage, max_content_length: int | None = None, max_form_memory_size: int | None = None, max_form_parts: int | None = None, stream_factory: StreamFactory = default_stream_factory, ) -> None: self.buffer_size = buffer_size self.cls = cls self.file_storage_cls = file_storage_cls self.max_content_length = max_content_length self.max_form_memory_size = max_form_memory_size self.max_form_parts = max_form_parts self.stream_factory = stream_factory def fail(self, message: str) -> NoReturn: raise ValueError(message) def get_part_charset(self, headers: Headers) -> str: content_type = headers.get("content-type") if content_type: parameters = parse_options_header(content_type)[1] ct_charset = parameters.get("charset", "").lower() # A safe list of encodings. Modern clients should only send ASCII or UTF-8. # This list will not be extended further. if ct_charset in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}: return ct_charset return "utf-8" def start_file_streaming(self, event: File, total_content_length: int) -> IO[bytes]: content_type = event.headers.get("content-type") try: content_length = int(event.headers["content-length"]) except (KeyError, ValueError): content_length = 0 container = self.stream_factory( total_content_length, content_type, event.filename, content_length, ) return container async def parse( self, body: Body, boundary: bytes, content_length: int ) -> tuple[MultiDict, MultiDict]: container: IO[bytes] | list[bytes] _write: Callable[[bytes], Any] parser = MultipartDecoder( boundary, self.max_content_length, max_parts=self.max_form_parts ) fields = [] files = [] current_part: Field | File field_size: int | None = None async for data in body: parser.receive_data(data) event = parser.next_event() while not isinstance(event, (Epilogue, NeedData)): if isinstance(event, Field): current_part = event field_size = 0 container = [] _write = container.append elif isinstance(event, File): current_part = event field_size = None container = self.start_file_streaming(event, content_length) _write = container.write elif isinstance(event, Data): if self.max_form_memory_size is not None and field_size is not None: field_size += len(event.data) if field_size > self.max_form_memory_size: raise RequestEntityTooLarge() _write(event.data) if not event.more_data: if isinstance(current_part, Field): value = b"".join(container).decode( self.get_part_charset(current_part.headers), "replace" ) fields.append((current_part.name, value)) else: container = cast(IO[bytes], container) container.seek(0) files.append( ( current_part.name, self.file_storage_cls( container, current_part.filename, current_part.name, headers=current_part.headers, ), ) ) event = parser.next_event() return self.cls(fields), self.cls(files) pallets-quart-6b318bd/src/quart/globals.py000066400000000000000000000036611524460054700206600ustar00rootroot00000000000000from __future__ import annotations from contextvars import ContextVar from typing import Protocol from typing import TYPE_CHECKING from typing import TypeVar from werkzeug.local import LocalProxy if TYPE_CHECKING: from .app import Quart from .ctx import _AppCtxGlobals from .ctx import AppContext from .sessions import SessionMixin from .wrappers import Request from .wrappers import Websocket T = TypeVar("T", covariant=True) class ProxyMixin(Protocol[T]): def _get_current_object(self) -> T: ... # These subclasses inform type checkers that the proxy objects look like the # proxied type along with the _get_current_object method. class QuartProxy(ProxyMixin[Quart], Quart): ... class AppContextProxy(ProxyMixin[AppContext], AppContext): ... class _AppCtxGlobalsProxy(ProxyMixin[_AppCtxGlobals], _AppCtxGlobals): ... class RequestProxy(ProxyMixin[Request], Request): ... class SessionMixinProxy(ProxyMixin[SessionMixin], SessionMixin): ... class WebsocketProxy(ProxyMixin[Websocket], Websocket): ... _no_app_msg = "Not within an app context" _cv_app: ContextVar[AppContext] = ContextVar("quart.app_ctx") app_ctx: AppContextProxy = LocalProxy( # type: ignore[assignment] _cv_app, unbound_message=_no_app_msg ) current_app: QuartProxy = LocalProxy( # type: ignore[assignment] _cv_app, "app", unbound_message=_no_app_msg ) g: _AppCtxGlobalsProxy = LocalProxy( # type: ignore[assignment] _cv_app, "g", unbound_message=_no_app_msg ) request: RequestProxy = LocalProxy( # type: ignore[assignment] _cv_app, "request", unbound_message="Not within a request context" ) session: SessionMixinProxy = LocalProxy( # type: ignore[assignment] _cv_app, "session", unbound_message="Not within a request nor websocket context" ) websocket: WebsocketProxy = LocalProxy( # type: ignore[assignment] _cv_app, "websocket", unbound_message="Not within a websocket context" ) pallets-quart-6b318bd/src/quart/helpers.py000066400000000000000000000324211524460054700206730ustar00rootroot00000000000000from __future__ import annotations import mimetypes import os import pkgutil import sys from collections.abc import Callable from collections.abc import Iterable from datetime import datetime from datetime import timedelta from datetime import UTC from functools import cache from functools import wraps from io import BytesIO from pathlib import Path from types import TracebackType from typing import Any from typing import cast from typing import NoReturn from zlib import adler32 from flask.helpers import get_root_path as get_root_path # noqa: F401 from werkzeug.exceptions import abort as werkzeug_abort from werkzeug.exceptions import NotFound from werkzeug.utils import redirect as werkzeug_redirect from werkzeug.utils import safe_join from werkzeug.wrappers import Response as WerkzeugResponse from .globals import app_ctx from .globals import current_app from .globals import request from .globals import session from .signals import message_flashed from .typing import FilePath from .typing import ResponseReturnValue from .typing import ResponseTypes from .utils import file_path_to_path from .wrappers import Response from .wrappers.response import ResponseBody DEFAULT_MIMETYPE = "application/octet-stream" locked_cached_property = property def get_debug_flag() -> bool: """Reads QUART_DEBUG environment variable to determine whether to run the app in debug mode. If unset, and development mode has been configured, it will be enabled automatically. """ value = os.getenv("QUART_DEBUG", None) return bool(value and value.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:`QUART_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("QUART_SKIP_DOTENV") if not val: return default return val.lower() in ("0", "false", "no") async def make_response(*args: Any) -> ResponseTypes: """Create a response, a simple wrapper function. This is most useful when you want to alter a Response before returning it, for example .. code-block:: python response = make_response(render_template('index.html')) response.headers['X-Header'] = 'Something' """ if not args: return current_app.response_class("") if len(args) == 1: args = args[0] return await current_app.make_response(cast(ResponseReturnValue, args)) async def make_push_promise(path: str) -> None: """Create a push promise, a simple wrapper function. This takes a path that should be pushed to the client if the protocol is HTTP/2. """ return await request.send_push_promise(path) async def flash(message: str, category: str = "message") -> None: """Add a message (with optional category) to the session store. This is typically used to flash a message to a user that will be stored in the session and shown during some other request. For example, .. code-block:: python @app.route('/login', methods=['POST']) async def login(): ... await flash('Login successful') return redirect(url_for('index')) allows the index route to show the flashed messages, without having to accept the message as an argument or otherwise. See :func:`~quart.helpers.get_flashed_messages` for message retrieval. """ flashes = session.get("_flashes", []) flashes.append((category, message)) session["_flashes"] = flashes app = current_app._get_current_object() await message_flashed.send_async( app, _sync_wrapper=app.ensure_async, # type: ignore[arg-type] message=message, category=category, ) def get_flashed_messages( with_categories: bool = False, category_filter: Iterable[str] = () ) -> list[str] | list[tuple[str, str]]: """Retrieve the flashed messages stored in the session. This is mostly useful in templates where it is exposed as a global function, for example .. code-block:: html+jinja
        {% for message in get_flashed_messages() %}
      • {{ message }}
      • {% endfor %}
      Note that caution is required for usage of ``category_filter`` as all messages will be popped, but only those matching the filter returned. See :func:`~quart.helpers.flash` for message creation. """ flashes = app_ctx._flashes if flashes is None: flashes = session.pop("_flashes", []) app_ctx._flashes = flashes if category_filter: flashes = [flash for flash in flashes if flash[0] in category_filter] if not with_categories: flashes = [flash[1] for flash in flashes] return flashes def get_template_attribute(template_name: str, attribute: str) -> Any: """Load a attribute from a template. This is useful in Python code in order to use attributes in templates. Arguments: template_name: To load the attribute from. attribute: The attribute name to load """ return getattr(current_app.jinja_env.get_template(template_name).module, attribute) def url_for( endpoint: str, *, _anchor: str | None = None, _external: bool | None = None, _method: str | None = None, _scheme: str | None = None, **values: Any, ) -> str: """Return the url for a specific endpoint. This is most useful in templates and redirects to create a URL that can be used in the browser. Arguments: endpoint: The endpoint to build a url for, if prefixed with ``.`` it targets endpoint's in the current blueprint. _anchor: Additional anchor text to append (i.e. #text). _external: Return an absolute url for external (to app) usage. _method: The method to consider alongside the endpoint. _scheme: A specific scheme to use. values: The values to build into the URL, as specified in the endpoint rule. """ return current_app.url_for( endpoint, _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external, **values, ) def stream_with_context(func: Callable) -> Callable: """Share the current request context with a generator. This allows the request context to be accessed within a streaming generator, for example, .. code-block:: python @app.route('/') def index() -> AsyncGenerator[bytes, None]: @stream_with_context async def generator() -> bytes: yield request.method.encode() yield b' ' yield request.path.encode() return generator() """ ctx = app_ctx.copy() @wraps(func) async def generator(*args: Any, **kwargs: Any) -> Any: async with ctx: async for data in func(*args, **kwargs): yield data return generator def find_package(name: str) -> tuple[Path | None, Path]: """Finds packages install prefix (or None) and it's containing Folder""" module = name.split(".")[0] loader = pkgutil.get_loader(module) if name == "__main__" or loader is None: package_path = Path.cwd() else: if hasattr(loader, "get_filename"): filename = loader.get_filename(module) else: __import__(name) filename = sys.modules[name].__file__ package_path = Path(filename).resolve().parent if hasattr(loader, "is_package"): is_package = loader.is_package(module) if is_package: package_path = Path(package_path).resolve().parent sys_prefix = Path(sys.prefix).resolve() try: package_path.relative_to(sys_prefix) except ValueError: return None, package_path else: return sys_prefix, package_path async def send_from_directory( directory: FilePath, file_name: str, *, mimetype: str | None = None, as_attachment: bool = False, attachment_filename: str | None = None, add_etags: bool = True, cache_timeout: int | None = None, conditional: bool = True, last_modified: datetime | None = None, ) -> Response: """Send a file from a given directory. Arguments: directory: Directory that when combined with file_name gives the file path. file_name: File name that when combined with directory gives the file path. See :func:`send_file` for the other arguments. """ raw_file_path = safe_join(str(directory), file_name) if raw_file_path is None: raise NotFound() file_path = Path(raw_file_path) if not file_path.is_file(): raise NotFound() return await send_file( file_path, mimetype=mimetype, as_attachment=as_attachment, attachment_filename=attachment_filename, add_etags=add_etags, cache_timeout=cache_timeout, conditional=conditional, last_modified=last_modified, ) async def send_file( filename_or_io: FilePath | BytesIO, mimetype: str | None = None, as_attachment: bool = False, attachment_filename: str | None = None, add_etags: bool = True, cache_timeout: int | None = None, conditional: bool = False, last_modified: datetime | None = None, ) -> Response: """Return a Response to send the filename given. Arguments: filename_or_io: The filename (path) to send, remember to use :func:`safe_join`. mimetype: Mimetype to use, by default it will be guessed or revert to the DEFAULT_MIMETYPE. as_attachment: If true use the attachment filename in a Content-Disposition attachment header. attachment_filename: Name for the filename, if it differs add_etags: Set etags based on the filename, size and modification time. last_modified: Used to override the last modified value. cache_timeout: Time in seconds for the response to be cached. """ file_body: ResponseBody file_size: int | None = None etag: str | None = None if isinstance(filename_or_io, BytesIO): file_body = current_app.response_class.io_body_class(filename_or_io) file_size = filename_or_io.getbuffer().nbytes else: file_path = file_path_to_path(filename_or_io) file_size = file_path.stat().st_size if attachment_filename is None: attachment_filename = file_path.name file_body = current_app.response_class.file_body_class(file_path) if last_modified is None: last_modified = datetime.fromtimestamp(file_path.stat().st_mtime, tz=UTC) if cache_timeout is None: cache_timeout = current_app.get_send_file_max_age(str(file_path)) etag = ( f"{file_path.stat().st_mtime}-{file_path.stat().st_size}" f"-{adler32(bytes(file_path))}" ) if mimetype is None and attachment_filename is not None: mimetype = mimetypes.guess_type(attachment_filename)[0] or DEFAULT_MIMETYPE if mimetype is None: raise ValueError( "The mime type cannot be inferred, please set it manually via the" " mimetype argument." ) response = current_app.response_class(file_body, mimetype=mimetype) response.content_length = file_size if as_attachment: response.headers.add( "Content-Disposition", "attachment", filename=attachment_filename ) if last_modified is not None: response.last_modified = last_modified response.cache_control.public = True if cache_timeout is not None: response.cache_control.max_age = cache_timeout response.expires = datetime.now(UTC) + timedelta(seconds=cache_timeout) if add_etags and etag is not None: response.set_etag(etag) if conditional: await response.make_conditional( request, accept_ranges=True, complete_length=file_size ) return response @cache def _split_blueprint_path(name: str) -> list[str]: bps = [name] while "." in bps[-1]: bps.append(bps[-1].rpartition(".")[0]) return bps def abort(code: int | Response, *args: Any, **kwargs: Any) -> NoReturn: """Raise an HTTPException for the given status code.""" if current_app: current_app.aborter(code, *args, **kwargs) werkzeug_abort(code, *args, **kwargs) def redirect(location: str, code: int = 302) -> WerkzeugResponse: """Redirect to the location with the status code.""" if current_app: return current_app.redirect(location, code=code) return werkzeug_redirect(location, code=code) class _CollectErrors: """A context manager that records and silences an error raised within it. Used to run all teardown functions, then raise any errors afterward. """ def __init__(self) -> None: self.errors: list[BaseException] = [] def __enter__(self) -> None: pass def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool: if exc_val is not None: self.errors.append(exc_val) return True def raise_any(self, message: str) -> None: if self.errors: raise BaseExceptionGroup(message, self.errors) pallets-quart-6b318bd/src/quart/json/000077500000000000000000000000001524460054700176265ustar00rootroot00000000000000pallets-quart-6b318bd/src/quart/json/__init__.py000066400000000000000000000022751524460054700217450ustar00rootroot00000000000000from __future__ import annotations import json from typing import Any from typing import IO from typing import TYPE_CHECKING from flask.json.provider import _default from ..globals import current_app if TYPE_CHECKING: from ..wrappers import Response # noqa: F401 def dumps(object_: Any, **kwargs: Any) -> str: if current_app: return current_app.json.dumps(object_, **kwargs) else: kwargs.setdefault("default", _default) return json.dumps(object_, **kwargs) def dump(object_: Any, fp: IO[str], **kwargs: Any) -> None: if current_app: current_app.json.dump(object_, fp, **kwargs) else: kwargs.setdefault("default", _default) json.dump(object_, fp, **kwargs) def loads(object_: str | bytes, **kwargs: Any) -> Any: if current_app: return current_app.json.loads(object_, **kwargs) else: return json.loads(object_, **kwargs) def load(fp: IO[str], **kwargs: Any) -> Any: if current_app: return current_app.json.load(fp, **kwargs) else: return json.load(fp, **kwargs) def jsonify(*args: Any, **kwargs: Any) -> Response: return current_app.json.response(*args, **kwargs) # type: ignore pallets-quart-6b318bd/src/quart/json/provider.py000066400000000000000000000002441524460054700220320ustar00rootroot00000000000000from flask.json.provider import DefaultJSONProvider as DefaultJSONProvider # noqa: F401 from flask.json.provider import JSONProvider as JSONProvider # noqa: F401 pallets-quart-6b318bd/src/quart/json/tag.py000066400000000000000000000012061524460054700207520ustar00rootroot00000000000000from flask.json.tag import JSONTag as JSONTag # noqa: F401 from flask.json.tag import PassDict as PassDict # noqa: F401 from flask.json.tag import PassList as PassList # noqa: F401 from flask.json.tag import TagBytes as TagBytes # noqa: F401 from flask.json.tag import TagDateTime as TagDateTime # noqa: F401 from flask.json.tag import TagDict as TagDict # noqa: F401 from flask.json.tag import TaggedJSONSerializer as TaggedJSONSerializer # noqa: F401 from flask.json.tag import TagMarkup as TagMarkup # noqa: F401 from flask.json.tag import TagTuple as TagTuple # noqa: F401 from flask.json.tag import TagUUID as TagUUID # noqa: F401 pallets-quart-6b318bd/src/quart/logging.py000066400000000000000000000042251524460054700206600ustar00rootroot00000000000000from __future__ import annotations import sys from logging import DEBUG from logging import Formatter from logging import getLogger from logging import Handler from logging import Logger from logging import LogRecord from logging import NOTSET from logging import StreamHandler from logging.handlers import QueueHandler from logging.handlers import QueueListener from queue import SimpleQueue as Queue from typing import TYPE_CHECKING if TYPE_CHECKING: from .app import Quart # noqa default_handler = StreamHandler(sys.stderr) default_handler.setFormatter( Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s") ) class LocalQueueHandler(QueueHandler): """Custom QueueHandler that skips record preparation. There is no need to prepare records that go into a local, in-process queue, we can skip that process and minimise the cost of logging further. """ def prepare(self, record: LogRecord) -> LogRecord: return record def _setup_logging_queue(*handlers: Handler) -> QueueHandler: """Create a new LocalQueueHandler and start an associated QueueListener.""" queue: Queue = Queue() queue_handler = LocalQueueHandler(queue) serving_listener = QueueListener(queue, *handlers, respect_handler_level=True) serving_listener.start() return queue_handler def has_level_handler(logger: Logger) -> bool: """Check if the logger already has a handler""" 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 return False def create_logger(app: Quart) -> Logger: """Create a logger for the app based on the app settings. This creates a logger named quart.app that has a log level based on the app configuration. """ logger = getLogger(app.name) if app.debug and logger.level == NOTSET: logger.setLevel(DEBUG) if not has_level_handler(logger): queue_handler = _setup_logging_queue(default_handler) logger.addHandler(queue_handler) return logger pallets-quart-6b318bd/src/quart/py.typed000066400000000000000000000000071524460054700203510ustar00rootroot00000000000000Marker pallets-quart-6b318bd/src/quart/routing.py000066400000000000000000000053001524460054700207140ustar00rootroot00000000000000from __future__ import annotations import warnings from collections.abc import Iterable from werkzeug.routing import Map from werkzeug.routing import MapAdapter from werkzeug.routing import Rule from .wrappers.base import BaseRequestWebsocket class QuartRule(Rule): def __init__( self, string: str, defaults: dict | None = None, subdomain: str | None = None, methods: Iterable[str] | None = None, endpoint: str | None = None, strict_slashes: bool | None = None, merge_slashes: bool | None = None, host: str | None = None, websocket: bool = False, provide_automatic_options: bool = False, ) -> None: super().__init__( string, defaults=defaults, subdomain=subdomain, methods=methods, endpoint=endpoint, strict_slashes=strict_slashes, merge_slashes=merge_slashes, host=host, websocket=websocket, ) self.provide_automatic_options = provide_automatic_options class QuartMap(Map): def bind_to_request( self, request: BaseRequestWebsocket, subdomain: str | None, server_name: str | None, ) -> MapAdapter: host: str if server_name is None: host = request.host.lower() else: host = server_name.lower() host = _normalise_host(request.scheme, host) if subdomain is None and not self.host_matching: request_host_parts = _normalise_host( request.scheme, request.host.lower() ).split(".") config_host_parts = host.split(".") offset = -len(config_host_parts) if request_host_parts[offset:] != config_host_parts: warnings.warn( f"Current server name '{request.host}' doesn't match configured" f" server name '{host}'", stacklevel=2, ) subdomain = "" else: subdomain = ".".join(filter(None, request_host_parts[:offset])) return super().bind( host, request.root_path, subdomain, request.scheme, request.method, request.path, request.query_string.decode(), ) def _normalise_host(scheme: str, host: str) -> str: # It is not common to write port 80 or 443 for a hostname, # so strip it if present. if scheme in {"http", "ws"} and host.endswith(":80"): return host[:-3] elif scheme in {"https", "wss"} and host.endswith(":443"): return host[:-4] else: return host pallets-quart-6b318bd/src/quart/sessions.py000066400000000000000000000210421524460054700210740ustar00rootroot00000000000000from __future__ import annotations import hashlib from datetime import datetime from datetime import UTC from typing import TYPE_CHECKING from flask.sessions import NullSession as NullSession # noqa: F401 from flask.sessions import SecureCookieSession as SecureCookieSession # noqa: F401 from flask.sessions import ( # noqa: F401 session_json_serializer as session_json_serializer, ) from flask.sessions import SessionMixin as SessionMixin # noqa: F401 from itsdangerous import BadSignature from itsdangerous import URLSafeTimedSerializer from werkzeug.wrappers import Response as WerkzeugResponse from .wrappers import BaseRequestWebsocket from .wrappers import Response if TYPE_CHECKING: from .app import Quart # noqa class SessionInterface: """Base class for session interfaces. Attributes: null_session_class: Storage class for null (no storage) sessions. pickle_based: Indicates if pickling is used for the session. """ null_session_class = NullSession pickle_based = False async def make_null_session(self, app: Quart) -> NullSession: """Create a Null session object. This is used in replacement of an actual session if sessions are not configured or active. """ return self.null_session_class() def is_null_session(self, instance: object) -> bool: """Returns True is the instance is a null session.""" return isinstance(instance, self.null_session_class) def get_cookie_name(self, app: Quart) -> str: """Helper method to return the Cookie Name for the App.""" return app.config["SESSION_COOKIE_NAME"] def get_cookie_domain(self, app: Quart) -> str | None: """Helper method to return the Cookie Domain for the App.""" rv = app.config["SESSION_COOKIE_DOMAIN"] return rv if rv else None def get_cookie_partitioned(self, app: Quart) -> bool: """Helper method to return the Cookie partitioned setting for the App.""" return app.config["SESSION_COOKIE_PARTITIONED"] def get_cookie_path(self, app: Quart) -> str: """Helper method to return the Cookie path for the App.""" return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] def get_cookie_httponly(self, app: Quart) -> bool: """Helper method to return if the Cookie should be HTTPOnly for the App.""" return app.config["SESSION_COOKIE_HTTPONLY"] def get_cookie_secure(self, app: Quart) -> bool: """Helper method to return if the Cookie should be Secure for the App.""" return app.config["SESSION_COOKIE_SECURE"] def get_cookie_samesite(self, app: Quart) -> str: """Helper method to return the Cookie Samesite configuration for the App.""" return app.config["SESSION_COOKIE_SAMESITE"] def get_expiration_time(self, app: Quart, session: SessionMixin) -> datetime | None: """Helper method to return the Session expiration time. If the session is not 'permanent' it will expire as and when the browser stops accessing the app. """ if session.permanent: return datetime.now(UTC) + app.permanent_session_lifetime else: return None def should_set_cookie(self, app: Quart, session: SessionMixin) -> bool: """Helper method to return if the Set Cookie header should be present. This triggers if the session is marked as modified or the app is configured to always refresh the cookie. """ if session.modified: return True save_each = app.config["SESSION_REFRESH_EACH_REQUEST"] return save_each and session.permanent async def open_session( self, app: Quart, request: BaseRequestWebsocket ) -> SessionMixin | None: """Open an existing session from the request or create one. Returns: The Session object or None if no session can be created, in which case the :attr:`null_session_class` is expected to be used. """ raise NotImplementedError() async def save_session( self, app: Quart, session: SessionMixin, response: Response | WerkzeugResponse | None, ) -> None: """Save the session argument to the response. Arguments: response: Can be None if the session is being saved after a websocket connection closes. Returns: The modified response, with the session stored. """ raise NotImplementedError() class SecureCookieSessionInterface(SessionInterface): """A Session interface that uses cookies as storage. This will store the data on the cookie in plain text, but with a signature to prevent modification. """ digest_method = staticmethod(hashlib.sha1) key_derivation = "hmac" salt = "cookie-session" serializer = session_json_serializer session_class = SecureCookieSession def get_signing_serializer(self, app: Quart) -> URLSafeTimedSerializer | None: """Return a serializer for the session that also signs data. This will return None if the app is not configured for secrets. """ 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 options = { "key_derivation": self.key_derivation, "digest_method": self.digest_method, } return URLSafeTimedSerializer( keys, # type: ignore[arg-type] salt=self.salt, serializer=self.serializer, signer_kwargs=options, ) async def open_session( self, app: Quart, request: BaseRequestWebsocket ) -> SecureCookieSession | None: """Open a secure cookie based session. This will return None if a signing serializer is not available, usually if the config SECRET_KEY is not set. """ signer = self.get_signing_serializer(app) if signer is None: return None cookie = request.cookies.get(self.get_cookie_name(app)) if cookie is None: return self.session_class() max_age = int(app.permanent_session_lifetime.total_seconds()) try: data = signer.loads(cookie, max_age=max_age) return self.session_class(data) except BadSignature: return self.session_class() async def save_session( self, app: Quart, session: SessionMixin, response: Response | WerkzeugResponse | None, ) -> None: """Saves the session to the response in a secure cookie.""" if response is None: if session.modified: app.logger.exception( "Secure Cookie Session modified during websocket handling. " "These modifications will be lost as a cookie cannot be set." ) return name = self.get_cookie_name(app) domain = self.get_cookie_domain(app) partitioned = self.get_cookie_partitioned(app) path = self.get_cookie_path(app) secure = self.get_cookie_secure(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, partitioned=partitioned, path=path, secure=secure, 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)) response.set_cookie( name, val, expires=expires, httponly=httponly, domain=domain, partitioned=partitioned, path=path, secure=secure, samesite=samesite, ) response.vary.add("Cookie") pallets-quart-6b318bd/src/quart/signals.py000066400000000000000000000070651524460054700206770ustar00rootroot00000000000000from __future__ import annotations from blinker import Namespace signals_available = True _signals = Namespace() #: Called before a template is rendered, connection functions # should have a signature of Callable[[Quart, Template, dict], None] before_render_template = _signals.signal("before-render-template") #: Called when a template has been rendered, connected functions # should have a signature of Callable[[Quart, Template, dict], None] template_rendered = _signals.signal("template-rendered") #: Called just after the request context has been created, connected # functions should have a signature of Callable[[Quart], None] request_started = _signals.signal("request-started") #: Called after a response is fully finalised, connected functions # should have a signature of Callable[[Quart, Response], None] request_finished = _signals.signal("request-finished") #: Called as the request context is teared down, connected functions # should have a signature of Callable[[Quart, Exception], None] request_tearing_down = _signals.signal("request-tearing-down") #: Called if there is an exception in a background task, connected # functions should have a signature of Callable[[Quart, Exception], None] got_background_exception = _signals.signal("got-background-exception") #: Called if there is an exception in a before or after serving # function, connected functions should have a signature of # Callable[[Quart, Exception], None] got_serving_exception = _signals.signal("got-serving-exception") #: Called if there is an exception handling the request, connected # functions should have a signature of Callable[[Quart, Exception], None] got_request_exception = _signals.signal("got-request-exception") #: Called just after the websocket context has been created, connected # functions should have a signature of Callable[[Quart], None] websocket_started = _signals.signal("websocket-started") #: Called on receipt of a message over the websocket, connected # functions should have a signature of Callable[[str | bytes], None] websocket_received = _signals.signal("websocket-received") #: Called when a message has been sent over the websocket, connected # functions should have a signature of Callable[[str | bytes], None] websocket_sent = _signals.signal("websocket-sent") #: Called after a response is fully finalised, connected functions # should have a signature of Callable[[Quart, Optional[Response]], None] websocket_finished = _signals.signal("websocket-finished") #: Called as the websocket context is teared down, connected functions # should have a signature of Callable[[Quart, Exception], None] websocket_tearing_down = _signals.signal("websocket-tearing-down") #: Called if there is an exception handling the websocket, connected # functions should have a signature of Callable[[Quart, Exception], None] got_websocket_exception = _signals.signal("got-websocket-exception") #: Called as the application context is teared down, connected functions # should have a signature of Callable[[Quart, Exception], None] appcontext_tearing_down = _signals.signal("appcontext-tearing-down") #: Called when the app context is pushed, connected functions should # have a signature of Callable[[Quart], None] appcontext_pushed = _signals.signal("appcontext-pushed") #: Called when the app context is popped, connected functions should # have a signature of Callable[[Quart], None] appcontext_popped = _signals.signal("appcontext-popped") #: Called on a flash invocation, connection functions # should have a signature of Callable[[Quart, str, str], None] message_flashed = _signals.signal("message-flashed") pallets-quart-6b318bd/src/quart/templating.py000066400000000000000000000127411524460054700214000ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncIterator from typing import Any from typing import TYPE_CHECKING from flask.templating import ( DispatchingJinjaLoader as DispatchingJinjaLoader, # noqa: F401 ) from jinja2 import Environment as BaseEnvironment from jinja2 import Template from .ctx import has_app_context from .ctx import has_request_context from .globals import app_ctx from .globals import current_app from .helpers import stream_with_context from .signals import before_render_template from .signals import template_rendered if TYPE_CHECKING: from .app import Quart # noqa from .ctx import _AppCtxGlobals from .sessions import SessionMixin from .wrappers import Request class Environment(BaseEnvironment): """Quart specific Jinja Environment. This changes the default Jinja loader to use the DispatchingJinjaLoader, and enables async Jinja by default. """ def __init__(self, app: Quart, **options: Any) -> None: """Create a Quart specific Jinja Environment. Arguments: app: The Quart app to bind to. options: The standard Jinja Environment options. """ if "loader" not in options: options["loader"] = app.create_global_jinja_loader() options["enable_async"] = True super().__init__(**options) async def render_template( template_name_or_list: str | list[str], **context: Any ) -> str: """Render the template with the context given. Arguments: template_name_or_list: Template name to render of a list of possible template names. context: The variables to pass to the template. """ ctx = app_ctx._get_current_object() await current_app.update_template_context(ctx, context) template = current_app.jinja_env.get_or_select_template(template_name_or_list) # type: ignore return await _render(template, context, current_app._get_current_object()) async def render_template_string(source: str, **context: Any) -> str: """Render the template source with the context given. Arguments: source: The template source code. context: The variables to pass to the template. """ ctx = app_ctx._get_current_object() await current_app.update_template_context(ctx, context) template = current_app.jinja_env.from_string(source) return await _render(template, context, current_app._get_current_object()) async def _render(template: Template, context: dict, app: Quart) -> str: await before_render_template.send_async( app, _sync_wrapper=app.ensure_async, # type: ignore[arg-type] template=template, context=context, ) rendered_template = await template.render_async(context) await template_rendered.send_async( app, _sync_wrapper=app.ensure_async, # type: ignore[arg-type] template=template, context=context, ) return rendered_template async def _default_template_ctx_processor() -> dict[str, Any]: context: dict[str, _AppCtxGlobals | Request | SessionMixin] = {} ctx = app_ctx._get_current_object() if has_app_context(): context["g"] = ctx.g if has_request_context(): context["request"] = ctx.request context["session"] = ctx.session return context async def stream_template( template_name_or_list: str | Template | list[str | Template], **context: Any ) -> AsyncIterator[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. Arguments: template_name_or_list: The name of the template to render. If a list is given, the first name to exist will be rendered. context: The variables to make available in the template. """ ctx = app_ctx._get_current_object() await current_app.update_template_context(ctx, context) template = current_app.jinja_env.get_or_select_template(template_name_or_list) return await _stream(current_app._get_current_object(), template, context) async def stream_template_string(source: str, **context: Any) -> AsyncIterator[str]: """Render a template from the given source with the *context* as a stream. This returns an iterator of strings, which can be used as a streaming response from a view. Arguments: source: The source code of the template to render. context: The variables to make available in the template. """ ctx = app_ctx._get_current_object() await current_app.update_template_context(ctx, context) template = current_app.jinja_env.from_string(source) return await _stream(current_app._get_current_object(), template, context) async def _stream( app: Quart, template: Template, context: dict[str, Any] ) -> AsyncIterator[str]: await before_render_template.send_async( app, _sync_wrapper=app.ensure_async, # type: ignore[arg-type] template=template, context=context, ) async def generate() -> AsyncIterator[str]: async for chunk in template.generate_async(context): yield chunk await template_rendered.send_async( app, _sync_wrapper=app.ensure_async, # type: ignore[arg-type] template=template, context=context, ) # If a request context is active, keep it while generating. if has_request_context(): return stream_with_context(generate)() else: return generate() pallets-quart-6b318bd/src/quart/testing/000077500000000000000000000000001524460054700203325ustar00rootroot00000000000000pallets-quart-6b318bd/src/quart/testing/__init__.py000066400000000000000000000022671524460054700224520ustar00rootroot00000000000000from __future__ import annotations from typing import Any from typing import TYPE_CHECKING from click.testing import CliRunner from ..cli import ScriptInfo from .app import TestApp from .client import QuartClient from .connections import WebsocketResponseError from .utils import make_test_body_with_headers from .utils import make_test_headers_path_and_query_string from .utils import make_test_scope from .utils import no_op_push from .utils import sentinel if TYPE_CHECKING: from ..app import Quart class QuartCliRunner(CliRunner): def __init__(self, app: Quart, **kwargs: Any) -> None: self.app = app super().__init__(**kwargs) def invoke(self, cli: Any = None, args: Any = None, **kwargs: Any) -> Any: # type: ignore 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) __all__ = ( "make_test_body_with_headers", "make_test_headers_path_and_query_string", "make_test_scope", "no_op_push", "QuartClient", "QuartCliRunner", "sentinel", "TestApp", "WebsocketResponseError", ) pallets-quart-6b318bd/src/quart/testing/app.py000066400000000000000000000052471524460054700214740ustar00rootroot00000000000000from __future__ import annotations import asyncio from collections.abc import Awaitable from types import TracebackType from typing import TYPE_CHECKING from hypercorn.typing import ASGIReceiveEvent from hypercorn.typing import ASGISendEvent from hypercorn.typing import LifespanScope from ..typing import TestClientProtocol if TYPE_CHECKING: from ..app import Quart # noqa DEFAULT_TIMEOUT = 6 class LifespanError(Exception): pass class TestApp: def __init__( self, app: Quart, startup_timeout: int = DEFAULT_TIMEOUT, shutdown_timeout: int = DEFAULT_TIMEOUT, ) -> None: self.app = app self.startup_timeout = startup_timeout self.shutdown_timeout = shutdown_timeout self._startup = asyncio.Event() self._shutdown = asyncio.Event() self._app_queue: asyncio.Queue = asyncio.Queue() self._task: Awaitable[None] = None def test_client(self) -> TestClientProtocol: return self.app.test_client() async def startup(self) -> None: scope: LifespanScope = { "type": "lifespan", "asgi": {"spec_version": "2.0"}, "state": {}, } self._task = asyncio.ensure_future( self.app(scope, self._asgi_receive, self._asgi_send) ) await self._app_queue.put({"type": "lifespan.startup"}) await asyncio.wait_for(self._startup.wait(), timeout=self.startup_timeout) if self._task.done(): # This will re-raise any exceptions in the task await self._task async def shutdown(self) -> None: await self._app_queue.put({"type": "lifespan.shutdown"}) await asyncio.wait_for(self._shutdown.wait(), timeout=self.shutdown_timeout) await self._task async def __aenter__(self) -> TestApp: await self.startup() return self async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: await self.shutdown() async def _asgi_receive(self) -> ASGIReceiveEvent: return await self._app_queue.get() async def _asgi_send(self, message: ASGISendEvent) -> None: if message["type"] == "lifespan.startup.complete": self._startup.set() elif message["type"] == "lifespan.shutdown.complete": self._shutdown.set() elif message["type"] == "lifespan.startup.failed": self._startup.set() raise LifespanError(f"Error during startup {message['message']}") elif message["type"] == "lifespan.shutdown.failed": self._shutdown.set() raise LifespanError(f"Error during shutdown {message['message']}") pallets-quart-6b318bd/src/quart/testing/client.py000066400000000000000000000344121524460054700221660ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from datetime import datetime from datetime import timedelta from http.cookiejar import CookieJar from types import TracebackType from typing import Any from typing import TYPE_CHECKING from urllib.request import Request as U2Request from werkzeug.datastructures import Authorization from werkzeug.datastructures import Headers from werkzeug.http import dump_cookie from ..datastructures import FileStorage from ..globals import _cv_app from ..sessions import SessionMixin from ..typing import TestHTTPConnectionProtocol from ..typing import TestWebsocketConnectionProtocol from ..wrappers import Response from .connections import TestHTTPConnection from .connections import TestWebsocketConnection from .utils import make_test_body_with_headers from .utils import make_test_headers_path_and_query_string from .utils import make_test_scope from .utils import sentinel if TYPE_CHECKING: from ..app import Quart # noqa class _TestWrapper: def __init__(self, headers: Headers) -> None: self.headers = headers def get_all(self, name: str, default: Any | None = None) -> list[str]: name = name.lower() result = [] for key, value in self.headers: if key.lower() == name: result.append(value) return result or default or [] class _TestCookieJarResponse: def __init__(self, headers: Headers) -> None: self.headers = headers def info(self) -> _TestWrapper: return _TestWrapper(self.headers) class QuartClient: http_connection_class: type[TestHTTPConnectionProtocol] websocket_connection_class: type[TestWebsocketConnectionProtocol] http_connection_class = TestHTTPConnection websocket_connection_class = TestWebsocketConnection def __init__(self, app: Quart, use_cookies: bool = True) -> None: self.app = app self.cookie_jar: CookieJar | None if use_cookies: self.cookie_jar = CookieJar() else: self.cookie_jar = None self.preserve_context = False self.push_promises: list[tuple[str, Headers]] = [] async def open( self, path: str, *, method: str = "GET", headers: dict | Headers | None = None, data: str | bytes | None = None, form: dict | None = None, files: dict[str, FileStorage] | None = None, query_string: dict | None = None, json: Any = sentinel, scheme: str = "http", follow_redirects: bool = False, root_path: str = "", http_version: str = "1.1", scope_base: dict | None = None, auth: Authorization | tuple[str, str] | None = None, subdomain: str | None = None, ) -> Response: self.push_promises = [] response = await self._make_request( path, method, headers, data, form, files, query_string, json, scheme, root_path, http_version, scope_base, auth, subdomain, ) if follow_redirects: while response.status_code >= 300 and response.status_code <= 399: # Most browsers respond to an HTTP 302 with a GET request to the # new location, despite what the HTTP spec says. HTTP 303 should # always be responded to with a GET request. if response.status_code == 302 or response.status_code == 303: method = "GET" response = await self._make_request( response.location, method, headers, data, form, files, query_string, json, scheme, root_path, http_version, scope_base, auth, subdomain, ) if self.preserve_context: _cv_app.set(self.app._preserved_context) return response def request( self, path: str, *, method: str = "GET", headers: dict | Headers | None = None, query_string: dict | None = None, scheme: str = "http", root_path: str = "", http_version: str = "1.1", scope_base: dict | None = None, auth: Authorization | tuple[str, str] | None = None, subdomain: str | None = None, ) -> TestHTTPConnectionProtocol: headers, path, query_string_bytes = make_test_headers_path_and_query_string( self.app, path, headers, query_string, auth, subdomain, ) if self.cookie_jar is not None: for cookie in self.cookie_jar: headers.add("cookie", f"{cookie.name}={cookie.value}") scope = make_test_scope( "http", path, method, headers, query_string_bytes, scheme, root_path, http_version, scope_base, _preserve_context=self.preserve_context, ) return self.http_connection_class( self.app, scope, _preserve_context=self.preserve_context ) def websocket( self, path: str, *, headers: dict | Headers | None = None, query_string: dict | None = None, scheme: str = "ws", subprotocols: list[str] | None = None, root_path: str = "", http_version: str = "1.1", scope_base: dict | None = None, auth: Authorization | tuple[str, str] | None = None, subdomain: str | None = None, ) -> TestWebsocketConnectionProtocol: headers, path, query_string_bytes = make_test_headers_path_and_query_string( self.app, path, headers, query_string, auth, subdomain, ) if self.cookie_jar is not None: for cookie in self.cookie_jar: headers.add("cookie", f"{cookie.name}={cookie.value}") scope = make_test_scope( "websocket", path, "GET", headers, query_string_bytes, scheme, root_path, http_version, scope_base, _preserve_context=self.preserve_context, ) return self.websocket_connection_class(self.app, scope) async def delete(self, *args: Any, **kwargs: Any) -> Response: """Make a DELETE request. See :meth:`~quart.testing.QuartClient.open` for argument details. """ return await self.open(*args, method="DELETE", **kwargs) async def get(self, *args: Any, **kwargs: Any) -> Response: """Make a GET request. See :meth:`~quart.testing.QuartClient.open` for argument details. """ return await self.open(*args, method="GET", **kwargs) async def head(self, *args: Any, **kwargs: Any) -> Response: """Make a HEAD request. See :meth:`~quart.testing.QuartClient.open` for argument details. """ return await self.open(*args, method="HEAD", **kwargs) async def options(self, *args: Any, **kwargs: Any) -> Response: """Make a OPTIONS request. See :meth:`~quart.testing.QuartClient.open` for argument details. """ return await self.open(*args, method="OPTIONS", **kwargs) async def patch(self, *args: Any, **kwargs: Any) -> Response: """Make a PATCH request. See :meth:`~quart.testing.QuartClient.open` for argument details. """ return await self.open(*args, method="PATCH", **kwargs) async def post(self, *args: Any, **kwargs: Any) -> Response: """Make a POST request. See :meth:`~quart.testing.QuartClient.open` for argument details. """ return await self.open(*args, method="POST", **kwargs) async def put(self, *args: Any, **kwargs: Any) -> Response: """Make a PUT request. See :meth:`~quart.testing.QuartClient.open` for argument details. """ return await self.open(*args, method="PUT", **kwargs) async def trace(self, *args: Any, **kwargs: Any) -> Response: """Make a TRACE request. See :meth:`~quart.testing.QuartClient.open` for argument details. """ return await self.open(*args, method="TRACE", **kwargs) def set_cookie( self, server_name: str, key: str, value: str = "", max_age: int | timedelta | None = None, expires: int | float | datetime | None = None, path: str = "/", domain: str | None = None, secure: bool = False, httponly: bool = False, samesite: str = None, ) -> None: """Set a cookie in the cookie jar. The arguments are the standard cookie morsels and this is a wrapper around the stdlib SimpleCookie code. """ cookie = dump_cookie( key, value=value, max_age=max_age, expires=expires, path=path, domain=domain, secure=secure, httponly=httponly, samesite=samesite, ) self.cookie_jar.extract_cookies( _TestCookieJarResponse(Headers([("set-cookie", cookie)])), # type: ignore U2Request(f"http://{server_name}{path}"), ) def delete_cookie( self, server_name: str, key: str, path: str = "/", domain: str | None = None ) -> None: """Delete a cookie (set to expire immediately).""" self.set_cookie( server_name, key, expires=0, max_age=0, path=path, domain=domain ) @asynccontextmanager async def session_transaction( self, path: str = "/", *, method: str = "GET", headers: dict | Headers | None = None, query_string: dict | None = None, scheme: str = "http", data: str | bytes | None = None, form: dict | None = None, json: Any = sentinel, root_path: str = "", http_version: str = "1.1", auth: Authorization | tuple[str, str] | None = None, ) -> AsyncGenerator[SessionMixin]: if self.cookie_jar is None: raise RuntimeError( "Session transactions only make sense with cookies enabled." ) if headers is None: headers = Headers() elif isinstance(headers, Headers): headers = headers elif headers is not None: headers = Headers(headers) for cookie in self.cookie_jar: headers.add("cookie", f"{cookie.name}={cookie.value}") original_ctx = _cv_app.get(None) async with self.app.test_request_context( path, method=method, headers=headers, query_string=query_string, scheme=scheme, data=data, form=form, json=json, root_path=root_path, http_version=http_version, auth=auth, ) as ctx: session_interface = self.app.session_interface session = await session_interface.open_session(self.app, ctx.request) if session is None: raise RuntimeError("Error opening the session. Check the secret_key?") token = _cv_app.set(original_ctx) try: yield session finally: _cv_app.reset(token) response = self.app.response_class(b"") if not session_interface.is_null_session(session): await session_interface.save_session(self.app, session, response) self.cookie_jar.extract_cookies( _TestCookieJarResponse(response.headers), # type: ignore U2Request(ctx.request.url), ) async def __aenter__(self) -> QuartClient: if self.preserve_context: raise RuntimeError("Cannot nest client invocations") self.preserve_context = True return self async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: self.preserve_context = False async def _make_request( self, path: str, method: str, headers: dict | Headers | None, data: str | bytes | None, form: dict | None, files: dict[str, FileStorage] | None, query_string: dict | None, json: Any, scheme: str, root_path: str, http_version: str, scope_base: dict | None, auth: Authorization | tuple[str, str] | None = None, subdomain: str | None = None, ) -> Response: headers, path, query_string_bytes = make_test_headers_path_and_query_string( self.app, path, headers, query_string, auth, subdomain ) request_data, body_headers = make_test_body_with_headers( data=data, form=form, files=files, json=json, app=self.app ) headers.update(**body_headers) if self.cookie_jar is not None: for cookie in self.cookie_jar: headers.add("cookie", f"{cookie.name}={cookie.value}") scope = make_test_scope( "http", path, method, headers, query_string_bytes, scheme, root_path, http_version, scope_base, _preserve_context=self.preserve_context, ) async with self.http_connection_class( self.app, scope, _preserve_context=self.preserve_context ) as connection: await connection.send(request_data) await connection.send_complete() response = await connection.as_response() if self.cookie_jar is not None: self.cookie_jar.extract_cookies( _TestCookieJarResponse(response.headers), # type: ignore U2Request(f"{scheme}://{headers['host']}{path}"), ) self.push_promises.extend(connection.push_promises) return response pallets-quart-6b318bd/src/quart/testing/connections.py000066400000000000000000000151611524460054700232320ustar00rootroot00000000000000from __future__ import annotations import asyncio from collections.abc import Awaitable from types import TracebackType from typing import Any from typing import TYPE_CHECKING from hypercorn.typing import ASGIReceiveEvent from hypercorn.typing import ASGISendEvent from hypercorn.typing import HTTPScope from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Headers from ..json import dumps from ..json import loads from ..utils import decode_headers from ..wrappers import Response if TYPE_CHECKING: from ..app import Quart # noqa class WebsocketResponseError(Exception): def __init__(self, response: Response) -> None: super().__init__(response) self.response = response class TestHTTPConnection: def __init__( self, app: Quart, scope: HTTPScope, _preserve_context: bool = False ) -> None: self.app = app self.headers: Headers | None = None self.push_promises: list[tuple[str, Headers]] = [] self.response_data = bytearray() self.scope = scope self.status_code: int | None = None self._preserve_context = _preserve_context self._send_queue: asyncio.Queue = asyncio.Queue() self._receive_queue: asyncio.Queue = asyncio.Queue() self._task: Awaitable[None] = None async def send(self, data: bytes) -> None: await self._send_queue.put( {"type": "http.request", "body": data, "more_body": True} ) async def send_complete(self) -> None: await self._send_queue.put( {"type": "http.request", "body": b"", "more_body": False} ) async def receive(self) -> bytes: data = await self._receive_queue.get() if isinstance(data, Exception): raise data else: return data async def disconnect(self) -> None: await self._send_queue.put({"type": "http.disconnect"}) async def __aenter__(self) -> TestHTTPConnection: self._task = asyncio.ensure_future( self.app(self.scope, self._asgi_receive, self._asgi_send) ) return self async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: if exc_type is not None: await self.disconnect() await self._task while not self._receive_queue.empty(): data = await self._receive_queue.get() if isinstance(data, bytes): self.response_data.extend(data) elif isinstance(data, Exception): raise data else: raise ValueError(f"Unexpected event {data}") async def as_response(self) -> Response: while not self._receive_queue.empty(): data = await self._receive_queue.get() if isinstance(data, bytes): self.response_data.extend(data) return self.app.response_class( bytes(self.response_data), self.status_code, self.headers ) async def _asgi_receive(self) -> ASGIReceiveEvent: return await self._send_queue.get() async def _asgi_send(self, message: ASGISendEvent) -> None: if message["type"] == "http.response.start": self.headers = decode_headers(message["headers"]) self.status_code = message["status"] elif message["type"] == "http.response.body": await self._receive_queue.put(message["body"]) if not message["more_body"]: await self.disconnect() elif message["type"] == "http.response.push": self.push_promises.append( (message["path"], decode_headers(message["headers"])) ) class TestWebsocketConnection: def __init__(self, app: Quart, scope: WebsocketScope) -> None: self.accepted = False self.app = app self.headers: Headers | None = None self.response_data = bytearray() self.scope = scope self.status_code: int | None = None self._send_queue: asyncio.Queue = asyncio.Queue() self._receive_queue: asyncio.Queue = asyncio.Queue() self._task: Awaitable[None] = None async def __aenter__(self) -> TestWebsocketConnection: self._task = asyncio.ensure_future( self.app(self.scope, self._asgi_receive, self._asgi_send) ) return self async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: await self.disconnect() await self._task while not self._receive_queue.empty(): data = await self._receive_queue.get() if isinstance(data, Exception): raise data async def receive(self) -> str | bytes: data = await self._receive_queue.get() if isinstance(data, Exception): raise data else: return data async def send(self, data: str | bytes) -> None: if isinstance(data, str): await self._send_queue.put({"type": "websocket.receive", "text": data}) else: await self._send_queue.put({"type": "websocket.receive", "bytes": data}) async def receive_json(self) -> Any: data = await self.receive() return loads(data) async def send_json(self, data: Any) -> None: raw = dumps(data) await self.send(raw) async def close(self, code: int) -> None: await self._send_queue.put({"type": "websocket.close", "code": code}) async def disconnect(self) -> None: await self._send_queue.put({"type": "websocket.disconnect"}) async def _asgi_receive(self) -> ASGIReceiveEvent: return await self._send_queue.get() async def _asgi_send(self, message: ASGISendEvent) -> None: if message["type"] == "websocket.accept": self.accepted = True elif message["type"] == "websocket.send": await self._receive_queue.put(message.get("bytes") or message.get("text")) elif message["type"] == "websocket.http.response.start": self.headers = decode_headers(message["headers"]) self.status_code = message["status"] elif message["type"] == "websocket.http.response.body": self.response_data.extend(message["body"]) if not message.get("more_body", False): await self._receive_queue.put( WebsocketResponseError( self.app.response_class( bytes(self.response_data), self.status_code, self.headers ) ) ) elif message["type"] == "websocket.close": await self.disconnect() pallets-quart-6b318bd/src/quart/testing/utils.py000066400000000000000000000163451524460054700220550ustar00rootroot00000000000000from __future__ import annotations from typing import Any from typing import cast from typing import Literal from typing import overload from typing import TYPE_CHECKING from urllib.parse import unquote from urllib.parse import urlencode from hypercorn.typing import HTTPScope from hypercorn.typing import Scope from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Authorization from werkzeug.datastructures import Headers from werkzeug.sansio.multipart import Data from werkzeug.sansio.multipart import Epilogue from werkzeug.sansio.multipart import Field from werkzeug.sansio.multipart import File from werkzeug.sansio.multipart import MultipartEncoder from werkzeug.sansio.multipart import Preamble from werkzeug.urls import iri_to_uri from ..datastructures import FileStorage from ..json import dumps from ..utils import encode_headers if TYPE_CHECKING: from ..app import Quart # noqa sentinel = object() def make_test_headers_path_and_query_string( app: Quart, path: str, headers: dict | Headers | None = None, query_string: dict | None = None, auth: Authorization | tuple[str, str] | None = None, subdomain: str | None = None, ) -> tuple[Headers, str, bytes]: """Make the headers and path with defaults for testing. Arguments: app: The application to test against. path: The path to request. If the query_string argument is not defined this argument will be partitioned on a '?' with the following part being considered the query_string. headers: Initial headers to send. query_string: To send as a dictionary, alternatively the query_string can be determined from the path. """ if headers is None: headers = Headers() elif isinstance(headers, Headers): headers = headers elif headers is not None: headers = Headers(headers) if auth is not None: if isinstance(auth, tuple): auth = Authorization("basic", {"username": auth[0], "password": auth[1]}) headers.setdefault("Authorization", auth.to_header()) headers.setdefault("User-Agent", "Quart") host = app.config["SERVER_NAME"] or "localhost" if subdomain is not None: host = f"{subdomain}.{host}" headers.setdefault("host", host) if "?" in path and query_string is not None: raise ValueError("Query string is defined in the path and as an argument") if query_string is None: path, _, query_string_raw = path.partition("?") else: query_string_raw = urlencode(query_string, doseq=True) query_string_bytes = query_string_raw.encode("ascii") return headers, unquote(path), query_string_bytes def make_test_body_with_headers( *, data: str | bytes | None = None, form: dict | None = None, files: dict[str, FileStorage] | None = None, json: Any = sentinel, app: Quart | None = None, ) -> tuple[bytes, Headers]: """Make the body bytes with associated headers. Arguments: data: Raw data to send in the request body. form: Key value paired data to send form encoded in the request body. files: Key FileStorage paired data to send as file encoded in the request body. json: Data to send json encoded in the request body. """ if [json is not sentinel, form is not None, data is not None].count(True) > 1: raise ValueError( "Quart test args 'json', 'form', and 'data' are mutually exclusive" ) if [json is not sentinel, files is not None, data is not None].count(True) > 1: raise ValueError( "Quart test args 'files', 'json', and 'data' are mutually exclusive" ) request_data = b"" headers = Headers() if isinstance(data, str): request_data = data.encode("utf-8") elif isinstance(data, bytes): request_data = data if json is not sentinel: request_data = dumps(json).encode("utf-8") headers["Content-Type"] = "application/json" elif files is not None: boundary = "----QuartBoundary" headers["Content-Type"] = f"multipart/form-data; boundary={boundary}" encoder = MultipartEncoder(boundary.encode()) request_data += encoder.send_event(Preamble(data=b"")) for key, file_storage in files.items(): request_data += encoder.send_event( File( name=key, filename=file_storage.filename, headers=file_storage.headers, ) ) chunk = file_storage.read(16384) while chunk != b"": request_data += encoder.send_event(Data(data=chunk, more_data=True)) chunk = file_storage.read(16384) request_data += encoder.send_event(Data(data=b"", more_data=False)) if form is not None: for key, value in form.items(): request_data += encoder.send_event(Field(name=key, headers=Headers())) request_data += encoder.send_event( Data(data=value.encode("utf-8"), more_data=False) ) request_data += encoder.send_event(Epilogue(data=b"")) elif form is not None: request_data = urlencode(form).encode("utf-8") headers["Content-Type"] = "application/x-www-form-urlencoded" return request_data, headers @overload def make_test_scope( type_: Literal["http"], path: str, method: str, headers: Headers, query_string: bytes, scheme: str, root_path: str, http_version: str, scope_base: dict | None, *, _preserve_context: bool = False, ) -> HTTPScope: ... @overload def make_test_scope( type_: Literal["websocket"], path: str, method: str, headers: Headers, query_string: bytes, scheme: str, root_path: str, http_version: str, scope_base: dict | None, *, _preserve_context: bool = False, ) -> WebsocketScope: ... def make_test_scope( type_: str, path: str, method: str, headers: Headers, query_string: bytes, scheme: str, root_path: str, http_version: str, scope_base: dict | None, *, _preserve_context: bool = False, ) -> Scope: scope = { "type": type_, "http_version": http_version, "asgi": {"spec_version": "2.1"}, "method": method, "scheme": scheme, "path": path, "raw_path": iri_to_uri(path).encode("ascii"), "query_string": query_string, "root_path": root_path, "headers": encode_headers(headers), "extensions": {}, "_quart._preserve_context": _preserve_context, } if scope_base is not None: scope.update(scope_base) if type_ == "http" and http_version in {"2", "3"}: scope["extensions"] = {"http.response.push": {}} elif type_ == "websocket": scope["extensions"] = {"websocket.http.response": {}} return cast(Scope, scope) async def no_op_push(path: str, headers: Headers) -> None: """A push promise sender that does nothing. This is best used when creating Request instances for testing outside of the QuartClient. The Request instance must know what to do with push promises, and this gives it the option of doing nothing. """ pass pallets-quart-6b318bd/src/quart/typing.py000066400000000000000000000232061524460054700205440ustar00rootroot00000000000000from __future__ import annotations import os from collections.abc import AsyncGenerator from collections.abc import Awaitable from collections.abc import Callable from collections.abc import Iterator from collections.abc import Mapping from collections.abc import Sequence from contextlib import AbstractAsyncContextManager from datetime import datetime from datetime import timedelta from http.cookiejar import CookieJar from types import TracebackType from typing import Any from typing import Protocol from typing import TYPE_CHECKING from typing import Union from hypercorn.typing import ASGIReceiveCallable from hypercorn.typing import ASGISendCallable from hypercorn.typing import HTTPScope from hypercorn.typing import LifespanScope from hypercorn.typing import WebsocketScope from .datastructures import FileStorage if TYPE_CHECKING: from werkzeug.datastructures import Authorization # noqa: F401 from werkzeug.datastructures import Headers # noqa: F401 from werkzeug.wrappers import Response as WerkzeugResponse # noqa: F401 from .app import Quart from .sessions import SessionMixin from .wrappers.response import Response # noqa: F401 FilePath = bytes | str | os.PathLike # The possible types that are directly convertible or are a Response object. ResponseValue = Union[ "Response", "WerkzeugResponse", bytes, str, Mapping[str, Any], # any jsonify-able dict list[Any], # any jsonify-able list Iterator[bytes], Iterator[str], ] StatusCode = int # the possible types for an individual HTTP header HeaderName = str HeaderValue = str | list[str] | tuple[str, ...] # the possible types for HTTP headers HeadersValue = Union[ "Headers", Mapping[HeaderName, HeaderValue], Sequence[tuple[HeaderName, HeaderValue]], ] # The possible types returned by a route function. ResponseReturnValue = ( ResponseValue | tuple[ResponseValue, HeadersValue] | tuple[ResponseValue, StatusCode] | tuple[ResponseValue, StatusCode, HeadersValue] ) ResponseTypes = Union["Response", "WerkzeugResponse"] AppOrBlueprintKey = str | None # The App key is None, whereas blueprints are named AfterRequestCallable = ( Callable[[ResponseTypes], ResponseTypes] | Callable[[ResponseTypes], Awaitable[ResponseTypes]] ) AfterServingCallable = Callable[[], None] | Callable[[], Awaitable[None]] AfterWebsocketCallable = ( Callable[[ResponseTypes | None], ResponseTypes | None] | Callable[[ResponseTypes | None], Awaitable[ResponseTypes | None]] ) BeforeRequestCallable = ( Callable[[], ResponseReturnValue | None] | Callable[[], Awaitable[ResponseReturnValue | None]] ) BeforeServingCallable = Callable[[], None] | Callable[[], Awaitable[None]] BeforeWebsocketCallable = ( Callable[[], ResponseReturnValue | None] | Callable[[], Awaitable[ResponseReturnValue | None]] ) ErrorHandlerCallable = ( Callable[[Any], ResponseReturnValue] | Callable[[Any], Awaitable[ResponseReturnValue]] ) ShellContextProcessorCallable = Callable[[], dict[str, Any]] TeardownCallable = ( Callable[[BaseException | None], None] | Callable[[BaseException | None], Awaitable[None]] ) TemplateContextProcessorCallable = ( Callable[[], dict[str, Any]] | Callable[[], Awaitable[dict[str, Any]]] ) TemplateFilterCallable = Callable[[Any], Any] TemplateGlobalCallable = Callable[[Any], Any] TemplateTestCallable = Callable[[Any], bool] URLDefaultCallable = Callable[[str, dict], None] URLValuePreprocessorCallable = Callable[[str | None, dict | None], None] WhileServingCallable = Callable[[], AsyncGenerator[None]] RouteCallable = ( Callable[..., ResponseReturnValue] | Callable[..., Awaitable[ResponseReturnValue]] ) WebsocketCallable = ( Callable[..., ResponseReturnValue | None] | Callable[..., Awaitable[ResponseReturnValue | None]] ) class ASGIHTTPProtocol(Protocol): def __init__(self, app: Quart, scope: HTTPScope) -> None: ... async def __call__( self, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: ... class ASGILifespanProtocol(Protocol): def __init__(self, app: Quart, scope: LifespanScope) -> None: ... async def __call__( self, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: ... class ASGIWebsocketProtocol(Protocol): def __init__(self, app: Quart, scope: WebsocketScope) -> None: ... async def __call__( self, receive: ASGIReceiveCallable, send: ASGISendCallable ) -> None: ... class TestHTTPConnectionProtocol(Protocol): push_promises: list[tuple[str, Headers]] def __init__( self, app: Quart, scope: HTTPScope, _preserve_context: bool = False ) -> None: ... async def send(self, data: bytes) -> None: ... async def send_complete(self) -> None: ... async def receive(self) -> bytes: ... async def disconnect(self) -> None: ... async def __aenter__(self) -> TestHTTPConnectionProtocol: ... async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: ... async def as_response(self) -> Response: ... class TestWebsocketConnectionProtocol(Protocol): def __init__(self, app: Quart, scope: WebsocketScope) -> None: ... async def __aenter__(self) -> TestWebsocketConnectionProtocol: ... async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: ... async def receive(self) -> str | bytes: ... async def send(self, data: str | bytes) -> None: ... async def receive_json(self) -> Any: ... async def send_json(self, data: Any) -> None: ... async def close(self, code: int) -> None: ... async def disconnect(self) -> None: ... class TestClientProtocol(Protocol): app: Quart cookie_jar: CookieJar | None http_connection_class: type[TestHTTPConnectionProtocol] push_promises: list[tuple[str, Headers]] websocket_connection_class: type[TestWebsocketConnectionProtocol] def __init__(self, app: Quart, use_cookies: bool = True) -> None: ... async def open( self, path: str, *, method: str = "GET", headers: dict | Headers | None = None, data: str | bytes | None = None, form: dict | None = None, files: dict[str, FileStorage] | None = None, query_string: dict | None = None, json: Any, scheme: str = "http", follow_redirects: bool = False, root_path: str = "", http_version: str = "1.1", scope_base: dict | None = None, auth: Authorization | tuple[str, str] | None = None, subdomain: str | None = None, ) -> Response: ... def request( self, path: str, *, method: str = "GET", headers: dict | Headers | None = None, query_string: dict | None = None, scheme: str = "http", root_path: str = "", http_version: str = "1.1", scope_base: dict | None = None, auth: Authorization | tuple[str, str] | None = None, subdomain: str | None = None, ) -> TestHTTPConnectionProtocol: ... def websocket( self, path: str, *, headers: dict | Headers | None = None, query_string: dict | None = None, scheme: str = "ws", subprotocols: list[str] | None = None, root_path: str = "", http_version: str = "1.1", scope_base: dict | None = None, auth: Authorization | tuple[str, str] | None = None, subdomain: str | None = None, ) -> TestWebsocketConnectionProtocol: ... async def delete(self, *args: Any, **kwargs: Any) -> Response: ... async def get(self, *args: Any, **kwargs: Any) -> Response: ... async def head(self, *args: Any, **kwargs: Any) -> Response: ... async def options(self, *args: Any, **kwargs: Any) -> Response: ... async def patch(self, *args: Any, **kwargs: Any) -> Response: ... async def post(self, *args: Any, **kwargs: Any) -> Response: ... async def put(self, *args: Any, **kwargs: Any) -> Response: ... async def trace(self, *args: Any, **kwargs: Any) -> Response: ... def set_cookie( self, server_name: str, key: str, value: str = "", max_age: int | timedelta | None = None, expires: int | float | datetime | None = None, path: str = "/", domain: str | None = None, secure: bool = False, httponly: bool = False, samesite: str = None, charset: str = "utf-8", ) -> None: ... def delete_cookie( self, server_name: str, key: str, path: str = "/", domain: str | None = None ) -> None: ... def session_transaction( self, path: str = "/", *, method: str = "GET", headers: dict | Headers | None = None, query_string: dict | None = None, scheme: str = "http", data: str | bytes | None = None, form: dict | None = None, json: Any = None, root_path: str = "", http_version: str = "1.1", ) -> AbstractAsyncContextManager[SessionMixin]: ... async def __aenter__(self) -> TestClientProtocol: ... async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: ... class TestAppProtocol(Protocol): def __init__(self, app: Quart) -> None: ... def test_client(self) -> TestClientProtocol: ... async def startup(self) -> None: ... async def shutdown(self) -> None: ... async def __aenter__(self) -> TestAppProtocol: ... async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: ... class Event(Protocol): def is_set(self) -> bool: ... def set(self) -> None: ... pallets-quart-6b318bd/src/quart/utils.py000066400000000000000000000115541524460054700203750ustar00rootroot00000000000000from __future__ import annotations import asyncio import inspect import os import subprocess import sys from collections.abc import AsyncIterator from collections.abc import Awaitable from collections.abc import Callable from collections.abc import Coroutine from collections.abc import Iterable from collections.abc import Iterator from contextvars import copy_context from functools import partial from functools import wraps from pathlib import Path from typing import Any from typing import TYPE_CHECKING from typing import TypeVar from werkzeug.datastructures import Headers from .typing import Event from .typing import FilePath if TYPE_CHECKING: from .wrappers.response import Response # noqa: F401 class MustReloadError(Exception): pass def file_path_to_path(*paths: FilePath) -> Path: # Flask supports bytes paths safe_paths: list[str | os.PathLike] = [] for path in paths: if isinstance(path, bytes): safe_paths.append(path.decode()) else: safe_paths.append(path) return Path(*safe_paths) def run_sync(func: Callable[..., Any]) -> Callable[..., Coroutine[None, None, Any]]: """Ensure that the sync function is run within the event loop. If the *func* is not a coroutine it will be wrapped such that it runs in the default executor (use loop.set_default_executor to change). This ensures that synchronous functions do not block the event loop. """ @wraps(func) async def _wrapper(*args: Any, **kwargs: Any) -> Any: loop = asyncio.get_running_loop() result = await loop.run_in_executor( None, copy_context().run, partial(func, *args, **kwargs) ) if inspect.isgenerator(result): return run_sync_iterable(result) else: return result _wrapper._quart_async_wrapper = True # type: ignore return _wrapper T = TypeVar("T") def run_sync_iterable(iterable: Iterator[T]) -> AsyncIterator[T]: async def _gen_wrapper() -> AsyncIterator[T]: # Wrap the generator such that each iteration runs # in the executor. Then rationalise the raised # errors so that it ends. def _inner() -> T: # https://bugs.python.org/issue26221 # StopIteration errors are swallowed by the # run_in_exector method try: return next(iterable) except StopIteration as e: raise StopAsyncIteration() from e loop = asyncio.get_running_loop() while True: try: yield await loop.run_in_executor(None, copy_context().run, _inner) except StopAsyncIteration: return return _gen_wrapper() def encode_headers(headers: Headers) -> list[tuple[bytes, bytes]]: return [(key.lower().encode(), value.encode()) for key, value in headers.items()] def decode_headers(headers: Iterable[tuple[bytes, bytes]]) -> Headers: return Headers([(key.decode(), value.decode()) for key, value in headers]) async def observe_changes( sleep: Callable[[float], Awaitable[Any]], shutdown_event: Event ) -> None: last_updates: dict[Path, float] = {} for module in list(sys.modules.values()): filename = getattr(module, "__file__", None) if filename is None: continue path = Path(filename) try: last_updates[Path(filename)] = path.stat().st_mtime except (FileNotFoundError, NotADirectoryError): pass while not shutdown_event.is_set(): await sleep(1) for index, (path, last_mtime) in enumerate(last_updates.items()): if index % 10 == 0: # Yield to the event loop await sleep(0) try: mtime = path.stat().st_mtime except FileNotFoundError as e: # File deleted raise MustReloadError() from e else: if mtime > last_mtime: raise MustReloadError() else: last_updates[path] = mtime async def cancel_tasks(tasks: set[asyncio.Task]) -> None: # Cancel any pending, and wait for the cancellation to # complete i.e. finish any remaining work. for task in tasks: task.cancel() await asyncio.gather(*tasks, return_exceptions=True) raise_task_exceptions(tasks) def raise_task_exceptions(tasks: set[asyncio.Task]) -> None: # Raise any unexpected exceptions for task in tasks: if not task.cancelled() and task.exception() is not None: raise task.exception() def run_reloader() -> None: while True: args = [sys.executable, *sys.orig_argv[1:]] new_environ = os.environ.copy() new_environ["QUART_RUN_MAIN"] = "true" exit_code = subprocess.call(args, env=new_environ, close_fds=False) if exit_code == 3: sys.exit(0) pallets-quart-6b318bd/src/quart/views.py000066400000000000000000000103721524460054700203670ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable from collections.abc import Collection from typing import Any from typing import ClassVar from .globals import current_app from .globals import request from .typing import ResponseReturnValue from .typing import RouteCallable http_method_funcs = frozenset( ["get", "post", "head", "options", "delete", "put", "trace", "patch"] ) class View: """Use to define routes within a class structure. A View subclass must implement the :meth:`dispatch_request` in order to respond to requests. For automatic method finding based on the request HTTP Verb see :class:`MethodView`. An example usage is, .. code-block:: python class SimpleView: methods = ['GET'] async def dispatch_request(id): return f"ID is {id}" app.add_url_rule('/', view_func=SimpleView.as_view('simple')) Note that class Attributes: decorators: A list of decorators to apply to a view method. The decorators are applied in the order of the list. methods: List of methods this view allows. provide_automatic_options: Override automatic OPTIONS if set, to either True or False. init_every_request: Create a new instance of this class for every request. """ decorators: ClassVar[list[Callable]] = [] methods: ClassVar[Collection[str] | None] = None provide_automatic_options: ClassVar[bool | None] = None init_every_request: ClassVar[bool] = True async def dispatch_request(self, **kwargs: Any) -> ResponseReturnValue: """Override and return a Response. This will be called with the request view_args, i.e. any url parameters. """ raise NotImplementedError() @classmethod def as_view(cls, name: str, *class_args: Any, **class_kwargs: Any) -> RouteCallable: if cls.init_every_request: async def view(**kwargs: Any) -> ResponseReturnValue: self = view.view_class(*class_args, **class_kwargs) # type: ignore return await current_app.ensure_async(self.dispatch_request)(**kwargs) else: self = cls(*class_args, **class_kwargs) async def view(**kwargs: Any) -> ResponseReturnValue: return await current_app.ensure_async(self.dispatch_request)(**kwargs) if cls.decorators: view.__name__ = name view.__module__ = cls.__module__ for decorator in cls.decorators: view = decorator(view) view.view_class: type[View] = 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): """A HTTP Method (verb) specific view class. This has an implementation of :meth:`dispatch_request` such that it calls a method based on the verb i.e. GET requests are handled by a `get` method. For example, .. code-block:: python class SimpleView(MethodView): async def get(id): return f"Get {id}" async def post(id): return f"Post {id}" app.add_url_rule('/', view_func=SimpleView.as_view('simple')) """ def __init_subclass__(cls, **kwargs: 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 async def dispatch_request(self, **kwargs: Any) -> ResponseReturnValue: handler = getattr(self, request.method.lower(), None) if handler is None and request.method == "HEAD": handler = getattr(self, "get", None) return await current_app.ensure_async(handler)(**kwargs) pallets-quart-6b318bd/src/quart/wrappers/000077500000000000000000000000001524460054700205205ustar00rootroot00000000000000pallets-quart-6b318bd/src/quart/wrappers/__init__.py000066400000000000000000000005621524460054700226340ustar00rootroot00000000000000from __future__ import annotations from .base import BaseRequestWebsocket from .base import ClientDisconnectedError from .request import Body from .request import Request from .response import Response from .websocket import Websocket __all__ = ( "BaseRequestWebsocket", "Body", "ClientDisconnectedError", "Request", "Response", "Websocket", ) pallets-quart-6b318bd/src/quart/wrappers/base.py000066400000000000000000000070761524460054700220160ustar00rootroot00000000000000from __future__ import annotations from typing import Any from typing import TYPE_CHECKING from hypercorn.typing import WWWScope from werkzeug.datastructures import Headers from werkzeug.sansio.request import Request as SansIORequest from .. import json if TYPE_CHECKING: from ..routing import QuartRule # noqa class ClientDisconnectedError(Exception): pass class BaseRequestWebsocket(SansIORequest): """This class is the basis for Requests and websockets.. Attributes: json_module: A custom json decoding/encoding module, it should have `dump`, `dumps`, `load`, and `loads` methods routing_exception: If an exception is raised during the route matching it will be stored here. url_rule: The rule that this request has been matched too. view_args: The keyword arguments for the view from the route matching. """ json_module: json.provider.JSONProvider = json # type: ignore routing_exception: Exception | None = None url_rule: QuartRule | None = None view_args: dict[str, Any] | None = None def __init__( self, method: str, scheme: str, path: str, query_string: bytes, headers: Headers, root_path: str, http_version: str, scope: WWWScope, ) -> None: """Create a request or websocket base object. Arguments: method: The HTTP verb. scheme: The scheme used for the request. path: The full unquoted path of the request. query_string: The raw bytes for the query string part. headers: The request headers. root_path: The root path that should be prepended to all routes. http_version: The HTTP version of the request. scope: Underlying ASGI scope dictionary. Attributes: args: The query string arguments. scheme: The URL scheme, http or https. """ super().__init__( method, scheme, scope.get("server"), root_path, path, query_string, headers, headers.get("Remote-Addr"), ) self.http_version = http_version self.scope = scope @property def endpoint(self) -> str | None: """Returns the corresponding endpoint matched for this request. This can be None if the request has not been matched with a rule. """ if self.url_rule is not None: return self.url_rule.endpoint else: return None @property def blueprint(self) -> str | None: """Returns the blueprint the matched endpoint belongs to. This can be None if the request has not been matched or the endpoint is not in a blueprint. """ if self.endpoint is not None and "." in self.endpoint: return self.endpoint.rsplit(".", 1)[0] else: return None @property def blueprints(self) -> list[str]: """Return the names of the current blueprints. The returned list is ordered from the current blueprint, upwards through parent blueprints. """ # Avoid circular import from ..helpers import _split_blueprint_path if self.blueprint is not None: return _split_blueprint_path(self.blueprint) else: return [] @property def script_root(self) -> str: return self.root_path @property def url_root(self) -> str: return self.root_url pallets-quart-6b318bd/src/quart/wrappers/request.py000066400000000000000000000327121524460054700225670ustar00rootroot00000000000000from __future__ import annotations import asyncio from collections.abc import Awaitable from collections.abc import Callable from collections.abc import Generator from typing import Any from typing import Literal from typing import NoReturn from typing import overload from hypercorn.typing import HTTPScope from werkzeug.datastructures import CombinedMultiDict from werkzeug.datastructures import Headers from werkzeug.datastructures import iter_multi_items from werkzeug.datastructures import MultiDict from werkzeug.exceptions import BadRequest from werkzeug.exceptions import RequestEntityTooLarge from werkzeug.exceptions import RequestTimeout from ..datastructures import FileStorage from ..formparser import FormDataParser from ..globals import current_app from .base import BaseRequestWebsocket from .base import ClientDisconnectedError SERVER_PUSH_HEADERS_TO_COPY = { "accept", "accept-encoding", "accept-language", "cache-control", "user-agent", } class Body: """A request body container. The request body can either be iterated over and consumed in parts (without building up memory usage) or awaited. .. code-block:: python async for data in body: ... # or simply complete = await body Note: It is not possible to iterate over the data and then await it. """ def __init__( self, expected_content_length: int | None, max_content_length: int | None ) -> None: self._data: bytes | None = None self._complete: asyncio.Event = asyncio.Event() self._max_content_length = max_content_length self._queue = asyncio.Queue[bytes]() # Exceptions must be raised within application (not ASGI) # calls, this is achieved by having the ASGI methods set this # to an exception on error. self._must_raise: Exception | None = None if ( expected_content_length is not None and max_content_length is not None and expected_content_length > max_content_length ): self._must_raise = RequestEntityTooLarge() def __aiter__(self) -> Body: return self async def __anext__(self) -> bytes: if self._must_raise is not None: raise self._must_raise if self._queue.empty() and self._complete.is_set(): raise StopAsyncIteration() return await self.get() def __await__(self) -> Generator[Any, None, Any]: # Must check the _must_raise before and after waiting on the # completion event as it may change whilst waiting and the # event may not be set if there is already an issue. if self._must_raise is not None: raise self._must_raise if self._data is not None: return self._data yield from self._complete.wait().__await__() if self._must_raise is not None: raise self._must_raise data = bytearray() while not self._queue.empty(): data.extend(self._queue.get_nowait()) if ( self._max_content_length is not None and len(data) > self._max_content_length ): raise RequestEntityTooLarge() self._data = bytes(data) return self._data async def put(self, data: bytes) -> None: await self._queue.put(data) async def get(self) -> bytes: try: return await self._queue.get() except asyncio.QueueShutDown: if self._complete.is_set(): return b"" else: raise ClientDisconnectedError() from None def disconnect(self) -> None: self._queue.shutdown() def set_complete(self) -> None: self._complete.set() self._queue.shutdown() def set_result(self, data: bytes) -> None: """Convenience method, mainly for testing.""" self._queue.put_nowait(data) self.set_complete() def clear(self) -> None: self._data = b"" class Request(BaseRequestWebsocket): """This class represents a request. It can be subclassed and the subclassed used in preference by replacing the :attr:`~quart.Quart.request_class` with your subclass. Attributes: body_class: The class to store the body data within. form_data_parser_class: Can be overridden to implement a different form data parsing. """ body_class = Body form_data_parser_class = FormDataParser lock_class = asyncio.Lock _max_content_length: int | None = None _max_form_memory_size: int | None = None _max_form_parts: int | None = None def __init__( self, method: str, scheme: str, path: str, query_string: bytes, headers: Headers, root_path: str, http_version: str, scope: HTTPScope, *, max_content_length: int | None = None, body_timeout: int | None = None, send_push_promise: Callable[[str, Headers], Awaitable[None]], ) -> None: """Create a request object. Arguments: method: The HTTP verb. scheme: The scheme used for the request. path: The full unquoted path of the request. query_string: The raw bytes for the query string part. headers: The request headers. root_path: The root path that should be prepended to all routes. http_version: The HTTP version of the request. body: An awaitable future for the body data i.e. ``data = await body`` max_content_length: The maximum length in bytes of the body (None implies no limit in Quart). body_timeout: The maximum time (seconds) to wait for the body before timing out. send_push_promise: An awaitable to send a push promise based off of this request (HTTP/2 feature). scope: Underlying ASGI scope dictionary. """ super().__init__( method, scheme, path, query_string, headers, root_path, http_version, scope ) self.body_timeout = body_timeout self.body = self.body_class(self.content_length, max_content_length) self._cached_json: dict[bool, Any] = {False: Ellipsis, True: Ellipsis} self._form: MultiDict | None = None self._files: MultiDict | None = None self._parsing_lock = self.lock_class() self._send_push_promise = send_push_promise @property def max_content_length(self) -> int | None: if self._max_content_length is not None: return self._max_content_length if current_app: return current_app.config["MAX_CONTENT_LENGTH"] return None @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: if self._max_form_memory_size is not None: return self._max_form_memory_size if current_app: return current_app.config["MAX_FORM_MEMORY_SIZE"] return None @max_form_memory_size.setter def max_form_memory_size(self, value: int | None) -> None: self._max_form_memory_size = value @property def max_form_parts(self) -> int | None: if self._max_form_parts is not None: return self._max_form_parts if current_app: return current_app.config["MAX_FORM_PARTS"] return None @max_form_parts.setter def max_form_parts(self, value: int | None) -> None: self._max_form_parts = value @property async def stream(self) -> NoReturn: raise NotImplementedError("Use body instead") @property async def data(self) -> bytes: return await self.get_data(as_text=False, parse_form_data=True) # type: ignore @overload async def get_data( self, cache: bool, as_text: Literal[False], parse_form_data: bool ) -> bytes: ... @overload async def get_data( self, cache: bool, as_text: Literal[True], parse_form_data: bool ) -> str: ... @overload async def get_data( self, cache: bool = True, as_text: bool = False, parse_form_data: bool = False ) -> str | bytes: ... async def get_data( self, cache: bool = True, as_text: bool = False, parse_form_data: bool = False ) -> str | bytes: """Get the request body data. Arguments: cache: If False the body data will be cleared, resulting in any subsequent calls returning an empty str | bytes and reducing memory usage. as_text: If True the data is returned as a decoded string, otherwise raw bytes are returned. parse_form_data: Parse the data as form data first, return any remaining data. """ if parse_form_data: await self._load_form_data() try: raw_data = await asyncio.wait_for(self.body, timeout=self.body_timeout) except TimeoutError as e: raise RequestTimeout() from e else: if not cache: self.body.clear() if as_text: return raw_data.decode() else: return raw_data @property async def values(self) -> CombinedMultiDict: sources = [self.args] if self.method != "GET": # Whilst GET requests are allowed to have a body, most # implementations do not allow this hence this # inconsistency may result in confusing values. form = await self.form sources.append(form) multidict_sources: list[MultiDict] = [] for source in sources: if not isinstance(source, MultiDict): multidict_sources.append(MultiDict(source)) else: multidict_sources.append(source) return CombinedMultiDict(multidict_sources) @property async def form(self) -> MultiDict: """The parsed form encoded data. Note file data is present in the :attr:`files`. """ await self._load_form_data() return self._form @property async def files(self) -> MultiDict[str, FileStorage]: """The parsed files. This will return an empty multidict unless the request mimetype was ``enctype="multipart/form-data"`` and the method POST, PUT, or PATCH. """ await self._load_form_data() return self._files def make_form_data_parser(self) -> FormDataParser: return self.form_data_parser_class( max_content_length=self.max_content_length, max_form_memory_size=self.max_form_memory_size, max_form_parts=self.max_form_parts, cls=self.parameter_storage_class, ) async def _load_form_data(self) -> None: async with self._parsing_lock: if self._form is None: parser = self.make_form_data_parser() try: self._form, self._files = await asyncio.wait_for( parser.parse( self.body, self.mimetype, self.content_length, self.mimetype_params, ), timeout=self.body_timeout, ) except TimeoutError as e: raise RequestTimeout() from e @property async def json(self) -> Any: return await self.get_json() async def get_json( self, force: bool = False, silent: bool = False, cache: bool = True ) -> Any: """Parses the body data as JSON and returns it. Arguments: force: Force JSON parsing even if the mimetype is not JSON. silent: Do not trigger error handling if parsing fails, without this the :meth:`on_json_loading_failed` will be called on error. cache: Cache the parsed JSON on this request object. """ if cache and self._cached_json[silent] is not Ellipsis: return self._cached_json[silent] if not (force or self.is_json): return None data = await self.get_data(cache=cache, as_text=True) try: result = self.json_module.loads(data) except ValueError as error: if silent: result = None else: result = self.on_json_loading_failed(error) if cache: self._cached_json[silent] = result return result def on_json_loading_failed(self, error: Exception) -> Any: """Handle a JSON parsing error. Arguments: error: The exception raised during parsing. Returns: Any value returned (if overridden) will be used as the default for any get_json calls. """ if current_app and current_app.debug: raise BadRequest(f"Failed to decode JSON: {error}") raise BadRequest() async def send_push_promise(self, path: str) -> None: headers = Headers() for name in SERVER_PUSH_HEADERS_TO_COPY: for value in self.headers.getlist(name): headers.add(name, value) await self._send_push_promise(path, headers) async def close(self) -> None: for _key, value in iter_multi_items(self._files or ()): value.close() pallets-quart-6b318bd/src/quart/wrappers/response.py000066400000000000000000000402461524460054700227360ustar00rootroot00000000000000from __future__ import annotations from abc import ABC from abc import abstractmethod from collections.abc import AsyncGenerator from collections.abc import AsyncIterable from collections.abc import AsyncIterator from collections.abc import Iterable from hashlib import md5 from io import BytesIO from os import PathLike from types import TracebackType from typing import Any from typing import Literal from typing import overload from typing import TYPE_CHECKING from aiofiles import open as async_open from aiofiles.base import AiofilesContextManager from aiofiles.threadpool.binary import AsyncBufferedIOBase from werkzeug.datastructures import ContentRange from werkzeug.datastructures import Headers from werkzeug.exceptions import RequestedRangeNotSatisfiable from werkzeug.http import parse_etags from werkzeug.sansio.http import is_resource_modified from werkzeug.sansio.response import Response as SansIOResponse from .. import json from ..globals import current_app from ..utils import file_path_to_path from ..utils import run_sync_iterable if TYPE_CHECKING: from .request import Request class ResponseBody(ABC): """Base class wrapper for response body data. This ensures that the following is possible (as Quart assumes so when returning the body to the ASGI server async with wrapper as response: async for data in response: send(data) """ @abstractmethod async def __aenter__(self) -> AsyncIterable: pass @abstractmethod async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: pass def _raise_if_invalid_range(begin: int, end: int, size: int) -> None: if begin >= end or abs(begin) > size: raise RequestedRangeNotSatisfiable() class DataBody(ResponseBody): def __init__(self, data: bytes) -> None: self.data = data self.begin = 0 self.end = len(self.data) async def __aenter__(self) -> DataBody: return self async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: pass def __aiter__(self) -> AsyncIterator[bytes]: return _DataBodyGen(self) async def make_conditional(self, begin: int, end: int | None) -> int: self.begin = begin self.end = len(self.data) if end is None else end self.end = min(len(self.data), self.end) _raise_if_invalid_range(self.begin, self.end, len(self.data)) return len(self.data) class _DataBodyGen(AsyncIterator[bytes]): def __init__(self, data_body: DataBody): self._data_body = data_body self._iterated = False async def __anext__(self) -> bytes: if self._iterated: raise StopAsyncIteration self._iterated = True return self._data_body.data[self._data_body.begin : self._data_body.end] class IterableBody(ResponseBody): def __init__(self, iterable: AsyncIterable[Any] | Iterable[Any]) -> None: self.iter: AsyncIterator[Any] if isinstance(iterable, Iterable): self.iter = run_sync_iterable(iter(iterable)) else: self.iter = iterable.__aiter__() # Can't use aiter() until 3.10 async def __aenter__(self) -> IterableBody: return self async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: if hasattr(self.iter, "aclose"): await self.iter.aclose() def __aiter__(self) -> AsyncIterator[Any]: return self.iter class FileBody(ResponseBody): """Provides an async file accessor with range setting. The :attr:`Response.response` attribute must be async-iterable and yield bytes, which this wrapper does for a file. In addition it allows a range to be set on the file, thereby supporting conditional requests. Attributes: buffer_size: Size in bytes to load per iteration. """ buffer_size = 8192 def __init__( self, file_path: str | PathLike, *, buffer_size: int | None = None ) -> None: self.file_path = file_path_to_path(file_path) self.size = self.file_path.stat().st_size self.begin = 0 self.end = self.size if buffer_size is not None: self.buffer_size = buffer_size self.file: AsyncBufferedIOBase | None = None self.file_manager: AiofilesContextManager = None async def __aenter__(self) -> FileBody: self.file_manager = async_open(self.file_path, mode="rb") self.file = await self.file_manager.__aenter__() await self.file.seek(self.begin) return self async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: await self.file_manager.__aexit__(exc_type, exc_value, tb) def __aiter__(self) -> FileBody: return self async def __anext__(self) -> bytes: current = await self.file.tell() if current >= self.end: raise StopAsyncIteration() read_size = min(self.buffer_size, self.end - current) chunk = await self.file.read(read_size) if chunk: return chunk else: raise StopAsyncIteration() async def make_conditional(self, begin: int, end: int | None) -> int: self.begin = begin self.end = self.size if end is None else end self.end = min(self.size, self.end) _raise_if_invalid_range(self.begin, self.end, self.size) return self.size class IOBody(ResponseBody): """Provides an async file accessor with range setting. The :attr:`Response.response` attribute must be async-iterable and yield bytes, which this wrapper does for a file. In addition it allows a range to be set on the file, thereby supporting conditional requests. Attributes: buffer_size: Size in bytes to load per iteration. """ buffer_size = 8192 def __init__(self, io_stream: BytesIO, *, buffer_size: int | None = None) -> None: self.io_stream = io_stream self.size = io_stream.getbuffer().nbytes self.begin = 0 self.end = self.size if buffer_size is not None: self.buffer_size = buffer_size async def __aenter__(self) -> IOBody: self.io_stream.seek(self.begin) return self async def __aexit__( self, exc_type: type, exc_value: BaseException, tb: TracebackType ) -> None: return None def __aiter__(self) -> IOBody: return self async def __anext__(self) -> bytes: current = self.io_stream.tell() if current >= self.end: raise StopAsyncIteration() read_size = min(self.buffer_size, self.end - current) chunk = self.io_stream.read(read_size) if chunk: return chunk else: raise StopAsyncIteration() async def make_conditional(self, begin: int, end: int | None) -> int: self.begin = begin self.end = self.size if end is None else end self.end = min(self.size, self.end) _raise_if_invalid_range(self.begin, self.end, self.size) return self.size class Response(SansIOResponse): """This class represents a response. It can be subclassed and the subclassed used in preference by replacing the :attr:`~quart.Quart.response_class` with your subclass. Attributes: automatically_set_content_length: If False the content length header must be provided. default_status: The status code to use if not provided. default_mimetype: The mimetype to use if not provided. implicit_sequence_conversion: Implicitly convert the response to a iterable in the get_data method, to allow multiple iterations. """ automatically_set_content_length = True default_mimetype = "text/html" data_body_class = DataBody file_body_class = FileBody implicit_sequence_conversion = True io_body_class = IOBody iterable_body_class = IterableBody json_module = json def __init__( self, response: ResponseBody | str | bytes | Iterable | AsyncIterable | None = None, status: int | None = None, headers: dict | Headers | None = None, mimetype: str | None = None, content_type: str | None = None, ) -> None: """Create a response object. The response itself can be a chunk of data or a iterable/generator of data chunks. The Content-Type can either be specified as a mimetype or content_type header or omitted to use the :attr:`default_mimetype`. Arguments: response: The response data or iterable over the data. status: Status code of the response. headers: Headers to attach to the response. mimetype: Mimetype of the response. content_type: Content-Type header value. Attributes: response: An iterable of the response bytes-data. """ super().__init__(status, headers, mimetype, content_type) self.timeout: Any = Ellipsis self.response: ResponseBody if response is None: self.response = self.iterable_body_class([]) elif isinstance(response, ResponseBody): self.response = response elif isinstance(response, (str, bytes)): self.set_data(response) else: self.response = self.iterable_body_class(response) @property def max_cookie_size(self) -> int: # type: ignore if current_app: return current_app.config["MAX_COOKIE_SIZE"] return super().max_cookie_size @overload async def get_data(self, as_text: Literal[True]) -> str: ... @overload async def get_data(self, as_text: Literal[False]) -> bytes: ... @overload async def get_data(self, as_text: bool = True) -> str | bytes: ... async def get_data(self, as_text: bool = False) -> str | bytes: """Return the body data.""" if self.implicit_sequence_conversion: await self.make_sequence() chunks = [] async with self.response as body: async for data in body: chunks.append(data.decode() if as_text else data) return ("" if as_text else b"").join(chunks) def set_data(self, data: str | bytes) -> None: """Set the response data. This will encode using the :attr:`charset`. """ if isinstance(data, str): bytes_data = data.encode() else: bytes_data = data self.response = self.data_body_class(bytes_data) if self.automatically_set_content_length: self.content_length = len(bytes_data) @property async def data(self) -> bytes: return await self.get_data(as_text=False) @data.setter def data(self, value: bytes) -> None: self.set_data(value) @property async def json(self) -> Any: return await self.get_json() async def get_json(self, force: bool = False, silent: bool = False) -> Any: """Parses the body data as JSON and returns it. Arguments: force: Force JSON parsing even if the mimetype is not JSON. silent: Do not trigger error handling if parsing fails, without this the :meth:`on_json_loading_failed` will be called on error. """ if not (force or self.is_json): return None data = await self.get_data(as_text=True) try: return self.json_module.loads(data) except ValueError: if silent: raise return None def _is_range_request_processable(self, request: Request) -> bool: return ( "If-Range" not in request.headers or not is_resource_modified( http_range=request.headers.get("Range"), http_if_range=request.headers.get("If-Range"), http_if_modified_since=request.headers.get("If-Modified-Since"), http_if_none_match=request.headers.get("If-None-Match"), http_if_match=request.headers.get("If-Match"), etag=self.headers.get("etag"), data=None, last_modified=self.headers.get("last-modified"), ignore_if_range=False, ) ) and "Range" in request.headers async def _process_range_request( self, request: Request, complete_length: int | None = None, accept_ranges: str | None = None, ) -> bool: if ( accept_ranges is None or complete_length is None or complete_length == 0 or not self._is_range_request_processable(request) ): return False request_range = request.range if request_range is None: raise RequestedRangeNotSatisfiable(complete_length) if request_range.units != "bytes" or len(request_range.ranges) > 1: raise RequestedRangeNotSatisfiable() begin, end = request_range.ranges[0] try: complete_length = await self.response.make_conditional(begin, end) # type: ignore except AttributeError: await self.make_sequence() complete_length = await self.response.make_conditional(begin, end) # type: ignore self.content_length = self.response.end - self.response.begin # type: ignore self.headers["Accept-Ranges"] = accept_ranges self.content_range = ContentRange( request_range.units, self.response.begin, # type: ignore self.response.end, # type: ignore complete_length, ) self.status_code = 206 return True async def make_conditional( self, request: Request, accept_ranges: bool | str = False, complete_length: int | None = None, ) -> Response: if request.method in {"GET", "HEAD"}: accept_ranges = _clean_accept_ranges(accept_ranges) is206 = await self._process_range_request( request, complete_length, accept_ranges ) if not is206 and not is_resource_modified( http_range=request.headers.get("Range"), http_if_range=request.headers.get("If-Range"), http_if_modified_since=request.headers.get("If-Modified-Since"), http_if_none_match=request.headers.get("If-None-Match"), http_if_match=request.headers.get("If-Match"), etag=self.headers.get("etag"), data=None, last_modified=self.headers.get("last-modified"), ignore_if_range=True, ): if parse_etags(request.headers.get("If-Match")): self.status_code = 412 else: self.status_code = 304 self.set_data(b"") del self.content_length return self async def make_sequence(self) -> None: data = b"".join([value async for value in self.iter_encode()]) self.response = self.data_body_class(data) async def iter_encode(self) -> AsyncGenerator[bytes]: async with self.response as response_body: async for item in response_body: if isinstance(item, str): yield item.encode() else: yield item async def freeze(self) -> None: """Freeze this object ready for pickling.""" self.set_data(await self.get_data()) async def add_etag(self, overwrite: bool = False, weak: bool = False) -> None: if overwrite or "etag" not in self.headers: self.set_etag(md5(await self.get_data(as_text=False)).hexdigest(), weak) def _set_or_pop_header(self, key: str, value: str) -> None: if value == "": self.headers.pop(key, None) else: self.headers[key] = value def _clean_accept_ranges(accept_ranges: bool | str) -> str: if accept_ranges is True: return "bytes" elif accept_ranges is False: return "none" elif isinstance(accept_ranges, str): return accept_ranges raise ValueError("Invalid accept_ranges value") pallets-quart-6b318bd/src/quart/wrappers/websocket.py000066400000000000000000000101421524460054700230560ustar00rootroot00000000000000from __future__ import annotations import asyncio from collections.abc import AsyncGenerator from collections.abc import Callable from typing import Any from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Headers from .base import BaseRequestWebsocket from .base import ClientDisconnectedError class Buffer: def __init__(self) -> None: self._queue = asyncio.Queue[bytes | str]() async def put(self, data: bytes | str) -> None: await self._queue.put(data) async def get(self) -> bytes | str: try: return await self._queue.get() except asyncio.QueueShutDown: raise ClientDisconnectedError() from None def disconnect(self) -> None: self._queue.shutdown() async def __anext__(self) -> AsyncGenerator[bytes | str]: while True: yield await self.get() class Websocket(BaseRequestWebsocket): """This class represents a websocket. It can be subclassed and the subclassed used in preference by replacing the :attr:`~quart.Quart.websocket_class` with your subclass. Attributes: buffer_class: The class to process the incoming messages. """ buffer_class = Buffer def __init__( self, path: str, query_string: bytes, scheme: str, headers: Headers, root_path: str, http_version: str, subprotocols: list[str], send: Callable, accept: Callable, close: Callable, scope: WebsocketScope, ) -> None: """Create a request object. Arguments: path: The full unquoted path of the request. query_string: The raw bytes for the query string part. scheme: The scheme used for the request. headers: The request headers. root_path: The root path that should be prepended to all routes. http_version: The HTTP version of the request. subprotocols: The subprotocols requested. receive: Returns an awaitable of the current data accept: Idempotent callable to accept the websocket connection. scope: Underlying ASGI scope dictionary. """ super().__init__( "GET", scheme, path, query_string, headers, root_path, http_version, scope ) self.buffer = self.buffer_class() self._accept = accept self._close = close self._send = send self._subprotocols = subprotocols @property def requested_subprotocols(self) -> list[str]: return self._subprotocols async def receive(self) -> str | bytes: await self.accept() return await self.buffer.get() async def send(self, data: str | bytes) -> None: # Must allow for the event loop to act if the user has say # setup a tight loop sending data over a websocket (as in the # example). So yield via the sleep. await asyncio.sleep(0) await self.accept() await self._send(data) async def receive_json(self) -> Any: data = await self.receive() return self.json_module.loads(data) async def send_json(self, *args: Any, **kwargs: Any) -> None: if args and kwargs: raise TypeError("Behavior undefined when passed both args and kwargs") elif len(args) == 1: data = args[0] else: data = args or kwargs raw = self.json_module.dumps(data) await self.send(raw) async def accept( self, headers: dict | Headers | None = None, subprotocol: str | None = None ) -> None: """Manually chose to accept the websocket connection. Arguments: headers: Additional headers to send with the acceptance response. subprotocol: The chosen subprotocol, optional. """ if headers is None: headers_ = Headers() else: headers_ = Headers(headers) await self._accept(headers_, subprotocol) async def close(self, code: int, reason: str = "") -> None: await self._close(code, reason) pallets-quart-6b318bd/tests/000077500000000000000000000000001524460054700160745ustar00rootroot00000000000000pallets-quart-6b318bd/tests/assets/000077500000000000000000000000001524460054700173765ustar00rootroot00000000000000pallets-quart-6b318bd/tests/assets/config.cfg000066400000000000000000000000211524460054700213150ustar00rootroot00000000000000FOO=bar BOB=jeff pallets-quart-6b318bd/tests/conftest.py000066400000000000000000000025601524460054700202760ustar00rootroot00000000000000from __future__ import annotations import pytest from hypercorn.typing import HTTPScope from hypercorn.typing import WebsocketScope @pytest.fixture(name="http_scope") def _http_scope() -> HTTPScope: return { "type": "http", "asgi": {}, "http_version": "1.1", "method": "GET", "scheme": "https", "path": "/", "raw_path": b"/", "query_string": b"a=b", "root_path": "", "headers": [ (b"User-Agent", b"Hypercorn"), (b"X-Hypercorn", b"Hypercorn"), (b"Referer", b"hypercorn"), ], "client": ("127.0.0.1", 80), "server": None, "state": {}, # type: ignore[typeddict-item] "extensions": {}, } @pytest.fixture(name="websocket_scope") def _websocket_scope() -> WebsocketScope: return { "type": "websocket", "asgi": {}, "http_version": "1.1", "scheme": "https", "path": "/", "raw_path": b"/", "query_string": b"a=b", "root_path": "", "headers": [ (b"User-Agent", b"Hypercorn"), (b"X-Hypercorn", b"Hypercorn"), (b"Referer", b"hypercorn"), ], "client": ("127.0.0.1", 80), "server": None, "subprotocols": [], "state": {}, # type: ignore[typeddict-item] "extensions": {}, } pallets-quart-6b318bd/tests/test_app.py000066400000000000000000000302331524460054700202660ustar00rootroot00000000000000from __future__ import annotations import asyncio from collections.abc import AsyncGenerator from typing import NoReturn from unittest.mock import AsyncMock import pytest from hypercorn.typing import HTTPScope from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Headers from werkzeug.exceptions import InternalServerError from werkzeug.wrappers import Response as WerkzeugResponse from quart.app import Quart from quart.globals import session from quart.globals import websocket from quart.sessions import SecureCookieSession from quart.sessions import SessionInterface from quart.testing import no_op_push from quart.testing import WebsocketResponseError from quart.typing import ResponseReturnValue from quart.typing import ResponseTypes from quart.wrappers import Request from quart.wrappers import Response TEST_RESPONSE = Response("") class SimpleError(Exception): pass def test_endpoint_overwrite() -> None: app = Quart(__name__) def route() -> str: return "" def route2() -> str: return "" async def route3() -> str: return "" app.add_url_rule("/a", "index", route, methods=["GET"]) app.add_url_rule( "/a/a", "index", route, methods=["GET"] ) # Should not assert, as same view func with pytest.raises(AssertionError): app.add_url_rule("/a/b", "index", route2, methods=["GET"]) app.add_url_rule("/b", "async", route3, methods=["GET"]) app.add_url_rule( "/b/a", "async", route3, methods=["GET"] ) # Should not assert, as same view func with pytest.raises(AssertionError): app.add_url_rule("/b/b", "async", route2, methods=["GET"]) @pytest.mark.parametrize( "methods, required_methods, automatic_options", [ ({}, {}, False), ({}, {}, True), ({"GET", "PUT"}, {}, False), ({"GET", "PUT"}, {}, True), ({}, {"GET", "PUT"}, False), ({}, {"GET", "PUT"}, True), ], ) def test_add_url_rule_methods( methods: set[str], required_methods: set[str], automatic_options: bool ) -> None: app = Quart(__name__) def route() -> str: return "" route.methods = methods # type: ignore route.required_methods = required_methods # type: ignore non_func_methods = {"PATCH"} if not methods else None app.add_url_rule( "/", "end", route, methods=non_func_methods, provide_automatic_options=automatic_options, ) result = {"PATCH"} if not methods else set() result.update(methods) result.update(required_methods) if "GET" in result: result.add("HEAD") assert app.url_map._rules_by_endpoint["end"][0].methods == result @pytest.mark.parametrize( "methods, arg_automatic, func_automatic, expected_methods, expected_automatic", [ ({"GET"}, True, None, {"HEAD", "GET"}, True), ({"GET"}, None, None, {"HEAD", "GET", "OPTIONS"}, True), ({"GET"}, None, True, {"HEAD", "GET"}, True), ({"GET", "OPTIONS"}, None, None, {"HEAD", "GET", "OPTIONS"}, False), ({"GET"}, False, True, {"HEAD", "GET"}, False), ({"GET"}, None, False, {"HEAD", "GET"}, False), ], ) def test_add_url_rule_automatic_options( methods: set[str], arg_automatic: bool | None, func_automatic: bool | None, expected_methods: set[str], expected_automatic: bool, ) -> None: app = Quart(__name__) def route() -> str: return "" route.provide_automatic_options = func_automatic # type: ignore app.add_url_rule( "/", "end", route, methods=methods, provide_automatic_options=arg_automatic ) assert app.url_map._rules_by_endpoint["end"][0].methods == expected_methods assert ( app.url_map._rules_by_endpoint["end"][0].provide_automatic_options # type: ignore == expected_automatic ) async def test_host_matching() -> None: app = Quart(__name__, static_host="quart.com", host_matching=True) @app.route("/", host="quart.com") async def route() -> str: return "" test_client = app.test_client() response = await test_client.get("/", headers={"host": "quart.com"}) assert response.status_code == 200 response = await test_client.get("/", headers={"host": "localhost"}) assert response.status_code == 404 async def test_subdomain() -> None: app = Quart(__name__, subdomain_matching=True) app.config["SERVER_NAME"] = "quart.com" @app.route("/", subdomain="") async def route(subdomain: str) -> str: return subdomain test_client = app.test_client() response = await test_client.get("/", headers={"host": "sub.quart.com"}) assert (await response.get_data(as_text=True)) == "sub" @pytest.mark.parametrize( "result, expected, raises", [ (None, None, True), ((None, 201), None, True), (TEST_RESPONSE, TEST_RESPONSE, False), ( ("hello", {"X-Header": "bob"}), Response("hello", headers={"X-Header": "bob"}), False, ), (("hello", 201), Response("hello", 201), False), ( ("hello", 201, {"X-Header": "bob"}), Response("hello", 201, headers={"X-Header": "bob"}), False, ), ( (WerkzeugResponse("hello"), 201, {"X-Header": "bob"}), WerkzeugResponse("hello", 201, {"X-Header": "bob"}), False, ), (InternalServerError(), InternalServerError().get_response(), False), ((val for val in "abcd"), Response(val for val in "abcd"), False), (int, None, True), ], ) async def test_make_response( result: ResponseReturnValue, expected: Response | WerkzeugResponse, raises: bool ) -> None: app = Quart(__name__) app.config["RESPONSE_TIMEOUT"] = None try: response = await app.make_response(result) except TypeError: if not raises: raise else: assert set(response.headers.keys()) == set(expected.headers.keys()) assert response.status_code == expected.status_code if isinstance(response, Response): assert (await response.get_data()) == (await expected.get_data()) # type: ignore elif isinstance(response, WerkzeugResponse): assert response.get_data() == expected.get_data() @pytest.fixture(name="basic_app") def _basic_app() -> Quart: app = Quart(__name__) @app.route("/") def route() -> str: return "" @app.route("/exception/") def exception() -> str: raise Exception() return app async def test_app_route_exception(basic_app: Quart) -> None: test_client = basic_app.test_client() response = await test_client.get("/exception/") assert response.status_code == 500 async def test_app_before_request_exception(basic_app: Quart) -> None: @basic_app.before_request def before() -> None: raise Exception() test_client = basic_app.test_client() response = await test_client.get("/") assert response.status_code == 500 async def test_app_after_request_exception(basic_app: Quart) -> None: @basic_app.after_request def after(_: ResponseTypes) -> None: raise Exception() test_client = basic_app.test_client() response = await test_client.get("/") assert response.status_code == 500 async def test_app_after_request_handler_exception(basic_app: Quart) -> None: @basic_app.after_request def after(_: ResponseTypes) -> None: raise Exception() test_client = basic_app.test_client() response = await test_client.get("/exception/") assert response.status_code == 500 async def test_app_handle_request_asyncio_cancelled_error( http_scope: HTTPScope, ) -> None: app = Quart(__name__) @app.route("/") async def index() -> NoReturn: raise asyncio.CancelledError() request = app.request_class( "GET", "http", "/", b"", Headers([("host", "quart.com")]), "", "1.1", http_scope, send_push_promise=no_op_push, ) with pytest.raises(asyncio.CancelledError): await app.handle_request(request) async def test_app_handle_websocket_asyncio_cancelled_error( websocket_scope: WebsocketScope, ) -> None: app = Quart(__name__) @app.websocket("/") async def index() -> NoReturn: raise asyncio.CancelledError() websocket = app.websocket_class( "/", b"", "wss", Headers([("host", "quart.com")]), "", "1.1", None, None, None, None, websocket_scope, ) with pytest.raises(asyncio.CancelledError): await app.handle_websocket(websocket) @pytest.fixture(name="session_app", scope="function") def _session_app() -> Quart: app = Quart(__name__) app.session_interface = AsyncMock(spec=SessionInterface) app.session_interface.open_session.return_value = SecureCookieSession() app.session_interface.is_null_session.return_value = False @app.route("/") async def route() -> str: session["a"] = "b" return "" @app.websocket("/ws/") async def ws() -> None: session["a"] = "b" await websocket.accept() await websocket.send("") @app.websocket("/ws_return/") async def ws_return() -> str: session["a"] = "b" return "" return app async def test_app_session(session_app: Quart) -> None: test_client = session_app.test_client() await test_client.get("/") session_app.session_interface.open_session.assert_called() # type: ignore session_app.session_interface.save_session.assert_called() # type: ignore async def test_app_session_websocket(session_app: Quart) -> None: test_client = session_app.test_client() async with test_client.websocket("/ws/") as test_websocket: await test_websocket.receive() session_app.session_interface.open_session.assert_called() # type: ignore session_app.session_interface.save_session.assert_called() # type: ignore async def test_app_session_websocket_return(session_app: Quart) -> None: test_client = session_app.test_client() with pytest.raises(WebsocketResponseError): async with test_client.websocket("/ws_return/") as test_websocket: await test_websocket.receive() session_app.session_interface.open_session.assert_called() # type: ignore session_app.session_interface.save_session.assert_called() # type: ignore @pytest.mark.parametrize( "debug, testing, raises", [ (False, False, False), (True, False, True), (False, True, True), (True, True, True), ], ) async def test_propagation( debug: bool, testing: bool, raises: bool, http_scope: HTTPScope ) -> None: app = Quart(__name__) @app.route("/") async def exception() -> ResponseReturnValue: raise SimpleError() app.debug = debug app.testing = testing test_client = app.test_client() if raises: with pytest.raises(SimpleError): await app.handle_request( Request( "GET", "http", "/", b"", Headers(), "", "1.1", http_scope, send_push_promise=no_op_push, ) ) else: response = await test_client.get("/") assert response.status_code == 500 async def test_test_app() -> None: startup = False shutdown = False serving = [] app = Quart(__name__) @app.before_serving async def before() -> None: nonlocal startup startup = True @app.after_serving async def after() -> None: nonlocal shutdown shutdown = True @app.while_serving async def lifespan() -> AsyncGenerator[None]: nonlocal serving serving.append(1) yield serving.append(2) @app.route("/") async def index() -> str: return "" async with app.test_app() as test_app: assert startup test_client = test_app.test_client() await test_client.get("/") assert not shutdown assert serving == [1] assert shutdown assert serving == [1, 2] pallets-quart-6b318bd/tests/test_asgi.py000066400000000000000000000254151524460054700204370ustar00rootroot00000000000000from __future__ import annotations import asyncio from unittest.mock import AsyncMock from unittest.mock import Mock import pytest from hypercorn.typing import ASGIReceiveEvent from hypercorn.typing import ASGISendEvent from hypercorn.typing import HTTPScope from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Headers from quart import Quart from quart.asgi import _convert_version from quart.asgi import _handle_exception from quart.asgi import ASGIHTTPConnection from quart.asgi import ASGIWebsocketConnection from quart.utils import encode_headers @pytest.mark.parametrize( "headers, expected", [([(b"host", b"quart")], "quart"), ([], "")] ) async def test_http_1_0_host_header(headers: list, expected: str) -> None: app = Quart(__name__) scope: HTTPScope = { "type": "http", "asgi": {}, "http_version": "1.0", "method": "GET", "scheme": "https", "path": "/", "raw_path": b"/", "query_string": b"", "root_path": "", "headers": headers, "client": ("127.0.0.1", 80), "server": None, "extensions": {}, "state": {}, # type: ignore[typeddict-item] } connection = ASGIHTTPConnection(app, scope) request = connection._create_request_from_scope(lambda: None) # type: ignore assert request.headers["host"] == expected async def test_http_completion() -> None: # Ensure that the connecion callable returns on completion app = Quart(__name__) scope: HTTPScope = { "type": "http", "asgi": {}, "http_version": "1.1", "method": "GET", "scheme": "https", "path": "/", "raw_path": b"/", "query_string": b"", "root_path": "", "headers": [(b"host", b"quart")], "client": ("127.0.0.1", 80), "server": None, "extensions": {}, "state": {}, # type: ignore[typeddict-item] } connection = ASGIHTTPConnection(app, scope) queue: asyncio.Queue = asyncio.Queue() queue.put_nowait({"type": "http.request", "body": b"", "more_body": False}) async def receive() -> ASGIReceiveEvent: # This will block after returning the first and only entry return await queue.get() async def send(message: ASGISendEvent) -> None: queue.put_nowait({"type": "http.disconnect"}) # This test fails if a timeout error is raised here await asyncio.wait_for(connection(receive, send), timeout=1) @pytest.mark.parametrize( "request_message", [ {"type": "http.request", "body": b"", "more_body": False}, {"type": "http.request", "more_body": False}, ], ) async def test_http_request_without_body(request_message: dict) -> None: app = Quart(__name__) scope: HTTPScope = { "type": "http", "asgi": {}, "http_version": "1.0", "method": "GET", "scheme": "https", "path": "/", "raw_path": b"/", "query_string": b"", "root_path": "", "headers": [(b"host", b"quart")], "client": ("127.0.0.1", 80), "server": None, "extensions": {}, "state": {}, # type: ignore[typeddict-item] } connection = ASGIHTTPConnection(app, scope) request = connection._create_request_from_scope(lambda: None) # type: ignore queue: asyncio.Queue = asyncio.Queue() queue.put_nowait(request_message) async def receive() -> ASGIReceiveEvent: # This will block after returning the first and only entry return await queue.get() body = None async def _handle_request() -> None: nonlocal body body = await asyncio.wait_for(request.body, timeout=1) await queue.put({"type": "http.disconnect"}) # This test fails with a timeout error if the request body is not received # within 1 second async with asyncio.TaskGroup() as task_group: request_task = task_group.create_task(_handle_request()) task_group.create_task( connection.handle_messages(request, receive, request_task) ) assert body == b"" async def test_websocket_completion() -> None: # Ensure that the connecion callable returns on completion app = Quart(__name__) scope: WebsocketScope = { "type": "websocket", "asgi": {}, "http_version": "1.1", "scheme": "wss", "path": "/", "raw_path": b"/", "query_string": b"", "root_path": "", "headers": [(b"host", b"quart")], "client": ("127.0.0.1", 80), "server": None, "subprotocols": [], "extensions": {"websocket.http.response": {}}, "state": {}, # type: ignore[typeddict-item] } connection = ASGIWebsocketConnection(app, scope) queue: asyncio.Queue = asyncio.Queue() queue.put_nowait({"type": "websocket.connect"}) async def receive() -> ASGIReceiveEvent: # This will block after returning the first and only entry return await queue.get() async def send(message: ASGISendEvent) -> None: queue.put_nowait({"type": "websocket.disconnect"}) # This test fails if a timeout error is raised here await asyncio.wait_for(connection(receive, send), timeout=1) def test_http_path_from_absolute_target() -> None: app = Quart(__name__) scope: HTTPScope = { "type": "http", "asgi": {}, "http_version": "1.1", "method": "GET", "scheme": "https", "path": "http://quart/path", "raw_path": b"/", "query_string": b"", "root_path": "", "headers": [(b"host", b"quart")], "client": ("127.0.0.1", 80), "server": None, "extensions": {}, "state": {}, # type: ignore[typeddict-item] } connection = ASGIHTTPConnection(app, scope) request = connection._create_request_from_scope(lambda: None) # type: ignore assert request.path == "/path" @pytest.mark.parametrize( "path, expected", [("/app", "/ "), ("/", "/ "), ("/app/", "/"), ("/app/2", "/2"), ("/x/app/2", "/ ")], ) def test_http_path_with_root_path(path: str, expected: str) -> None: app = Quart(__name__) scope: HTTPScope = { "type": "http", "asgi": {}, "http_version": "1.1", "method": "GET", "scheme": "https", "path": path, "raw_path": b"/", "query_string": b"", "root_path": "/app", "headers": [(b"host", b"quart")], "client": ("127.0.0.1", 80), "server": None, "extensions": {}, "state": {}, # type: ignore[typeddict-item] } connection = ASGIHTTPConnection(app, scope) request = connection._create_request_from_scope(lambda: None) # type: ignore assert request.path == expected def test_websocket_path_from_absolute_target() -> None: app = Quart(__name__) scope: WebsocketScope = { "type": "websocket", "asgi": {}, "http_version": "1.1", "scheme": "wss", "path": "ws://quart/path", "raw_path": b"/", "query_string": b"", "root_path": "", "headers": [(b"host", b"quart")], "client": ("127.0.0.1", 80), "server": None, "subprotocols": [], "extensions": {"websocket.http.response": {}}, "state": {}, # type: ignore[typeddict-item] } connection = ASGIWebsocketConnection(app, scope) websocket = connection._create_websocket_from_scope(lambda: None) # type: ignore assert websocket.path == "/path" @pytest.mark.parametrize( "path, expected", [("/app", "/ "), ("/", "/ "), ("/app/", "/"), ("/app/2", "/2"), ("/x/app/2", "/ ")], ) def test_websocket_path_with_root_path(path: str, expected: str) -> None: app = Quart(__name__) scope: WebsocketScope = { "type": "websocket", "asgi": {}, "http_version": "1.1", "scheme": "wss", "path": path, "raw_path": b"/", "query_string": b"", "root_path": "/app", "headers": [(b"host", b"quart")], "client": ("127.0.0.1", 80), "server": None, "subprotocols": [], "extensions": {"websocket.http.response": {}}, "state": {}, # type: ignore[typeddict-item] } connection = ASGIWebsocketConnection(app, scope) websocket = connection._create_websocket_from_scope(lambda: None) # type: ignore assert websocket.path == expected @pytest.mark.parametrize( "scope, headers, subprotocol, has_headers", [ ({}, Headers(), None, False), ({}, Headers(), "abc", False), ({"asgi": {"spec_version": "2.1"}}, Headers({"a": "b"}), None, True), ({"asgi": {"spec_version": "2.1.1"}}, Headers({"a": "b"}), None, True), ], ) async def test_websocket_accept_connection( scope: dict, headers: Headers, subprotocol: str | None, has_headers: bool ) -> None: connection = ASGIWebsocketConnection(Quart(__name__), scope) # type: ignore mock_send = AsyncMock() await connection.accept_connection(mock_send, headers, subprotocol) if has_headers: mock_send.assert_called_with( { "subprotocol": subprotocol, "type": "websocket.accept", "headers": encode_headers(headers), } ) else: mock_send.assert_called_with( {"headers": [], "subprotocol": subprotocol, "type": "websocket.accept"} ) async def test_websocket_accept_connection_warns( websocket_scope: WebsocketScope, ) -> None: connection = ASGIWebsocketConnection(Quart(__name__), websocket_scope) async def mock_send(message: ASGISendEvent) -> None: pass with pytest.warns(UserWarning): await connection.accept_connection(mock_send, Headers({"a": "b"}), None) def test__convert_version() -> None: assert _convert_version("2.1") == [2, 1] def test_http_asgi_scope_from_request() -> None: app = Quart(__name__) scope = { "headers": [(b"host", b"quart")], "http_version": "1.0", "method": "GET", "scheme": "https", "path": "/", "query_string": b"", "test_result": "PASSED", } connection = ASGIHTTPConnection(app, scope) # type: ignore request = connection._create_request_from_scope(lambda: None) # type: ignore assert request.scope["test_result"] == "PASSED" # type: ignore @pytest.mark.parametrize( "debug, testing, raises", [ (True, False, False), (True, True, False), (False, True, False), (False, False, True), ], ) async def test__handle_exception(debug: bool, testing: bool, raises: bool) -> None: app = Mock() app.config = {} app.debug = debug app.testing = testing if raises: with pytest.raises(ValueError): await _handle_exception(app, ValueError()) else: await _handle_exception(app, ValueError()) pallets-quart-6b318bd/tests/test_background_tasks.py000066400000000000000000000030101524460054700230230ustar00rootroot00000000000000from __future__ import annotations import asyncio import time from quart import current_app from quart import Quart async def test_background_task() -> None: app = Quart(__name__) app.config["DATA"] = "data" data = None async def background() -> None: nonlocal data await asyncio.sleep(0.5) data = current_app.config["DATA"] @app.route("/") async def index() -> str: app.add_background_task(background) return "" async with app.test_app(): test_client = app.test_client() await test_client.get("/") assert data == "data" async def test_lifespan_background_task() -> None: app = Quart(__name__) app.config["DATA"] = "data" data = None async def background() -> None: nonlocal data await asyncio.sleep(0.5) data = current_app.config["DATA"] @app.before_serving async def startup() -> None: app.add_background_task(background) async with app.test_app(): pass assert data == "data" async def test_sync_background_task() -> None: app = Quart(__name__) app.config["DATA"] = "data" data = None def background() -> None: nonlocal data time.sleep(0.5) data = current_app.config["DATA"] @app.route("/") async def index() -> str: app.add_background_task(background) return "" async with app.test_app(): test_client = app.test_client() await test_client.get("/") assert data == "data" pallets-quart-6b318bd/tests/test_basic.py000066400000000000000000000167321524460054700205770ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncGenerator from typing import cast import pytest from werkzeug.wrappers import Response as WerkzeugResponse from quart import abort from quart import jsonify from quart import Quart from quart import request from quart import Response from quart import ResponseReturnValue from quart import url_for from quart import websocket from quart.testing import WebsocketResponseError @pytest.fixture def app() -> Quart: app = Quart(__name__) @app.route("/") async def index() -> ResponseReturnValue: return "index" @app.route("/��") async def iri() -> ResponseReturnValue: return "💔" @app.route("/sync/") def sync() -> ResponseReturnValue: return "index" @app.route("/json/", methods=["POST"]) async def json() -> ResponseReturnValue: data = await request.get_json() return jsonify(data) @app.route("/implicit_json/", methods=["POST"]) async def implicit_json() -> ResponseReturnValue: data = await request.get_json() return data @app.route("/werkzeug/") async def werkzeug() -> ResponseReturnValue: return WerkzeugResponse(b"Hello") @app.route("/error/") async def error() -> ResponseReturnValue: abort(409) return "OK" @app.route("/param/") async def param(value: str) -> ResponseReturnValue: return value @app.route("/stream") async def stream() -> ResponseReturnValue: async def _gen() -> AsyncGenerator[str]: yield "Hello " yield "World" return _gen() # type: ignore @app.errorhandler(409) async def generic_http_handler(_: Exception) -> ResponseReturnValue: return "Something Unique", 409 @app.errorhandler(404) async def not_found_handler(_: Exception) -> ResponseReturnValue: return "Not Found", 404 @app.websocket("/ws/") async def ws() -> None: # async for message in websocket: while True: message = await websocket.receive() await websocket.send(message) @app.websocket("/ws/abort/") async def ws_abort() -> None: abort(401) return app @pytest.mark.parametrize("path", ["/", "/sync/"]) async def test_index(path: str, app: Quart) -> None: test_client = app.test_client() response = await test_client.get(path) assert response.status_code == 200 assert b"index" in (await response.get_data()) async def test_iri(app: Quart) -> None: test_client = app.test_client() response = await test_client.get("/��") assert "💔".encode() in (await response.get_data()) async def test_options(app: Quart) -> None: test_client = app.test_client() response = await test_client.options("/") assert response.status_code == 200 assert {method.strip() for method in response.headers["Allow"].split(",")} == { "HEAD", "OPTIONS", "GET", } async def test_json(app: Quart) -> None: test_client = app.test_client() response = await test_client.post("/json/", json={"value": "json"}) assert response.status_code == 200 assert b'{"value":"json"}\n' == (await response.get_data()) async def test_implicit_json(app: Quart) -> None: test_client = app.test_client() response = await test_client.post("/implicit_json/", json={"value": "json"}) assert response.status_code == 200 assert b'{"value":"json"}\n' == (await response.get_data()) async def test_implicit_json_list(app: Quart) -> None: test_client = app.test_client() response = await test_client.post("/implicit_json/", json=["a", 2]) assert response.status_code == 200 assert b'["a",2]\n' == (await response.get_data()) async def test_werkzeug(app: Quart) -> None: test_client = app.test_client() response = await test_client.get("/werkzeug/") assert response.status_code == 200 assert b"Hello" == (await response.get_data()) async def test_generic_error(app: Quart) -> None: test_client = app.test_client() response = await test_client.get("/error/") assert response.status_code == 409 assert b"Something Unique" in (await response.get_data()) async def test_url_defaults(app: Quart) -> None: @app.url_defaults def defaults(_: str, values: dict) -> None: values["value"] = "hello" async with app.test_request_context("/"): assert url_for("param") == "/param/hello" async def test_not_found_error(app: Quart) -> None: test_client = app.test_client() response = await test_client.get("/not_found/") assert response.status_code == 404 assert b"Not Found" in (await response.get_data()) async def test_make_response_str(app: Quart) -> None: response = await app.make_response("Result") assert response.status_code == 200 assert (await response.get_data()) == b"Result" # type: ignore response = await app.make_response(("Result", 200)) response = await app.make_response(("Result", {"name": "value"})) assert response.status_code == 200 assert (await response.get_data()) == b"Result" # type: ignore assert response.headers["name"] == "value" response = await app.make_response(("Result", 404, {"name": "value"})) assert response.status_code == 404 assert (await response.get_data()) == b"Result" # type: ignore assert response.headers["name"] == "value" async def test_make_response_response(app: Quart) -> None: response = await app.make_response(Response("Result")) assert response.status_code == 200 assert (await response.get_data()) == b"Result" # type: ignore response = await app.make_response((Response("Result"), {"name": "value"})) assert response.status_code == 200 assert (await response.get_data()) == b"Result" # type: ignore assert response.headers["name"] == "value" response = await app.make_response((Response("Result"), 404, {"name": "value"})) assert response.status_code == 404 assert (await response.get_data()) == b"Result" # type: ignore assert response.headers["name"] == "value" async def test_make_response_errors(app: Quart) -> None: with pytest.raises(TypeError): await app.make_response(("Result", {"name": "value"}, 200)) # type: ignore with pytest.raises(TypeError): await app.make_response(("Result", {"name": "value"}, 200, "a")) # type: ignore with pytest.raises(TypeError): await app.make_response(("Result",)) # type: ignore async def test_websocket(app: Quart) -> None: test_client = app.test_client() data = b"bob" async with test_client.websocket("/ws/") as test_websocket: await test_websocket.send(data) result = await test_websocket.receive() assert cast(bytes, result) == data async def test_websocket_abort(app: Quart) -> None: test_client = app.test_client() try: async with test_client.websocket("/ws/abort/") as test_websocket: await test_websocket.receive() except WebsocketResponseError as error: assert error.response.status_code == 401 async def test_root_path(app: Quart) -> None: test_client = app.test_client() response = await test_client.get("/", root_path="/bob") assert response.status_code == 404 response = await test_client.get("/bob/", root_path="/bob") assert response.status_code == 200 async def test_stream(app: Quart) -> None: test_client = app.test_client() response = await test_client.get("/stream") assert (await response.get_data()) == b"Hello World" pallets-quart-6b318bd/tests/test_blueprints.py000066400000000000000000000326531524460054700217050ustar00rootroot00000000000000from __future__ import annotations from typing import cast import click import pytest from quart import abort from quart import Blueprint from quart import g from quart import Quart from quart import render_template_string from quart import request from quart import ResponseReturnValue from quart import websocket from quart.views import MethodView async def test_blueprint_route() -> None: app = Quart(__name__) blueprint = Blueprint("blueprint", __name__) @blueprint.route("/page/") async def route() -> ResponseReturnValue: return "OK" app.register_blueprint(blueprint) async with app.test_request_context("/page/"): assert request.blueprint == "blueprint" async def test_blueprint_websocket() -> None: app = Quart(__name__) blueprint = Blueprint("blueprint", __name__) @blueprint.websocket("/ws/") async def ws() -> None: while True: await websocket.send(websocket.blueprint.encode()) app.register_blueprint(blueprint) test_client = app.test_client() async with test_client.websocket("/ws/") as test_websocket: result = await test_websocket.receive() assert cast(bytes, result) == b"blueprint" async def test_blueprint_url_prefix() -> None: app = Quart(__name__) blueprint = Blueprint("blueprint", __name__) prefix = Blueprint("prefix", __name__, url_prefix="/prefix") @app.route("/page/") @blueprint.route("/page/") @prefix.route("/page/") async def route() -> ResponseReturnValue: return "OK" app.register_blueprint(blueprint, url_prefix="/blueprint") app.register_blueprint(prefix) async with app.test_request_context("/page/"): assert request.blueprint is None async with app.test_request_context("/prefix/page/"): assert request.blueprint == "prefix" async with app.test_request_context("/blueprint/page/"): assert request.blueprint == "blueprint" async def test_empty_path_with_url_prefix() -> None: app = Quart(__name__) prefix = Blueprint("prefix", __name__, url_prefix="/prefix") @prefix.route("") async def empty_path_route() -> ResponseReturnValue: return "OK" app.register_blueprint(prefix) test_client = app.test_client() response = await test_client.get("/prefix") assert response.status_code == 200 assert await response.get_data() == b"OK" async def test_blueprint_template_filter() -> None: app = Quart(__name__) blueprint = Blueprint("blueprint", __name__) @blueprint.app_template_filter() def reverse(value: str) -> str: return value[::-1] @blueprint.route("/") async def route() -> ResponseReturnValue: return await render_template_string("{{ name|reverse }}", name="hello") app.register_blueprint(blueprint) response = await app.test_client().get("/") assert b"olleh" in (await response.get_data()) async def test_blueprint_error_handler() -> None: app = Quart(__name__) blueprint = Blueprint("blueprint", __name__) @blueprint.route("/error/") async def error() -> ResponseReturnValue: abort(409) return "OK" @blueprint.errorhandler(409) async def handler(_: Exception) -> ResponseReturnValue: return "Something Unique", 409 app.register_blueprint(blueprint) response = await app.test_client().get("/error/") assert response.status_code == 409 assert b"Something Unique" in (await response.get_data()) async def test_blueprint_method_view() -> None: app = Quart(__name__) blueprint = Blueprint("blueprint", __name__) class Views(MethodView): async def get(self) -> ResponseReturnValue: return "GET" async def post(self) -> ResponseReturnValue: return "POST" blueprint.add_url_rule("/", view_func=Views.as_view("simple")) app.register_blueprint(blueprint) test_client = app.test_client() response = await test_client.get("/") assert "GET" == (await response.get_data(as_text=True)) response = await test_client.post("/") assert "POST" == (await response.get_data(as_text=True)) @pytest.mark.parametrize( "cli_group, args", [ ("named", ["named", "cmd"]), (None, ["cmd"]), ], ) def test_cli_blueprints(cli_group: str | None, args: list[str]) -> None: app = Quart(__name__) blueprint = Blueprint("blueprint", __name__, cli_group=cli_group) @blueprint.cli.command("cmd") def command() -> None: click.echo("command") app.register_blueprint(blueprint) app_runner = app.test_cli_runner() result = app_runner.invoke(args=args) assert "command" in result.output @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"), ], ) async def test_nesting_url_prefixes( parent_init: str | None, child_init: str | None, parent_registration: str | None, child_registration: str | None, ) -> None: app = Quart(__name__) parent = Blueprint("parent", __name__, url_prefix=parent_init) child = Blueprint("child", __name__, url_prefix=child_init) @child.route("/") def index() -> ResponseReturnValue: return "index" parent.register_blueprint(child, url_prefix=child_registration) app.register_blueprint(parent, url_prefix=parent_registration) test_client = app.test_client() response = await test_client.get("/parent/child/") assert response.status_code == 200 @pytest.mark.parametrize( "parent_subdomain, child_subdomain, expected_subdomain", [ (None, None, None), ("parent", None, "parent"), (None, "child", "child"), ("parent", "child", "child.parent"), ], ) async def test_nesting_subdomains( parent_subdomain: str | None, child_subdomain: str | None, expected_subdomain: str | None, ) -> None: app = Quart(__name__) domain_name = "domain.tld" app.config["SERVER_NAME"] = domain_name parent = Blueprint("parent", __name__, subdomain=parent_subdomain) child = Blueprint("child", __name__, subdomain=child_subdomain) @child.route("/") def index() -> ResponseReturnValue: return "index" parent.register_blueprint(child) app.register_blueprint(parent) test_client = app.test_client() response = await test_client.get("/", subdomain=expected_subdomain) assert response.status_code == 200 async def test_nesting_and_sibling() -> None: app = Quart(__name__) parent = Blueprint("parent", __name__, url_prefix="/parent") child = Blueprint("child", __name__, url_prefix="/child") @child.route("/") def index() -> ResponseReturnValue: return "index" parent.register_blueprint(child) app.register_blueprint(parent) app.register_blueprint(child, url_prefix="/sibling") test_client = app.test_client() response = await test_client.get("/parent/child/") assert response.status_code == 200 response = await test_client.get("/sibling/") assert response.status_code == 200 def test_unique_blueprint_names() -> None: app = Quart(__name__) bp = Blueprint("bp", __name__) bp2 = Blueprint("bp", __name__) app.register_blueprint(bp) with pytest.raises(ValueError): app.register_blueprint(bp) with pytest.raises(ValueError): app.register_blueprint(bp2, url_prefix="/a") app.register_blueprint(bp, name="alt") async def test_nested_blueprint() -> None: app = Quart(__name__) parent = Blueprint("parent", __name__, url_prefix="/parent") child = Blueprint("child", __name__) grandchild = Blueprint("grandchild", __name__) sibling = Blueprint("sibling", __name__) @parent.errorhandler(403) async def forbidden(_: Exception) -> ResponseReturnValue: return "Parent no", 403 @parent.route("/") async def parent_index() -> ResponseReturnValue: return "Parent yes" @parent.route("/no") async def parent_no() -> ResponseReturnValue: abort(403) @child.route("/") async def child_index() -> ResponseReturnValue: return "Child yes" @child.route("/no") async def child_no() -> ResponseReturnValue: abort(403) @grandchild.errorhandler(403) async def grandchild_forbidden(_: Exception) -> ResponseReturnValue: return "Grandchild no", 403 @grandchild.route("/") async def grandchild_index() -> ResponseReturnValue: return "Grandchild yes" @grandchild.route("/no") async def grandchild_no() -> ResponseReturnValue: abort(403) @sibling.route("/sibling") async def sibling_index() -> ResponseReturnValue: return "Sibling yes" child.register_blueprint(grandchild, url_prefix="/grandchild") parent.register_blueprint(child, url_prefix="/child") parent.register_blueprint(sibling) app.register_blueprint(parent) app.register_blueprint(parent, url_prefix="/alt", name="alt") client = app.test_client() assert (await (await client.get("/parent/")).get_data()) == b"Parent yes" assert (await (await client.get("/parent/child/")).get_data()) == b"Child yes" assert (await (await client.get("/parent/sibling")).get_data()) == b"Sibling yes" assert (await (await client.get("/alt/sibling")).get_data()) == b"Sibling yes" assert ( await (await client.get("/parent/child/grandchild/")).get_data() ) == b"Grandchild yes" assert (await (await client.get("/parent/no")).get_data()) == b"Parent no" assert (await (await client.get("/parent/child/no")).get_data()) == b"Parent no" assert ( await (await client.get("/parent/child/grandchild/no")).get_data() ) == b"Grandchild no" async def test_blueprint_renaming() -> None: app = Quart(__name__) bp = Blueprint("bp", __name__) bp2 = Blueprint("bp2", __name__) @bp.get("/") async def index() -> str: return request.endpoint @bp.get("/error") async def error() -> str: abort(403) @bp.errorhandler(403) async def forbidden(_: Exception) -> ResponseReturnValue: return "Error", 403 @bp2.get("/") async def index2() -> str: return 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") client = app.test_client() assert (await (await client.get("/a/")).get_data()) == b"bp.index" assert (await (await client.get("/b/")).get_data()) == b"alt.index" assert (await (await client.get("/a/a/")).get_data()) == b"bp.sub.index2" assert (await (await client.get("/b/a/")).get_data()) == b"alt.sub.index2" assert (await (await client.get("/a/error")).get_data()) == b"Error" assert (await (await client.get("/b/error")).get_data()) == b"Error" def test_self_registration() -> None: bp = Blueprint("bp", __name__) with pytest.raises(ValueError): bp.register_blueprint(bp) async def test_nested_callback_order() -> None: app = Quart(__name__) parent = Blueprint("parent", __name__) child = Blueprint("child", __name__) @app.before_request async def app_before1() -> None: g.setdefault("seen", []).append("app_1") @app.teardown_request async def app_teardown1(exc: BaseException | None = None) -> None: assert g.seen.pop() == "app_1" @app.before_request async def app_before2() -> None: g.setdefault("seen", []).append("app_2") @app.teardown_request async def app_teardown2(exc: BaseException | None = None) -> None: assert g.seen.pop() == "app_2" @app.context_processor async def app_ctx() -> dict: return dict(key="app") @parent.before_request async def parent_before1() -> None: g.setdefault("seen", []).append("parent_1") @parent.teardown_request async def parent_teardown1(exc: BaseException | None = None) -> None: assert g.seen.pop() == "parent_1" @parent.before_request async def parent_before2() -> None: g.setdefault("seen", []).append("parent_2") @parent.teardown_request async def parent_teardown2(exc: BaseException | None = None) -> None: assert g.seen.pop() == "parent_2" @parent.context_processor async def parent_ctx() -> dict: return dict(key="parent") @child.before_request async def child_before1() -> None: g.setdefault("seen", []).append("child_1") @child.teardown_request async def child_teardown1(exc: BaseException | None = None) -> None: assert g.seen.pop() == "child_1" @child.before_request async def child_before2() -> None: g.setdefault("seen", []).append("child_2") @child.teardown_request async def child_teardown2(exc: BaseException | None = None) -> None: assert g.seen.pop() == "child_2" @child.context_processor async def child_ctx() -> dict: return dict(key="child") @child.route("/a") async def a() -> str: return ", ".join(g.seen) @child.route("/b") async def b() -> str: return await render_template_string("{{ key }}") parent.register_blueprint(child) app.register_blueprint(parent) client = app.test_client() assert ( await (await client.get("/a")).get_data() ) == b"app_1, app_2, parent_1, parent_2, child_1, child_2" assert (await (await client.get("/b")).get_data()) == b"child" pallets-quart-6b318bd/tests/test_cli.py000066400000000000000000000076351524460054700202670ustar00rootroot00000000000000from __future__ import annotations import os import tempfile from collections.abc import Generator from pathlib import Path from unittest.mock import Mock import pytest from _pytest.monkeypatch import MonkeyPatch from click.testing import CliRunner import quart.cli from quart.app import Quart from quart.cli import AppGroup from quart.cli import cli from quart.cli import load_dotenv from quart.cli import ScriptInfo @pytest.fixture(scope="module") def reset_env() -> None: os.environ.pop("QUART_DEBUG", None) @pytest.fixture(name="app") def loadable_app(monkeypatch: MonkeyPatch) -> Mock: app = Mock(spec=Quart) app.cli = AppGroup() module = Mock() module.app = app monkeypatch.setattr(quart.cli, "import_module", lambda _: module) return app @pytest.fixture(name="dev_app") def loadable_dev_app(app: Mock) -> Mock: app.debug = True return app @pytest.fixture(name="debug_env") def debug_env_patch(monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("QUART_DEBUG", "true") @pytest.fixture(name="no_debug_env") def no_debug_env_patch(monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("QUART_DEBUG", "false") @pytest.fixture(name="empty_cwd") def empty_cwd() -> Generator[Path]: directory = tempfile.TemporaryDirectory() cwd = os.getcwd() os.chdir(directory.name) yield Path(directory.name) os.chdir(cwd) directory.cleanup() def test_script_info_load_app(app: Mock) -> None: info = ScriptInfo("module:app") assert info.load_app() == app def test_version_command() -> None: runner = CliRunner() result = runner.invoke(cli, ["--version"]) assert "Quart" in result.output def test_run_command(app: Mock) -> None: runner = CliRunner() runner.invoke(cli, ["--app", "module:app", "run"]) app.run.assert_called_once_with( debug=False, host="127.0.0.1", port=5000, certfile=None, keyfile=None, use_reloader=False, ) def test_run_command_development_debug_disabled( dev_app: Mock, no_debug_env: None ) -> None: runner = CliRunner() runner.invoke(cli, ["--app", "module:app", "run"]) dev_app.run.assert_called_once_with( debug=False, host="127.0.0.1", port=5000, certfile=None, keyfile=None, use_reloader=False, ) def test_load_dotenv(empty_cwd: Path) -> None: value = "dotenv" with open(empty_cwd / ".env", "w", encoding="utf8") as env: env.write(f"TEST_ENV_VAR={value}\n") load_dotenv() assert os.environ.pop("TEST_ENV_VAR", None) == value def test_load_dotquartenv(empty_cwd: Path) -> None: value = "dotquartenv" with open(empty_cwd / ".quartenv", "w", encoding="utf8") as env: env.write(f"TEST_ENV_VAR={value}\n") load_dotenv() assert os.environ.pop("TEST_ENV_VAR", None) == value def test_load_dotenv_beats_dotquartenv(empty_cwd: Path) -> None: env_value = "dotenv" quartenv_value = "dotquartenv" with open(empty_cwd / ".env", "w", encoding="utf8") as env: env.write(f"TEST_ENV_VAR={env_value}\n") with open(empty_cwd / ".quartenv", "w", encoding="utf8") as env: env.write(f"TEST_ENV_VAR={quartenv_value}\n") load_dotenv() assert os.environ.pop("TEST_ENV_VAR", None) == env_value def test_run_command_debug_option(app: Mock) -> None: runner = CliRunner() runner.invoke(cli, ["--app", "module:app", "run", "--debug"]) app.run.assert_called_once_with( debug=True, host="127.0.0.1", port=5000, certfile=None, keyfile=None, use_reloader=True, ) def test_run_command_no_debug_option(dev_app: Mock) -> None: runner = CliRunner() runner.invoke(cli, ["--app", "module:app", "run", "--no-debug"]) dev_app.run.assert_called_once_with( debug=False, host="127.0.0.1", port=5000, certfile=None, keyfile=None, use_reloader=False, ) pallets-quart-6b318bd/tests/test_ctx.py000066400000000000000000000133531524460054700203100ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import cast from unittest.mock import Mock import pytest from hypercorn.typing import HTTPScope from werkzeug.datastructures import Headers from werkzeug.exceptions import BadRequest from quart.app import Quart from quart.ctx import after_this_request from quart.ctx import AppContext from quart.ctx import copy_current_app_context from quart.ctx import copy_current_request_context from quart.ctx import copy_current_websocket_context from quart.ctx import has_app_context from quart.ctx import has_request_context from quart.globals import current_app from quart.globals import request from quart.globals import websocket from quart.routing import QuartRule from quart.testing import make_test_headers_path_and_query_string from quart.testing import no_op_push from quart.wrappers import Request async def test_request_context_match(http_scope: HTTPScope) -> None: app = Quart(__name__) url_adapter = Mock() rule = QuartRule("/", methods={"GET"}, endpoint="index") url_adapter.match.return_value = (rule, {"arg": "value"}) app.create_url_adapter = lambda *_: url_adapter # type: ignore request = Request( "GET", "http", "/", b"", Headers([("host", "quart.com")]), "", "1.1", http_scope, send_push_promise=no_op_push, ) async with AppContext(app, request=request): assert request.url_rule == rule assert request.view_args == {"arg": "value"} async def test_bad_request_if_websocket_route(http_scope: HTTPScope) -> None: app = Quart(__name__) url_adapter = Mock() url_adapter.match.side_effect = BadRequest() app.create_url_adapter = lambda *_: url_adapter # type: ignore request = Request( "GET", "http", "/", b"", Headers([("host", "quart.com")]), "", "1.1", http_scope, send_push_promise=no_op_push, ) async with AppContext(app, request=request): assert isinstance(request.routing_exception, BadRequest) async def test_after_this_request(http_scope: HTTPScope) -> None: app = Quart(__name__) headers, path, query_string = make_test_headers_path_and_query_string(app, "/") async with AppContext( Quart(__name__), request=Request( "GET", "http", path, query_string, headers, "", "1.1", http_scope, send_push_promise=no_op_push, ), ) as context: after_this_request(lambda: "hello") # type: ignore assert context._after_request_functions[0]() == "hello" # type: ignore async def test_has_request_context(http_scope: HTTPScope) -> None: app = Quart(__name__) headers, path, query_string = make_test_headers_path_and_query_string(app, "/") request = Request( "GET", "http", path, query_string, headers, "", "1.1", http_scope, send_push_promise=no_op_push, ) async with AppContext(Quart(__name__), request=request): assert has_request_context() is True assert has_app_context() is True assert has_request_context() is False assert has_app_context() is False async def test_has_app_context() -> None: async with AppContext(Quart(__name__)): assert has_app_context() is True assert has_app_context() is False async def test_copy_current_app_context() -> None: app = Quart(__name__) @app.route("/") async def index() -> str: current_app.foo = "bar" # type: ignore @copy_current_app_context async def within_context() -> None: assert current_app.foo == "bar" # type: ignore await asyncio.ensure_future(within_context()) return "" test_client = app.test_client() response = await test_client.get("/") assert response.status_code == 200 def test_copy_current_app_context_error() -> None: with pytest.raises(RuntimeError): copy_current_app_context(lambda: None)() async def test_copy_current_request_context() -> None: app = Quart(__name__) @app.route("/") async def index() -> str: @copy_current_request_context async def within_context() -> None: assert request.path == "/" await asyncio.ensure_future(within_context()) return "" test_client = app.test_client() response = await test_client.get("/") assert response.status_code == 200 def test_copy_current_request_context_error() -> None: with pytest.raises(RuntimeError): copy_current_request_context(lambda: None)() async def test_works_without_copy_current_request_context() -> None: app = Quart(__name__) @app.route("/") async def index() -> str: async def within_context() -> None: assert request.path == "/" await asyncio.ensure_future(within_context()) return "" test_client = app.test_client() response = await test_client.get("/") assert response.status_code == 200 async def test_copy_current_websocket_context() -> None: app = Quart(__name__) @app.websocket("/") async def index() -> None: @copy_current_websocket_context async def within_context() -> str: return websocket.path data = await asyncio.ensure_future(within_context()) await websocket.send(data.encode()) test_client = app.test_client() async with test_client.websocket("/") as test_websocket: data = await test_websocket.receive() assert cast(bytes, data) == b"/" def test_copy_current_websocket_context_error() -> None: with pytest.raises(RuntimeError): copy_current_websocket_context(lambda: None)() pallets-quart-6b318bd/tests/test_debug.py000066400000000000000000000005721524460054700205770ustar00rootroot00000000000000from __future__ import annotations from quart import Quart from quart.debug import traceback_response async def test_debug() -> None: app = Quart(__name__) async with app.test_request_context("/"): response = await traceback_response(Exception("Unique error")) assert response.status_code == 500 assert b"Unique error" in (await response.get_data()) pallets-quart-6b318bd/tests/test_exceptions.py000066400000000000000000000011731524460054700216700ustar00rootroot00000000000000from __future__ import annotations from http import HTTPStatus import pytest from werkzeug.exceptions import abort from werkzeug.exceptions import HTTPException from quart import Response @pytest.mark.parametrize("status", [400, HTTPStatus.BAD_REQUEST]) def test_abort(status: int | HTTPStatus) -> None: with pytest.raises(HTTPException) as exc_info: abort(status) assert exc_info.value.get_response().status_code == 400 def test_abort_with_response() -> None: with pytest.raises(HTTPException) as exc_info: abort(Response("Message", 205)) assert exc_info.value.get_response().status_code == 205 pallets-quart-6b318bd/tests/test_formparser.py000066400000000000000000000014241524460054700216660ustar00rootroot00000000000000from __future__ import annotations import pytest from werkzeug.exceptions import RequestEntityTooLarge from quart.formparser import MultiPartParser from quart.wrappers.request import Body async def test_multipart_max_form_memory_size() -> None: """max_form_memory_size is tracked across multiple data events.""" data = b"--bound\r\nContent-Disposition: form-field; name=a\r\n\r\n" data += b"a" * 15 + b"\r\n--bound--" body = Body(None, None) body.set_result(data) # The buffer size is less than the max size, so multiple data events will be # returned. The field size is greater than the max. parser = MultiPartParser(max_form_memory_size=10, buffer_size=5) with pytest.raises(RequestEntityTooLarge): await parser.parse(body, b"bound", 0) pallets-quart-6b318bd/tests/test_helpers.py000066400000000000000000000212211524460054700211450ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncGenerator from datetime import datetime from datetime import UTC from io import BytesIO from pathlib import Path import pytest from werkzeug.exceptions import NotFound from quart import Blueprint from quart import Quart from quart import request from quart.helpers import flash from quart.helpers import get_flashed_messages from quart.helpers import make_response from quart.helpers import send_file from quart.helpers import send_from_directory from quart.helpers import stream_with_context from quart.helpers import url_for SERVER_NAME = "localhost" ROOT_PATH = Path(__file__).parents[0] @pytest.fixture def app() -> Quart: app = Quart(__name__) app.config["SERVER_NAME"] = SERVER_NAME app.secret_key = "secret" @app.route("/") async def index() -> str: return "" async def index_post() -> str: return "" app.add_url_rule( "/post", view_func=index_post, methods=["POST"], endpoint="index_post" ) @app.route("/resource/") async def resource(id: int) -> str: return str(id) return app @pytest.fixture def host_matched_app() -> Quart: app = Quart(__name__, host_matching=True, static_host="localhost") app.config["SERVER_NAME"] = SERVER_NAME @app.route("/") async def index() -> str: return "" @app.route("/", host="quart.com") async def host() -> str: return "" return app async def test_make_response(app: Quart) -> None: async with app.app_context(): response = await make_response("foo", 202) assert response.status_code == 202 assert b"foo" in (await response.get_data()) # type: ignore async def test_flash(app: Quart) -> None: async with app.test_request_context("/"): await flash("message") assert get_flashed_messages() == ["message"] assert get_flashed_messages() == ["message"] async def test_flash_category(app: Quart) -> None: async with app.test_request_context("/"): await flash("bar", "error") await flash("foo", "info") assert get_flashed_messages(with_categories=True) == [ ("error", "bar"), ("info", "foo"), ] assert get_flashed_messages(with_categories=True) == [ ("error", "bar"), ("info", "foo"), ] async def test_flash_category_filter(app: Quart) -> None: async with app.test_request_context("/"): await flash("bar", "error") await flash("foo", "info") assert get_flashed_messages(category_filter=["error"]) == ["bar"] assert get_flashed_messages(category_filter=["error"]) == ["bar"] async def test_url_for(app: Quart) -> None: async with app.test_request_context("/"): assert url_for("index") == "/" assert url_for("index_post", _method="POST") == "/post" assert url_for("resource", id=5) == "/resource/5" async def test_url_for_host_matching(host_matched_app: Quart) -> None: async with host_matched_app.app_context(): assert url_for("index", _external=True) == "http:///" assert url_for("host", _external=True) == "http://quart.com/" async def test_url_for_external(app: Quart) -> None: async with app.test_request_context("/"): assert url_for("index") == "/" assert url_for("index", _external=True) == "http://localhost/" assert ( url_for("resource", id=5, _external=True) == "http://localhost/resource/5" ) assert url_for("resource", id=5, _external=False) == "/resource/5" async with app.app_context(): assert url_for("index") == "http://localhost/" assert url_for("index", _external=False) == "/" async def test_url_for_scheme(app: Quart) -> None: async with app.test_request_context("/"): assert url_for("index", _scheme="https") == "https://localhost/" assert url_for("index", _scheme="https", _external=True) == "https://localhost/" assert ( url_for("resource", id=5, _scheme="https", _external=True) == "https://localhost/resource/5" ) async def test_url_for_anchor(app: Quart) -> None: async with app.test_request_context("/"): assert url_for("index", _anchor="&foo") == "/#&foo" assert url_for("resource", id=5, _anchor="&foo") == "/resource/5#&foo" async def test_url_for_blueprint_relative(app: Quart) -> None: blueprint = Blueprint("blueprint", __name__) @blueprint.route("/") def index() -> str: return "" app.register_blueprint(blueprint, url_prefix="/blue") async with app.test_request_context("/blue/"): assert url_for(".index") == "/blue/" assert url_for("index") == "/" async def test_url_for_root_path(app: Quart) -> None: async with app.test_request_context("/", root_path="/bob"): assert url_for("index") == "/bob/" assert url_for("index_post", _method="POST") == "/bob/post" assert url_for("resource", id=5) == "/bob/resource/5" async def test_stream_with_context() -> None: app = Quart(__name__) @app.route("/") async def index() -> AsyncGenerator[bytes]: @stream_with_context async def generator() -> AsyncGenerator[bytes]: yield request.method.encode() yield b" " yield request.path.encode() return generator() test_client = app.test_client() response = await test_client.get("/") result = await response.get_data(as_text=False) assert result == b"GET /" async def test_send_from_directory_raises() -> None: with pytest.raises(NotFound): await send_from_directory(str(ROOT_PATH), "no_file.no") async def test_send_file_path(tmp_path: Path) -> None: app = Quart(__name__) file_ = tmp_path / "send.img" file_.write_text("something") async with app.app_context(): response = await send_file(Path(file_)) assert (await response.get_data(as_text=False)) == file_.read_bytes() async def test_send_file_bytes_io() -> None: app = Quart(__name__) io_stream = BytesIO(b"something") async with app.app_context(): response = await send_file(io_stream, mimetype="text/plain") assert (await response.get_data(as_text=False)) == b"something" async def test_send_file_no_mimetype() -> None: app = Quart(__name__) async with app.app_context(): with pytest.raises(ValueError): await send_file(BytesIO(b"something")) async def test_send_file_as_attachment(tmp_path: Path) -> None: app = Quart(__name__) file_ = tmp_path / "send.img" file_.write_text("something") async with app.app_context(): response = await send_file(Path(file_), as_attachment=True) assert response.headers["content-disposition"] == "attachment; filename=send.img" async def test_send_file_as_attachment_name(tmp_path: Path) -> None: app = Quart(__name__) file_ = tmp_path / "send.img" file_.write_text("something") async with app.app_context(): response = await send_file( Path(file_), as_attachment=True, attachment_filename="send.html" ) assert response.headers["content-disposition"] == "attachment; filename=send.html" async def test_send_file_mimetype(tmp_path: Path) -> None: app = Quart(__name__) file_ = tmp_path / "send.bob" file_.write_text("something") async with app.app_context(): response = await send_file(Path(file_), mimetype="application/bob") assert (await response.get_data(as_text=False)) == file_.read_bytes() assert response.headers["Content-Type"] == "application/bob" async def test_send_file_last_modified(tmp_path: Path) -> None: app = Quart(__name__) file_ = tmp_path / "send.img" file_.write_text("something") async with app.app_context(): response = await send_file(str(file_)) mtime = datetime.fromtimestamp(file_.stat().st_mtime, tz=UTC) mtime = mtime.replace(microsecond=0) assert response.last_modified == mtime async def test_send_file_last_modified_override(tmp_path: Path) -> None: app = Quart(__name__) file_ = tmp_path / "send.img" file_.write_text("something") last_modified = datetime(2015, 10, 10, tzinfo=UTC) async with app.app_context(): response = await send_file(str(file_), last_modified=last_modified) assert response.last_modified == last_modified async def test_send_file_max_age(tmp_path: Path) -> None: app = Quart(__name__) file_ = tmp_path / "send.img" file_.write_text("something") async with app.app_context(): response = await send_file(str(file_)) assert ( response.cache_control.max_age == app.config["SEND_FILE_MAX_AGE_DEFAULT"].total_seconds() ) pallets-quart-6b318bd/tests/test_routing.py000066400000000000000000000020141524460054700211710ustar00rootroot00000000000000from __future__ import annotations import warnings import pytest from hypercorn.typing import HTTPScope from werkzeug.datastructures import Headers from quart.routing import QuartMap from quart.testing import no_op_push from quart.wrappers.request import Request @pytest.mark.parametrize( "server_name, warns", [("localhost", False), ("quart.com", True)], ) async def test_bind_warning( server_name: str, warns: bool, http_scope: HTTPScope ) -> None: map_ = QuartMap(host_matching=False) request = Request( "GET", "http", "/", b"", Headers([("host", "Localhost")]), "", "1.1", http_scope, send_push_promise=no_op_push, ) if warns: with pytest.warns(UserWarning): map_.bind_to_request(request, subdomain=None, server_name=server_name) else: with warnings.catch_warnings(): warnings.simplefilter("error") map_.bind_to_request(request, subdomain=None, server_name=server_name) pallets-quart-6b318bd/tests/test_sessions.py000066400000000000000000000057101524460054700213560ustar00rootroot00000000000000from __future__ import annotations from http.cookies import SimpleCookie from hypercorn.typing import HTTPScope from werkzeug.datastructures import Headers from quart.app import Quart from quart.sessions import SecureCookieSession from quart.sessions import SecureCookieSessionInterface from quart.testing import no_op_push from quart.wrappers import Request from quart.wrappers import Response async def test_secure_cookie_session_interface_open_session( http_scope: HTTPScope, ) -> None: session = SecureCookieSession() session["something"] = "else" interface = SecureCookieSessionInterface() app = Quart(__name__) app.secret_key = "secret" response = Response("") await interface.save_session(app, session, response) request = Request( "GET", "http", "/", b"", Headers(), "", "1.1", http_scope, send_push_promise=no_op_push, ) request.headers["Cookie"] = response.headers["Set-Cookie"] new_session = await interface.open_session(app, request) assert new_session == session async def test_secure_cookie_session_interface_save_session() -> None: session = SecureCookieSession() session["something"] = "else" interface = SecureCookieSessionInterface() app = Quart(__name__) app.secret_key = "secret" response = Response("") await interface.save_session(app, session, response) cookies: SimpleCookie = SimpleCookie() cookies.load(response.headers["Set-Cookie"]) cookie = cookies[app.config["SESSION_COOKIE_NAME"]] assert cookie["path"] == interface.get_cookie_path(app) assert cookie["httponly"] == "" if not interface.get_cookie_httponly(app) else True assert cookie["secure"] == "" if not interface.get_cookie_secure(app) else True assert cookie["samesite"] == (interface.get_cookie_samesite(app) or "") assert cookie["domain"] == (interface.get_cookie_domain(app) or "") assert cookie["expires"] == (interface.get_expiration_time(app, session) or "") assert response.headers["Vary"] == "Cookie" async def _save_session(session: SecureCookieSession) -> Response: interface = SecureCookieSessionInterface() app = Quart(__name__) app.secret_key = "secret" response = Response("") await interface.save_session(app, session, response) return response async def test_secure_cookie_session_interface_save_session_no_modification() -> None: session = SecureCookieSession() session["something"] = "else" session.modified = False response = await _save_session(session) assert response.headers.get("Set-Cookie") is None async def test_secure_cookie_session_interface_save_session_no_access() -> None: session = SecureCookieSession() session["something"] = "else" session.accessed = False session.modified = False response = await _save_session(session) assert response.headers.get("Set-Cookie") is None assert response.headers.get("Vary") is None pallets-quart-6b318bd/tests/test_static_hosting.py000066400000000000000000000020471524460054700225320ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from quart.app import Quart async def test_host_matching() -> None: app = Quart(__name__, static_folder="./assets", static_url_path="/static") test_client = app.test_client() response = await test_client.get("/static/config.cfg") assert response.status_code == 200 data = await response.get_data(as_text=False) expected_data = (Path(__file__).parent / "assets/config.cfg").read_bytes() assert data == expected_data response = await test_client.get("/static/foo") assert response.status_code == 404 # Should not be able to escape ! response = await test_client.get("/static/../foo") assert response.status_code == 404 response = await test_client.get("/static/../assets/config.cfg") assert response.status_code == 404 # Non-escaping path with .. response = await test_client.get("/static/foo/../config.cfg") assert response.status_code == 200 data = await response.get_data(as_text=False) assert data == expected_data pallets-quart-6b318bd/tests/test_sync.py000066400000000000000000000022571524460054700204670ustar00rootroot00000000000000from __future__ import annotations import threading from collections.abc import Generator import pytest from quart import Quart from quart import request from quart import ResponseReturnValue @pytest.fixture(name="app") def _app() -> Quart: app = Quart(__name__) @app.route("/", methods=["GET", "POST"]) def index() -> ResponseReturnValue: return request.method @app.route("/gen") def gen() -> ResponseReturnValue: def _gen() -> Generator[bytes]: yield b"%d" % threading.current_thread().ident for _ in range(2): yield b"b" return _gen(), 200 return app async def test_sync_request_context(app: Quart) -> None: test_client = app.test_client() response = await test_client.get("/") assert b"GET" in (await response.get_data()) response = await test_client.post("/") assert b"POST" in (await response.get_data()) async def test_sync_generator(app: Quart) -> None: test_client = app.test_client() response = await test_client.get("/gen") result = await response.get_data() assert result[-2:] == b"bb" assert int(result[:-2]) != threading.current_thread().ident pallets-quart-6b318bd/tests/test_templating.py000066400000000000000000000110721524460054700216520ustar00rootroot00000000000000from __future__ import annotations import asyncio import pytest from quart import Blueprint from quart import g from quart import Quart from quart import render_template_string from quart import Response from quart import ResponseReturnValue from quart import session from quart import stream_template_string @pytest.fixture(scope="function") def app() -> Quart: app = Quart(__name__) app.secret_key = "secret" return app @pytest.fixture(scope="function") def blueprint() -> Blueprint: blueprint = Blueprint("blueprint", __name__) @blueprint.route("/") def index() -> str: return "" return blueprint async def test_template_render(app: Quart) -> None: async with app.app_context(): rendered = await render_template_string("{{ foo }}", foo="bar") assert rendered == "bar" async def test_default_template_context(app: Quart) -> None: async with app.app_context(): g.foo = "bar" rendered = await render_template_string("{{ g.foo }}") assert rendered == "bar" async with app.test_request_context("/"): session["foo"] = "bar" rendered = await render_template_string( "{{ request.method }} {{ request.path }} {{ session.foo }}" ) assert rendered == "GET / bar" async def test_template_context_processors(app: Quart, blueprint: Blueprint) -> None: @blueprint.context_processor async def blueprint_context() -> dict: await asyncio.sleep(0.01) # Test the ability to await return {"context": "foo"} @blueprint.app_context_processor async def app_blueprint_context() -> dict: return {"global_context": "boo"} @app.context_processor async def app_context() -> dict: return {"context": "bar"} app.register_blueprint(blueprint) async with app.app_context(): rendered = await render_template_string("{{ context }}") assert rendered == "bar" async with app.test_request_context("/"): rendered = await render_template_string("{{ context }} {{ global_context }}") assert rendered == "foo boo" async with app.test_request_context("/other"): rendered = await render_template_string("{{ context }} {{ global_context }}") assert rendered == "bar boo" async def test_template_globals(app: Quart, blueprint: Blueprint) -> None: @blueprint.app_template_global() def blueprint_global(value: str) -> str: return value.upper() @app.template_global() def app_global(value: str) -> str: return value.lower() app.register_blueprint(blueprint) async with app.app_context(): rendered = await render_template_string( "{{ app_global('BAR') }} {{ blueprint_global('foo') }}" ) assert rendered == "bar FOO" async def test_template_filters(app: Quart, blueprint: Blueprint) -> None: @blueprint.app_template_filter() def blueprint_filter(value: str) -> str: return value.upper() @app.template_filter() def app_filter(value: str) -> str: return value.lower() app.register_blueprint(blueprint) async with app.app_context(): rendered = await render_template_string("{{ 'App' | app_filter }}") assert rendered == "app" async with app.test_request_context("/"): rendered = await render_template_string("{{ 'App' | blueprint_filter }}") assert rendered == "APP" async def test_template_tests(app: Quart, blueprint: Blueprint) -> None: @blueprint.app_template_test() def blueprint_test(value: int) -> bool: return value == 5 @app.template_test() def app_test(value: int) -> bool: return value == 3 app.register_blueprint(blueprint) async with app.app_context(): rendered = await render_template_string("{% if 3 is app_test %}foo{% endif %}") assert rendered == "foo" async with app.test_request_context("/"): rendered = await render_template_string( "{% if 5 is blueprint_test %}bar{% endif %}" ) assert rendered == "bar" async def test_simple_stream(app: Quart) -> None: @app.get("/") async def index() -> ResponseReturnValue: return await stream_template_string("{{ config }}", config=42) # type: ignore test_client = app.test_client() response = await test_client.get("/") assert (await response.data) == b"42" @app.get("/2") async def index2() -> ResponseReturnValue: return Response(await stream_template_string("{{ config }}", config=43)) test_client = app.test_client() response = await test_client.get("/2") assert (await response.data) == b"43" pallets-quart-6b318bd/tests/test_testing.py000066400000000000000000000330371524460054700211700ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable from io import BytesIO import pytest from werkzeug.datastructures import Headers from werkzeug.wrappers import Response as WerkzeugResponse from quart import jsonify from quart import Quart from quart import redirect from quart import request from quart import Response from quart import session from quart import websocket from quart.datastructures import FileStorage from quart.testing import make_test_body_with_headers from quart.testing import make_test_headers_path_and_query_string from quart.testing import make_test_scope from quart.testing import QuartClient as Client from quart.testing import WebsocketResponseError async def test_methods() -> None: app = Quart(__name__) methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"] @app.route("/", methods=methods) async def echo() -> str: return request.method client = Client(app) for method in methods: func = getattr(client, method.lower()) response = await func("/") assert method in (await response.get_data(as_text=True)) @pytest.mark.parametrize( ( "path", "query_string", "subdomain", "expected_path", "expected_query_string", "expected_host", ), [ ("/path", {"a": "b"}, None, "/path", b"a=b", "localhost"), ("/path", {"a": ["b", "c"]}, None, "/path", b"a=b&a=c", "localhost"), ("/path?b=c", None, None, "/path", b"b=c", "localhost"), ("/path%20", None, None, "/path ", b"", "localhost"), ("/path", None, "api", "/path", b"", "api.localhost"), ], ) def test_build_headers_path_and_query_string( path: str, query_string: dict | None, subdomain: str | None, expected_path: str, expected_query_string: bytes, expected_host: str, ) -> None: headers, result_path, result_qs = make_test_headers_path_and_query_string( Quart(__name__), path, None, query_string, None, subdomain ) assert result_path == expected_path assert headers["User-Agent"] == "Quart" assert headers["host"] == expected_host assert result_qs == expected_query_string def test_build_headers_path_and_query_string_with_query_string_error() -> None: with pytest.raises(ValueError): make_test_headers_path_and_query_string( Quart(__name__), "/?a=b", None, {"c": "d"} ) def test_build_headers_path_and_query_string_with_auth() -> None: headers, *_ = make_test_headers_path_and_query_string( Quart(__name__), "/", None, None, ("user", "pass"), ) assert headers["Authorization"] == "Basic dXNlcjpwYXNz" def test_make_test_body_with_headers_data() -> None: body, headers = make_test_body_with_headers(data="data") assert body == b"data" assert headers == Headers() def test_make_test_body_with_headers_form() -> None: body, headers = make_test_body_with_headers(form={"a": "b"}) assert body == b"a=b" assert headers == Headers({"Content-Type": "application/x-www-form-urlencoded"}) def test_make_test_body_with_headers_files() -> None: body, headers = make_test_body_with_headers( files={"a": FileStorage(BytesIO(b"abc"), filename="Quart")} ) assert body == ( b'\r\n------QuartBoundary\r\nContent-Disposition: form-data; name="a"; ' b'filename="Quart"\r\n\r\nabc\r\n------QuartBoundary--\r\n' ) assert headers == Headers( {"Content-Type": "multipart/form-data; boundary=----QuartBoundary"} ) def test_make_test_body_with_headers_form_and_files() -> None: body, headers = make_test_body_with_headers( form={"b": "c"}, files={"a": FileStorage(BytesIO(b"abc"), filename="Quart")} ) assert body == ( b'\r\n------QuartBoundary\r\nContent-Disposition: form-data; name="a"; ' b'filename="Quart"\r\n\r\nabc\r\n------QuartBoundary\r\n' b'Content-Disposition: form-data; name="b"\r\n\r\nc\r\n' b"------QuartBoundary--\r\n" ) assert headers == Headers( {"Content-Type": "multipart/form-data; boundary=----QuartBoundary"} ) def test_make_test_body_with_headers_json() -> None: body, headers = make_test_body_with_headers(json={"a": "b"}) assert body == b'{"a": "b"}' assert headers == Headers({"Content-Type": "application/json"}) def test_make_test_body_with_headers_argument_error() -> None: with pytest.raises(ValueError): make_test_body_with_headers(json={}, data="", form={}, files={}) make_test_body_with_headers(form={}, files={}) @pytest.mark.parametrize( "path, expected_raw_path", [ ("/", b"/"), ("/��", b"/%E2%9D%A4%EF%B8%8F"), ], ) def test_make_test_scope_with_scope_base(path: str, expected_raw_path: bytes) -> None: scope = make_test_scope( "http", path, "GET", Headers(), b"", "http", "", "1.1", {"client": ("127.0.0.2", "1234")}, ) assert scope == { "type": "http", "http_version": "1.1", "asgi": {"spec_version": "2.1"}, "method": "GET", "scheme": "http", "path": path, "raw_path": expected_raw_path, "query_string": b"", "root_path": "", "headers": [], "extensions": {}, "_quart._preserve_context": False, "client": ("127.0.0.2", "1234"), } @pytest.mark.parametrize( "headers, expected", [ (None, Headers({"User-Agent": "Quart", "host": "localhost"})), ( Headers({"User-Agent": "Quarty", "host": "quart.com"}), Headers({"User-Agent": "Quarty", "host": "quart.com"}), ), ], ) def test_build_headers_path_and_query_string_headers_defaults( headers: Headers, expected: Headers ) -> None: result, path, query_string = make_test_headers_path_and_query_string( Quart(__name__), "/path", headers ) assert result == expected assert path == "/path" assert query_string == b"" async def test_remote_addr() -> None: app = Quart(__name__) @app.route("/") async def echo() -> str: return request.remote_addr client = Client(app) response = await client.get("/", scope_base={"client": ("127.0.0.2", "1234")}) assert (await response.get_data(as_text=True)) == "127.0.0.2" async def test_json() -> None: app = Quart(__name__) @app.route("/", methods=["POST"]) async def echo() -> Response: data = await request.get_json() return jsonify(data) client = Client(app) response = await client.post("/", json={"a": "b"}) assert (await response.get_json()) == {"a": "b"} async def test_form() -> None: app = Quart(__name__) @app.route("/", methods=["POST"]) async def echo() -> Response: data = await request.form return jsonify(**data) client = Client(app) response = await client.post("/", form={"a": "b"}) assert (await response.get_json()) == {"a": "b"} async def test_files() -> None: app = Quart(__name__) @app.route("/", methods=["POST"]) async def echo() -> Response: files = await request.files data = files["file"].read() return data client = Client(app) response = await client.post( "/", files={"file": FileStorage(BytesIO(b"bar"), filename="a.txt")} ) assert (await response.get_data(as_text=True)) == "bar" async def test_data() -> None: app = Quart(__name__) @app.route("/", methods=["POST"]) async def echo() -> str: data = await request.get_data(as_text=True) return data # type: ignore client = Client(app) headers = {"Content-Type": "application/octet-stream"} response = await client.post("/", data=b"ABCDEFG", headers=headers) assert (await response.get_data(as_text=False)) == b"ABCDEFG" async def test_query_string() -> None: app = Quart(__name__) @app.route("/", methods=["GET"]) async def echo() -> Response: data = request.args return jsonify(**data) client = Client(app) response = await client.get("/", query_string={"a": "b"}) assert (await response.get_json()) == {"a": "b"} async def test_redirect() -> None: app = Quart(__name__) @app.route("/", methods=["GET"]) async def echo() -> str: return request.method @app.route("/redirect", methods=["GET"]) async def redir() -> WerkzeugResponse: return redirect("/") client = Client(app) assert (await client.get("/redirect", follow_redirects=True)).status_code == 200 async def test_cookie_jar() -> None: app = Quart(__name__) app.secret_key = "secret" @app.route("/", methods=["GET"]) async def echo() -> Response: foo = session.get("foo") bar = request.cookies.get("bar") session["foo"] = "bar" response = jsonify({"foo": foo, "bar": bar}) response.set_cookie("bar", "foo") return response client = Client(app) response = await client.get("/") assert (await response.get_json()) == {"foo": None, "bar": None} response = await client.get("/") assert (await response.get_json()) == {"foo": "bar", "bar": "foo"} async def test_redirect_cookie_jar() -> None: app = Quart(__name__) app.secret_key = "secret" @app.route("/a") async def a() -> WerkzeugResponse: response = redirect("/b") response.set_cookie("bar", "foo") return response @app.route("/b") async def echo() -> Response: bar = request.cookies.get("bar") response = jsonify({"bar": bar}) return response client = Client(app) response = await client.get("/a", follow_redirects=True) assert (await response.get_json()) == {"bar": "foo"} async def test_set_cookie() -> None: app = Quart(__name__) @app.route("/", methods=["GET"]) async def echo() -> Response: return jsonify({"foo": request.cookies.get("foo")}) client = Client(app) client.set_cookie("localhost", "foo", "bar") response = await client.get("/") assert (await response.get_json()) == {"foo": "bar"} async def test_websocket_bad_request() -> None: app = Quart(__name__) @app.route("/") async def index() -> str: return "" test_client = app.test_client() with pytest.raises(WebsocketResponseError): async with test_client.websocket("/"): pass async def test_push_promise() -> None: app = Quart(__name__) @app.route("/") async def index() -> str: await request.send_push_promise("/") return "" test_client = app.test_client() await test_client.get("/", http_version="2") assert test_client.push_promises[0][0] == "/" async def test_session_transactions() -> None: app = Quart(__name__) app.secret_key = "secret" @app.route("/") async def index() -> str: return str(session["foo"]) test_client = app.test_client() async with test_client.session_transaction() as local_session: assert len(local_session) == 0 local_session["foo"] = [42] assert len(local_session) == 1 response = await test_client.get("/") assert (await response.get_data()) == b"[42]" async with test_client.session_transaction() as local_session: assert len(local_session) == 1 assert local_session["foo"] == [42] async def test_with_usage() -> None: app = Quart(__name__) app.secret_key = "secret" @app.route("/") async def index() -> str: session["hello"] = "world" return "Hello" async with app.test_client() as client: await client.get("/") assert request.method == "GET" assert session["hello"] == "world" async def test_websocket_json() -> None: app = Quart(__name__) @app.websocket("/ws/") async def ws() -> None: data = await websocket.receive_json() await websocket.send_json(data) async with app.test_client().websocket("/ws/") as test_websocket: await test_websocket.send_json({"foo": "bar"}) data = await test_websocket.receive_json() assert data == {"foo": "bar"} async def test_middleware() -> None: app = Quart(__name__) @app.route("/") async def index() -> str: return "Hello" class OddMiddleware: def __init__(self, app: Callable) -> None: self.app = app async def __call__( self, scope: dict, receive: Callable, send: Callable ) -> None: if scope["path"] != "/": await send( { "type": "http.response.start", "status": 401, "headers": [(b"content-length", b"0")], } ) await send( { "type": "http.response.body", "body": b"", "more_body": False, } ) else: await self.app(scope, receive, send) app.asgi_app = OddMiddleware(app.asgi_app) # type: ignore client = app.test_client() response = await client.get("/") assert response.status_code == 200 response = await client.get("/odd") assert response.status_code == 401 async def test_auth() -> None: app = Quart(__name__) @app.get("/") async def echo() -> str: return f"{request.authorization.username}:{request.authorization.password}" client = Client(app) response = await client.get("/", auth=("user", "pass")) assert (await response.get_data(as_text=True)) == "user:pass" pallets-quart-6b318bd/tests/test_utils.py000066400000000000000000000005731524460054700206520ustar00rootroot00000000000000from __future__ import annotations from werkzeug.datastructures import Headers from quart.utils import decode_headers from quart.utils import encode_headers def test_encode_headers() -> None: assert encode_headers(Headers({"Foo": "Bar"})) == [(b"foo", b"Bar")] def test_decode_headers() -> None: assert decode_headers([(b"foo", b"Bar")]) == Headers({"Foo": "Bar"}) pallets-quart-6b318bd/tests/test_views.py000066400000000000000000000041271524460054700206460ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable from typing import Any import pytest from quart import Quart from quart import request from quart import ResponseReturnValue from quart.views import MethodView from quart.views import View @pytest.fixture def app() -> Quart: app = Quart(__name__) return app async def test_view(app: Quart) -> None: class Views(View): methods = ["GET", "POST"] async def dispatch_request( self, *args: Any, **kwargs: Any ) -> ResponseReturnValue: return request.method app.add_url_rule("/", view_func=Views.as_view("simple")) test_client = app.test_client() response = await test_client.get("/") assert "GET" == (await response.get_data(as_text=True)) response = await test_client.put("/") assert response.status_code == 405 async def test_method_view(app: Quart) -> None: class Views(MethodView): async def get(self) -> ResponseReturnValue: return "GET" async def post(self) -> ResponseReturnValue: return "POST" app.add_url_rule("/", view_func=Views.as_view("simple")) test_client = app.test_client() response = await test_client.get("/") assert "GET" == (await response.get_data(as_text=True)) response = await test_client.post("/") assert "POST" == (await response.get_data(as_text=True)) async def test_view_decorators(app: Quart) -> None: def decorate_status_code(func: Callable) -> Callable: async def wrapper(*args: Any, **kwargs: Any) -> ResponseReturnValue: response = await func(*args, **kwargs) return response, 201 return wrapper class Views(View): decorators = [decorate_status_code] methods = ["GET"] async def dispatch_request( self, *args: Any, **kwargs: Any ) -> ResponseReturnValue: return request.method app.add_url_rule("/", view_func=Views.as_view("simple")) test_client = app.test_client() response = await test_client.get("/") assert response.status_code == 201 pallets-quart-6b318bd/tests/wrappers/000077500000000000000000000000001524460054700177375ustar00rootroot00000000000000pallets-quart-6b318bd/tests/wrappers/test_base.py000066400000000000000000000077461524460054700223000ustar00rootroot00000000000000from __future__ import annotations from base64 import b64encode import pytest from hypercorn.typing import HTTPScope from werkzeug.datastructures import Headers from quart.wrappers.base import BaseRequestWebsocket def test_basic_authorization(http_scope: HTTPScope) -> None: headers = Headers() headers["Authorization"] = "Basic {}".format( b64encode(b"identity:secret").decode("ascii") ) request = BaseRequestWebsocket( "GET", "http", "/", b"", headers, "", "1.1", http_scope ) auth = request.authorization assert auth.username == "identity" assert auth.password == "secret" def test_digest_authorization(http_scope: HTTPScope) -> None: headers = Headers() headers["Authorization"] = ( "Digest " 'username="identity", ' 'realm="realm@rea.lm", ' 'nonce="abcd1234", ' 'uri="/path", ' 'response="abcd1235", ' 'opaque="abcd1236"' ) request = BaseRequestWebsocket( "GET", "http", "/", b"", headers, "", "1.1", http_scope ) auth = request.authorization assert auth.username == "identity" assert auth.realm == "realm@rea.lm" assert auth.nonce == "abcd1234" assert auth.uri == "/path" assert auth.response == "abcd1235" assert auth.opaque == "abcd1236" @pytest.mark.parametrize( "method, scheme, host, path, query_string," "expected_path, expected_full_path, expected_url, expected_base_url," "expected_url_root, expected_host_url", [ ( "GET", "http", "quart.com", "/", b"", "/", "/?", "http://quart.com/", "http://quart.com/", "http://quart.com/", "http://quart.com/", ), ( "GET", "http", "quart.com", "/", b"a=b", "/", "/?a=b", "http://quart.com/?a=b", "http://quart.com/", "http://quart.com/", "http://quart.com/", ), ( "GET", "https", "quart.com", "/branch/leaf", b"a=b", "/branch/leaf", "/branch/leaf?a=b", "https://quart.com/branch/leaf?a=b", "https://quart.com/branch/leaf", "https://quart.com/", "https://quart.com/", ), ], ) def test_url_structure( method: str, scheme: str, host: str, path: str, query_string: bytes, expected_path: str, expected_full_path: str, expected_url: str, expected_base_url: str, expected_url_root: str, expected_host_url: str, http_scope: HTTPScope, ) -> None: base_request_websocket = BaseRequestWebsocket( method, scheme, path, query_string, Headers({"host": host}), "", "1.1", http_scope, ) assert base_request_websocket.path == expected_path assert base_request_websocket.query_string == query_string assert base_request_websocket.full_path == expected_full_path assert base_request_websocket.url == expected_url assert base_request_websocket.base_url == expected_base_url assert base_request_websocket.url_root == expected_url_root assert base_request_websocket.host_url == expected_host_url assert base_request_websocket.host == host assert base_request_websocket.method == method assert base_request_websocket.scheme == scheme assert base_request_websocket.is_secure == scheme.endswith("s") def test_query_string(http_scope: HTTPScope) -> None: base_request_websocket = BaseRequestWebsocket( "GET", "http", "/", b"a=b&a=c&f", Headers({"host": "localhost"}), "", "1.1", http_scope, ) assert base_request_websocket.query_string == b"a=b&a=c&f" assert base_request_websocket.args.getlist("a") == ["b", "c"] assert base_request_websocket.args["f"] == "" pallets-quart-6b318bd/tests/wrappers/test_request.py000066400000000000000000000107441524460054700230460ustar00rootroot00000000000000from __future__ import annotations import asyncio from urllib.parse import urlencode import pytest from hypercorn.typing import HTTPScope from werkzeug.datastructures import Headers from werkzeug.exceptions import RequestEntityTooLarge from werkzeug.exceptions import RequestTimeout from quart.testing import no_op_push from quart.wrappers.request import Body from quart.wrappers.request import Request async def _fill_body(body: Body, semaphore: asyncio.Semaphore, limit: int) -> None: for number in range(limit): await body.put(b"%d" % number) await semaphore.acquire() body.set_complete() async def test_full_body() -> None: body = Body(None, None) limit = 3 semaphore = asyncio.Semaphore(limit) asyncio.ensure_future(_fill_body(body, semaphore, limit)) assert b"012" == await body async def test_body_streaming() -> None: body = Body(None, None) limit = 3 semaphore = asyncio.Semaphore(0) asyncio.ensure_future(_fill_body(body, semaphore, limit)) index = 0 result = [] async for data in body: semaphore.release() result.append(data) index += 1 assert result == [b"0", b"1", b"2", b""] assert b"" == await body async def test_body_stream_single_chunk() -> None: body = Body(None, None) body.set_result(b"data") async def _check_data() -> None: async for data in body: assert data == b"data" await asyncio.wait_for(_check_data(), 1) async def test_body_streaming_no_data() -> None: body = Body(None, None) semaphore = asyncio.Semaphore(0) asyncio.ensure_future(_fill_body(body, semaphore, 0)) async for data in body: assert data == b"" assert b"" == await body async def test_body_exceeds_max_content_length() -> None: max_content_length = 5 body = Body(None, max_content_length) semaphore = asyncio.Semaphore(max_content_length + 1) asyncio.ensure_future(_fill_body(body, semaphore, max_content_length + 1)) with pytest.raises(RequestEntityTooLarge): await body async def test_request_exceeds_max_content_length(http_scope: HTTPScope) -> None: max_content_length = 5 headers = Headers() headers["Content-Length"] = str(max_content_length + 1) request = Request( "POST", "http", "/", b"", headers, "", "1.1", http_scope, max_content_length=max_content_length, send_push_promise=no_op_push, ) with pytest.raises(RequestEntityTooLarge): await request.get_data() async def test_request_get_data_timeout(http_scope: HTTPScope) -> None: request = Request( "POST", "http", "/", b"", Headers(), "", "1.1", http_scope, body_timeout=1, send_push_promise=no_op_push, ) with pytest.raises(RequestTimeout): await request.get_data() @pytest.mark.parametrize( "method, expected", [("GET", ["b", "c"]), ("POST", ["b", "c", "d"])], ) async def test_request_values( method: str, expected: list[str], http_scope: HTTPScope ) -> None: request = Request( method, "http", "/", b"a=b&a=c", Headers( {"host": "quart.com", "Content-Type": "application/x-www-form-urlencoded"} ), "", "1.1", http_scope, send_push_promise=no_op_push, ) request.body.set_result(urlencode({"a": "d"}).encode()) assert (await request.values).getlist("a") == expected async def test_request_send_push_promise(http_scope: HTTPScope) -> None: push_promise: tuple[str, Headers] = None async def _push(path: str, headers: Headers) -> None: nonlocal push_promise push_promise = (path, headers) request = Request( "GET", "http", "/", b"a=b&a=c", Headers( { "host": "quart.com", "Content-Type": "application/x-www-form-urlencoded", "Accept": "*/*", "Accept-Encoding": "gzip", "User-Agent": "quart", } ), "", "2", http_scope, send_push_promise=_push, ) await request.send_push_promise("/") assert push_promise[0] == "/" valid_headers = {"Accept": "*/*", "Accept-Encoding": "gzip", "User-Agent": "quart"} assert len(push_promise[1]) == len(valid_headers) for name, value in valid_headers.items(): assert push_promise[1][name] == value pallets-quart-6b318bd/tests/wrappers/test_response.py000066400000000000000000000146271524460054700232200ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncGenerator from datetime import datetime from datetime import UTC from http import HTTPStatus from io import BytesIO from pathlib import Path from typing import Any import pytest from hypothesis import given from hypothesis import strategies as strategies from werkzeug.datastructures import Headers from werkzeug.exceptions import RequestedRangeNotSatisfiable from quart.testing import no_op_push from quart.typing import HTTPScope from quart.wrappers import Request from quart.wrappers.response import DataBody from quart.wrappers.response import FileBody from quart.wrappers.response import IOBody from quart.wrappers.response import IterableBody from quart.wrappers.response import Response async def test_data_wrapper() -> None: wrapper = DataBody(b"abcdef") results = [] async with wrapper as response: async for data in response: results.append(data) assert results == [b"abcdef"] async def _simple_async_generator() -> AsyncGenerator[bytes]: yield b"abc" yield b"def" @pytest.mark.parametrize( "iterable", [[b"abc", b"def"], (data for data in [b"abc", b"def"]), _simple_async_generator()], ) async def test_iterable_wrapper(iterable: Any) -> None: wrapper = IterableBody(iterable) results = [] async with wrapper as response: async for data in response: results.append(data) assert results == [b"abc", b"def"] async def test_file_wrapper(tmp_path: Path) -> None: file_ = tmp_path / "file_wrapper" file_.write_text("abcdef") wrapper = FileBody(Path(file_), buffer_size=3) results = [] async with wrapper as response: async for data in response: results.append(data) assert results == [b"abc", b"def"] async def test_io_wrapper() -> None: wrapper = IOBody(BytesIO(b"abcdef"), buffer_size=3) results = [] async with wrapper as response: async for data in response: results.append(data) assert results == [b"abc", b"def"] @pytest.mark.parametrize( "status, expected", [(201, 201), (None, 200), (HTTPStatus.PARTIAL_CONTENT, 206)] ) def test_response_status(status: Any, expected: int) -> None: response = Response(b"Body", status=status) assert response.status_code == expected async def test_response_body() -> None: response = Response(b"Body") assert b"Body" == (await response.get_data()) # Fetch again to ensure it isn't exhausted assert b"Body" == (await response.get_data()) async def test_response_make_conditional(http_scope: HTTPScope) -> None: request = Request( "GET", "https", "/", b"", Headers([("Range", "bytes=0-3")]), "", "1.1", http_scope, send_push_promise=no_op_push, ) response = Response(b"abcdef") await response.make_conditional(request, accept_ranges=True, complete_length=6) assert b"abcd" == (await response.get_data()) assert response.status_code == 206 assert response.accept_ranges == "bytes" assert response.content_range.units == "bytes" assert response.content_range.start == 0 assert response.content_range.stop == 4 assert response.content_range.length == 6 async def test_response_make_conditional_no_condition(http_scope: HTTPScope) -> None: request = Request( "GET", "https", "/", b"", Headers(), "", "1.1", http_scope, send_push_promise=no_op_push, ) response = Response(b"abcdef") await response.make_conditional(request, complete_length=6) assert b"abcdef" == (await response.get_data()) assert response.status_code == 200 async def test_response_make_conditional_out_of_bound(http_scope: HTTPScope) -> None: request = Request( "GET", "https", "/", b"", Headers([("Range", "bytes=0-666")]), "", "1.1", http_scope, send_push_promise=no_op_push, ) response = Response(b"abcdef") await response.make_conditional(request, complete_length=6) assert b"abcdef" == (await response.get_data()) assert response.status_code == 206 async def test_response_make_conditional_not_modified(http_scope: HTTPScope) -> None: response = Response(b"abcdef") await response.add_etag() request = Request( "GET", "https", "/", b"", Headers([("If-None-Match", response.get_etag()[0])]), "", "1.1", http_scope, send_push_promise=no_op_push, ) await response.make_conditional(request) assert response.status_code == 304 assert b"" == (await response.get_data()) assert "content-length" not in response.headers @pytest.mark.parametrize( "range_", ["second=0-3", "bytes=0-2,3-5", "bytes=8-16"], ) async def test_response_make_conditional_not_satisfiable( range_: str, http_scope: HTTPScope ) -> None: request = Request( "GET", "https", "/", b"", Headers([("Range", range_)]), "", "1.1", http_scope, send_push_promise=no_op_push, ) response = Response(b"abcdef") with pytest.raises(RequestedRangeNotSatisfiable): await response.make_conditional(request, complete_length=6) def test_response_cache_control() -> None: response = Response(b"Body") response.cache_control.max_age = 2 assert response.headers["Cache-Control"] == "max-age=2" response.cache_control.no_cache = True assert response.headers["Cache-Control"] == "max-age=2, no-cache" async def test_empty_response() -> None: response = Response() assert b"" == (await response.get_data()) @given( value=strategies.datetimes( timezones=strategies.just(UTC), # The min_value and max_value are needed because # wsgiref uses the function time.gmtime on the generated timestamps, # which fails on windows with values outside of these bounds min_value=datetime(1970, 1, 2), max_value=datetime(3000, 1, 2), ) ) @pytest.mark.parametrize("header", ["date", "expires", "last_modified", "retry_after"]) def test_datetime_headers(header: str, value: datetime) -> None: response = Response(b"Body") value = value.replace(microsecond=0) setattr(response, header, value) assert response.headers.get(header.title().replace("_", "-")) assert getattr(response, header) == value pallets-quart-6b318bd/uv.lock000066400000000000000000010267561524460054700162570ustar00rootroot00000000000000version = 1 revision = 2 requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.15'", "python_full_version < '3.15'", ] [[package]] name = "accessible-pygments" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, ] [[package]] name = "aiofiles" version = "25.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } 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.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] name = "ast-serialize" version = "0.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/34/16/6e520b57cd8c75914b38c670ad4593d13c22911e4306cc7165dab8b0789b/ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7", size = 863924, upload-time = "2026-08-07T11:27:34.579Z" }, { url = "https://files.pythonhosted.org/packages/03/e1/48802de9b22a2bcad42ec80601a17e3f69172fe4f590e6311bcc2b323aeb/ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119", size = 1177662, upload-time = "2026-08-07T11:27:36.196Z" }, { url = "https://files.pythonhosted.org/packages/38/d4/323438db76bded3a1f3523a3167b8325916b2ddceb2107a330c6ec9fcf4d/ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725", size = 1167072, upload-time = "2026-08-07T11:27:37.646Z" }, { url = "https://files.pythonhosted.org/packages/77/82/53c5400b54144b56de8ed7f957fd1ccd97e42482009292ab46121d15f8dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a", size = 1225497, upload-time = "2026-08-07T11:27:39.225Z" }, { url = "https://files.pythonhosted.org/packages/44/5f/36c07327a8b91303fbf1382c7c3e8a2902072dbe1b9546138a5288e75ff0/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980", size = 1227101, upload-time = "2026-08-07T11:27:40.799Z" }, { url = "https://files.pythonhosted.org/packages/9d/48/5adf5c67addc7ddb328122208c6d375a84cf154984f412b4087330a157bd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19", size = 1424001, upload-time = "2026-08-07T11:27:42.708Z" }, { url = "https://files.pythonhosted.org/packages/38/a1/70074dd3869d2b0e934f91891d8d6b734361cd3b80f85ca7ece2e668ecdd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea", size = 1245545, upload-time = "2026-08-07T11:27:44.4Z" }, { url = "https://files.pythonhosted.org/packages/e3/be/53b9c0a8a6399950c2e3546bdfab96d2b299d5b114b47eb94fd3c49c4054/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951", size = 1248961, upload-time = "2026-08-07T11:27:45.781Z" }, { url = "https://files.pythonhosted.org/packages/eb/13/3651d3812548a2bda15e26e5dd51aadb48cf682d0865370255fcf0e367dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9", size = 1243877, upload-time = "2026-08-07T11:27:47.325Z" }, { url = "https://files.pythonhosted.org/packages/21/a0/521f0bf000f675e9312a4aae2c8ba7a992405d072a85c485e08fd59433b9/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3", size = 1293903, upload-time = "2026-08-07T11:27:49.264Z" }, { url = "https://files.pythonhosted.org/packages/b1/7e/402fc902568aa2ee65865a3e151f000db0153da8ce6b1be4c9c349025f8d/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a", size = 1401070, upload-time = "2026-08-07T11:27:50.947Z" }, { url = "https://files.pythonhosted.org/packages/ff/7c/97d4b66c057f1706fc8be6dd532cc77c988794357c8f4ffdb6adabb39562/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53", size = 1502602, upload-time = "2026-08-07T11:27:52.364Z" }, { url = "https://files.pythonhosted.org/packages/89/6f/72cc3b71562001bba46e898ccfbf1844f7939b3e28912736206102f2e5a8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b", size = 1495848, upload-time = "2026-08-07T11:27:53.803Z" }, { url = "https://files.pythonhosted.org/packages/a0/53/d6f629d1e49308b2f363dae028baa213ec222c9106fa1f7f0d1f7b41499a/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14", size = 1556556, upload-time = "2026-08-07T11:27:55.342Z" }, { url = "https://files.pythonhosted.org/packages/ee/22/340f35dd8dfc6d412d53dc20699ca014b8d228db923e8ed4759c512b162c/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a", size = 1417822, upload-time = "2026-08-07T11:27:56.712Z" }, { url = "https://files.pythonhosted.org/packages/11/29/6dde5c13fbebc051d3a6df4ec0a6fd1d5359333cc1193f7f609f3410b4d8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda", size = 1445153, upload-time = "2026-08-07T11:27:58.275Z" }, { url = "https://files.pythonhosted.org/packages/62/c5/f473a8ed030f7a0ca24b9849cca184677a50c053867a7b808c2e1289bbd3/ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827", size = 1063711, upload-time = "2026-08-07T11:27:59.793Z" }, { url = "https://files.pythonhosted.org/packages/23/63/39e171fcd38ca057c2e1979d5ee81ac7a3502784abe3d83df7454f7a0978/ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6", size = 1103740, upload-time = "2026-08-07T11:28:01.405Z" }, { url = "https://files.pythonhosted.org/packages/21/1c/d00762b399e7726d68d0a088cc946e3a4c60f1c6176f557608f672f627f3/ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f", size = 1076021, upload-time = "2026-08-07T11:28:02.947Z" }, { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" }, { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" }, { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" }, { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" }, { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" }, { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" }, { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" }, { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" }, { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" }, { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" }, { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" }, { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" }, { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" }, { url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" }, { url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" }, { url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" }, { url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" }, { url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" }, { url = "https://files.pythonhosted.org/packages/d1/5d/c650b1f2cc1e75193358da95a080261422e8cd10b66d7370b1688c9915c5/ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5", size = 852914, upload-time = "2026-08-07T11:28:32.929Z" }, { url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" }, { url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" }, { url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" }, { url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" }, { url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" }, { url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" }, { url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" }, { url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" }, { url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" }, { url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" }, { url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" }, { url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" }, { url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" }, { url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" }, { url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" }, { url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" }, { url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" }, { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" }, ] [[package]] name = "babel" version = "2.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "beautifulsoup4" version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, ] [[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 = "7.1.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/70/d2/47e8bc06fe2a06d3f5bdf20f1126ab66c4e99dc48d940e7ba873f7ac7131/cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50", size = 40680, upload-time = "2026-08-01T21:20:40.434Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/d8/767faeda872075724b95dd675466a645f1b92aadcdcf2d1429dcfd76c176/cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0", size = 16830, upload-time = "2026-08-01T21:20:38.977Z" }, ] [[package]] name = "certifi" version = "2026.7.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] name = "cfgv" version = "3.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "charset-normalizer" version = "3.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] name = "click" version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[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 = "coverage" version = "7.15.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, ] [[package]] name = "distlib" version = "0.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, ] [[package]] name = "docutils" version = "0.22.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] [[package]] name = "filelock" version = "3.32.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6d/30/03b03951873a1a0ffc7e8ca0e10c15597b59e8d0e39260704cd2ea087bc4/filelock-3.32.4.tar.gz", hash = "sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30", size = 222126, upload-time = "2026-08-23T17:37:55.363Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/01/a4/9b63d595d748e3aff8812b65eacc1a2c4bd90b7c2012e08e72373b4835eb/filelock-3.32.4-py3-none-any.whl", hash = "sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd", size = 99864, upload-time = "2026-08-23T17:37:53.913Z" }, ] [[package]] name = "flask" version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, { name = "click" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, { name = "werkzeug" }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] [[package]] name = "gha-update" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "httpx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/e8/eb710e08998a22b314cc068f14805cfdd12e3934a5496c8916c5a164c65a/gha_update-0.2.0.tar.gz", hash = "sha256:328ee0db09346ad13ee90646698cea2ec1f9035964ddd7c2a728a91034c3f4b0", size = 4756, upload-time = "2025-07-14T03:13:33.254Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4b/29/a0e42b0b80d614aa82929f65cce0d51443eea296802b2094cadc5660321b/gha_update-0.2.0-py3-none-any.whl", hash = "sha256:ec5641bf23f71baa1232fc61b3059fb08456e1b78150d1e9c1bab69b37046e49", size = 5323, upload-time = "2025-07-14T03:13:31.932Z" }, ] [[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 = "h2" version = "4.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hpack" }, { name = "hyperframe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" }, ] [[package]] name = "hpack" version = "4.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, ] [[package]] name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] 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" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] 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 = "hypercorn" version = "0.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, { name = "h2" }, { name = "priority" }, { name = "wsproto" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/01/39f41a014b83dd5c795217362f2ca9071cf243e6a75bdcd6cd5b944658cc/hypercorn-0.18.0.tar.gz", hash = "sha256:d63267548939c46b0247dc8e5b45a9947590e35e64ee73a23c074aa3cf88e9da", size = 68420, upload-time = "2025-11-08T13:54:04.78Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/35/850277d1b17b206bd10874c8a9a3f52e059452fb49bb0d22cbb908f6038b/hypercorn-0.18.0-py3-none-any.whl", hash = "sha256:225e268f2c1c2f28f6d8f6db8f40cb8c992963610c5725e13ccfcddccb24b1cd", size = 61640, upload-time = "2025-11-08T13:54:03.202Z" }, ] [[package]] name = "hyperframe" version = "6.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, ] [[package]] name = "hypothesis" version = "6.165.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/e2/0fad246d2b6330e1f78479bfc566b5c22be82aee8a865cde9a08f648487d/hypothesis-6.165.10.tar.gz", hash = "sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32", size = 503703, upload-time = "2026-08-16T22:56:15.404Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/05/c1/9a9538e6d185baf5cc7f15bc3b76e08efbb3de4b3c782f234356449c0dd7/hypothesis-6.165.10-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f839d29d0cc12048cf073d88ca4fdf94d420bc2b8afd69641ff6d496422ccd4f", size = 783243, upload-time = "2026-08-16T22:55:44.058Z" }, { url = "https://files.pythonhosted.org/packages/a1/30/b70d9d79e871a75cbdeccd9067f20ecdb9eb2a1dfa03c630be3ad13b8b30/hypothesis-6.165.10-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e10858f57ed0e74baa04393845f469fe8ad502c16ece4499bef7700c575611bd", size = 778815, upload-time = "2026-08-16T22:55:46.948Z" }, { url = "https://files.pythonhosted.org/packages/db/52/6f0a9b7aab24b0635e2238f3fbddea5b54b17879ac813df42a3cc3384c5c/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5", size = 1108009, upload-time = "2026-08-16T22:54:53.082Z" }, { url = "https://files.pythonhosted.org/packages/f6/06/8d0d4e11ff02350d09ec9f9e90af354158e59e16a8907ba5199a4ff2d7e8/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1", size = 1136596, upload-time = "2026-08-16T22:54:54.443Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/01a1e440f2e38dc1ccf5d597af5b8a0bee5f21b674c99c123b5554de9690/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52", size = 1135234, upload-time = "2026-08-16T22:55:08.911Z" }, { url = "https://files.pythonhosted.org/packages/7d/18/8a26c24d3d9db20265f39df341ab265858c094e209571e3179cf237935f4/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f", size = 1157528, upload-time = "2026-08-16T22:56:02.159Z" }, { url = "https://files.pythonhosted.org/packages/ea/8e/ce3c829b1937402d7944420ca26a05a0c8563e894dcff03d34ffa279d306/hypothesis-6.165.10-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb", size = 1112870, upload-time = "2026-08-16T22:54:55.919Z" }, { url = "https://files.pythonhosted.org/packages/f2/1b/4c4926d6c9a2b5d7cc090cc1e91219d6796102aa2a2c4b8f961c939e60b5/hypothesis-6.165.10-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb", size = 1149683, upload-time = "2026-08-16T22:55:30.567Z" }, { url = "https://files.pythonhosted.org/packages/cb/f9/df24eb28412f82465e2b7707f0ff1ec274d580bce389d4d9156617dc7bba/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e", size = 1283402, upload-time = "2026-08-16T22:54:18.054Z" }, { url = "https://files.pythonhosted.org/packages/4d/07/c2b2a761300cf60b90ccebba4328175331e67d34f4fbd39429a7ddcdce49/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef", size = 1409948, upload-time = "2026-08-16T22:54:22.343Z" }, { url = "https://files.pythonhosted.org/packages/f4/ec/1c2bf1acdd0e273d81f833f85caf0ae5423db68a783554992fca36e6c541/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143", size = 1265023, upload-time = "2026-08-16T22:54:41.402Z" }, { url = "https://files.pythonhosted.org/packages/3d/a8/7f984908b7391160c7801b84e51ca8e4ba88c89e8d8811aa1aa7c03de73c/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c", size = 1282698, upload-time = "2026-08-16T22:56:06.998Z" }, { url = "https://files.pythonhosted.org/packages/48/78/3a5d91c2d0250521736c42dfa2402b75049bc5fe2fb716c10bc84bb91ed1/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7", size = 1324816, upload-time = "2026-08-16T22:54:46.675Z" }, { url = "https://files.pythonhosted.org/packages/6f/99/27450763853a034bca1574d3e0a315164b33ff49c3862df6872dda45e25e/hypothesis-6.165.10-cp310-abi3-win32.whl", hash = "sha256:b33dc30170a7402e03c180f2c5ef69dc077152f35b91621e9cebcde9c7d71746", size = 669039, upload-time = "2026-08-16T22:55:11.962Z" }, { url = "https://files.pythonhosted.org/packages/2c/fc/ff2988b72b5705ad9ca500444bf3f43e3c2f41edfa034bbfeb23b215791a/hypothesis-6.165.10-cp310-abi3-win_amd64.whl", hash = "sha256:e9f924aa610c0618445e1e8738c822c3190ce2a2699a0cb48ec3a351a96761f2", size = 675213, upload-time = "2026-08-16T22:55:01.697Z" }, { url = "https://files.pythonhosted.org/packages/c5/8b/821810d36f78d9d9421cd2c5d9d36983b45bb3575c3086276cc5c76f9f73/hypothesis-6.165.10-cp310-abi3-win_arm64.whl", hash = "sha256:1d305448e9bd8e2f4f3cea0eafd809efdaab4e998a0019bc615650c8463e42f1", size = 673537, upload-time = "2026-08-16T22:54:47.898Z" }, { url = "https://files.pythonhosted.org/packages/b1/fb/c82c5bd92864ffcf319772fedc8c9bf2dbe4ca14baa0fee6e49e67b5ba1c/hypothesis-6.165.10-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9d77c3be7b429875036ad0f0597c6e5cc6bb17894a4da005e3807de64d2673ad", size = 784726, upload-time = "2026-08-16T22:54:32.371Z" }, { url = "https://files.pythonhosted.org/packages/0e/b9/3d7acd08506da85557e65147b7f3fca8c47684e33be90bee0acb523920db/hypothesis-6.165.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:490c56b830772b0eca3b4b2cecb3741a1ed26b1d7206a279e1525dbf0aa95ee4", size = 776375, upload-time = "2026-08-16T22:55:13.303Z" }, { url = "https://files.pythonhosted.org/packages/38/6b/922e8b3f9a706dd89d440b9545d2c6231c65e74da1c1fee3ff36c251b9c4/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d", size = 1106763, upload-time = "2026-08-16T22:55:06.129Z" }, { url = "https://files.pythonhosted.org/packages/01/39/f5b9a5d390d4edd1ad472334493ac442963ebeb4daaa74ff4bdac6ef292f/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620", size = 1156778, upload-time = "2026-08-16T22:54:33.824Z" }, { url = "https://files.pythonhosted.org/packages/b5/5f/5fbe1be4326337fd6acefe2d18ed44007ee1dc1f98fe5b3c0eb22942364d/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb", size = 1280756, upload-time = "2026-08-16T22:55:54.834Z" }, { url = "https://files.pythonhosted.org/packages/25/c0/cf6f9e1ef632a1a75694eed0db3a02e6fc75c367a363e94acee52f043c64/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96", size = 1323889, upload-time = "2026-08-16T22:55:56.567Z" }, { url = "https://files.pythonhosted.org/packages/cc/cc/662b94880f260b0a88de1fdcf60fc9984f6e2a796da549542adc10a7bc83/hypothesis-6.165.10-cp313-cp313-win_amd64.whl", hash = "sha256:c01dd04044c472e47193b54f68e84e08d6ebf4f29551885aa959b015f7cd9747", size = 672346, upload-time = "2026-08-16T22:56:03.792Z" }, { url = "https://files.pythonhosted.org/packages/3f/77/55e020c9c576532ff7d20bf8b1dfa052ecbd5ada1949b02f76c44c966f7e/hypothesis-6.165.10-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9ccac776b2ca93b324806facd526ccb45da0fd035001c899a35b02c44431e209", size = 784833, upload-time = "2026-08-16T22:55:21.255Z" }, { url = "https://files.pythonhosted.org/packages/4f/f2/01da2adf829cf549eaddcabb8e8072077fb3d26da4275f4c1e89b2c0af74/hypothesis-6.165.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e5f95f7b622e4171096d92175dda0a560f0955ade9b8a3a07bdcf151f7359611", size = 776545, upload-time = "2026-08-16T22:56:10.159Z" }, { url = "https://files.pythonhosted.org/packages/cf/8e/58d4f842895220b793c53fc94a6489705b3665bb4d0ae4d338ce03fdf9fb/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e", size = 1107271, upload-time = "2026-08-16T22:54:50.266Z" }, { url = "https://files.pythonhosted.org/packages/8f/b8/206468912d2153306bb8a41afdfc59e45b7a73a0495bbe4b9cb4f0e79c1d/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191", size = 1156915, upload-time = "2026-08-16T22:54:25.89Z" }, { url = "https://files.pythonhosted.org/packages/fb/d3/bf5a22929b70a4cfd3edf69c5642b029b27ddb5cfda48fa295d384b01abb/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424", size = 1281205, upload-time = "2026-08-16T22:54:44.083Z" }, { url = "https://files.pythonhosted.org/packages/07/a2/d7b2ba444d36fc84d4779f4431e74dd9b023dc63bcf282199f6e48ad39f4/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889", size = 1324243, upload-time = "2026-08-16T22:55:41.123Z" }, { url = "https://files.pythonhosted.org/packages/d1/95/afe6b531fd01928c6f63d394ee413fa2338d088b2b44efcc23596b54477e/hypothesis-6.165.10-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063", size = 616382, upload-time = "2026-08-16T22:55:18.449Z" }, { url = "https://files.pythonhosted.org/packages/48/86/9b4fb75f520a028edec50ffc904a94d724180395d71feb6d7a0ce7bb6f00/hypothesis-6.165.10-cp314-cp314-win_amd64.whl", hash = "sha256:d1ea02fa8ab3d33eb1125eade81f7136341eb429152c6dbe2ae6f8bc33b3fbdd", size = 672145, upload-time = "2026-08-16T22:54:24.831Z" }, { url = "https://files.pythonhosted.org/packages/f9/ba/f7bbaae0c789bab7ddb764d2056ee1a463cc95a8acbccc90d4184e48b242/hypothesis-6.165.10-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ed1a5891e59472884a03cb9875483e8fc131c80a275c60967f8afc5458a0c8ff", size = 783287, upload-time = "2026-08-16T22:54:23.751Z" }, { url = "https://files.pythonhosted.org/packages/3a/83/01ef80772b4abd335c49405576dc503cede94fb5da30ba2643a119013aea/hypothesis-6.165.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:09772e328a26e50486ac572be34f9887f9aa185efe7ebb16bde4e8f6038db1f4", size = 774991, upload-time = "2026-08-16T22:55:25.987Z" }, { url = "https://files.pythonhosted.org/packages/a3/0b/f47506241f9d5a5a2efe4c65b6bf4830e9d9576e5d3779007a260699e608/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413", size = 1105499, upload-time = "2026-08-16T22:54:51.864Z" }, { url = "https://files.pythonhosted.org/packages/84/fe/abb3909b7089835112fbe75bf00d817d733b3a8032759783db0a24ff1e56/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3", size = 1155685, upload-time = "2026-08-16T22:54:30.94Z" }, { url = "https://files.pythonhosted.org/packages/73/2f/1964738921640184067121ae77414522fc3f0463fc26c6e25a4f3b8e42ca/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647", size = 1279177, upload-time = "2026-08-16T22:54:40.179Z" }, { url = "https://files.pythonhosted.org/packages/34/c5/312af8ae038d3af9cf3f7f1021c1abfe31c0d9035e4cf63519e0a7dc983e/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3", size = 1322921, upload-time = "2026-08-16T22:54:42.7Z" }, { url = "https://files.pythonhosted.org/packages/9e/e7/b0a2fde7570c090a1b914026266a421c751ef10138fffe37fe0ef9e675c0/hypothesis-6.165.10-cp314-cp314t-win_amd64.whl", hash = "sha256:cc2da5aa4edf14743fa9257e5ba3513963999f01211635702479d8e92b8207c8", size = 672147, upload-time = "2026-08-16T22:55:27.527Z" }, { url = "https://files.pythonhosted.org/packages/47/fd/985aa564d6ffd06483d45a62b40d319df0a703cd8bc1d041de17d102fbaa/hypothesis-6.165.10-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:eeab73050ea58c13dd56e329f594c1dfe32ebd7bb169bbdf4f8ceefbc31ec6b5", size = 782882, upload-time = "2026-08-16T22:55:37.93Z" }, { url = "https://files.pythonhosted.org/packages/f8/2c/6cc11151e450f72353a490940cd0db704680d07b78dc75dcc9f480e0d0e1/hypothesis-6.165.10-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:4c68e983d0007d014bb01ad4bcbba78bc432c73a1755ff36d5102ceefa18299a", size = 774584, upload-time = "2026-08-16T22:55:51.822Z" }, { url = "https://files.pythonhosted.org/packages/10/39/ef26fa79c1738dfe9cdb1a3584fb6717d26429ca6c9d011cc4fdf08130c2/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0", size = 1104876, upload-time = "2026-08-16T22:54:58.937Z" }, { url = "https://files.pythonhosted.org/packages/4e/f4/3fcc84e7637f42bf00d987093b9418083ac8db81b87392608a60f4b7c5fd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558", size = 1133353, upload-time = "2026-08-16T22:54:28.635Z" }, { url = "https://files.pythonhosted.org/packages/35/59/21c5c14179c38f8d0de3560e7f1825c083311b3013b63f817d7dc78dfcbd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8", size = 1132300, upload-time = "2026-08-16T22:56:08.539Z" }, { url = "https://files.pythonhosted.org/packages/14/af/fbb56059961e416b2de7b9dc5352db2e8572bd5ea46892957e4c1e5548ab/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245", size = 1155175, upload-time = "2026-08-16T22:55:19.824Z" }, { url = "https://files.pythonhosted.org/packages/0f/53/77fb0c2dad445858555429c4e06cf94a59ae8d2407dd6426b5af97c84828/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f", size = 1109881, upload-time = "2026-08-16T22:55:32.029Z" }, { url = "https://files.pythonhosted.org/packages/a8/7b/d187f673ff30e6ada640953636f978ffe64a6332f756b64163c2277f8d0c/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2", size = 1144963, upload-time = "2026-08-16T22:56:13.428Z" }, { url = "https://files.pythonhosted.org/packages/e0/60/31d504e364134d60af23e5f6365db0da3cf4a51b3ed3d4836e5a2cff12cf/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48", size = 1278684, upload-time = "2026-08-16T22:55:22.971Z" }, { url = "https://files.pythonhosted.org/packages/ef/e6/89d26834a08c02f8da149e541dd40d7a96f68d9722f43146e69a77436ed7/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c", size = 1407202, upload-time = "2026-08-16T22:55:14.949Z" }, { url = "https://files.pythonhosted.org/packages/dc/61/20d1e72246867ea195440092e8bb422c7ddc2f271b87b5b65679d5532719/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3", size = 1261395, upload-time = "2026-08-16T22:56:05.448Z" }, { url = "https://files.pythonhosted.org/packages/2a/9b/ebab6c3c2b90a16abb4119198178652d12aff83cc8ec2cfde5276c69fb1e/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe", size = 1279213, upload-time = "2026-08-16T22:55:35.066Z" }, { url = "https://files.pythonhosted.org/packages/23/78/69b219b524231d36eb20c792e1f01e7cb037e02bd0af1c29f77ed9a969c0/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d", size = 1322367, upload-time = "2026-08-16T22:54:21.279Z" }, { url = "https://files.pythonhosted.org/packages/55/63/ad5cc153dcc72ae5e7905fb9b3585f3e48ce892a2d6366f90163e867a69d/hypothesis-6.165.10-cp315-abi3.abi3t-win32.whl", hash = "sha256:c6559380469295c4009215fe1cab561301591a3bee2e2fb3f4f96d2273a3affc", size = 666038, upload-time = "2026-08-16T22:56:11.797Z" }, { url = "https://files.pythonhosted.org/packages/80/32/b62307b73fbc99f0a4381d6f9456df76fbcbb7a27ef7256e26f0376f48ea/hypothesis-6.165.10-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:30797f20ca45e57f526d2df872f63ba453cb4e1091ad542184a7a951af8da79d", size = 671941, upload-time = "2026-08-16T22:55:00.235Z" }, { url = "https://files.pythonhosted.org/packages/c2/dd/e0f98add0548ef73ea7afac45da1fb8efc854d7f9931db568754d0f963f3/hypothesis-6.165.10-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:c53e9b1c36350df9965ec44d6c0d4e0bbbb38f720dd2b0e1256dc6524d411015", size = 669931, upload-time = "2026-08-16T22:55:50.205Z" }, ] [[package]] name = "identify" version = "2.6.19" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, ] [[package]] name = "idna" version = "3.19" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] name = "imagesize" version = "2.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fb/5e/513ff06670c84e7b9887c1fdf61b2d42b4f574a831f2f1d2222023049d8a/imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5", size = 1883774, upload-time = "2026-08-24T12:35:19.13Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[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 = "librt" version = "0.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, ] [[package]] name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "mdit-py-plugins" version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mypy" version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ast-serialize" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" }, { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" }, { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" }, { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" }, { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" }, { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" }, { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" }, { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" }, { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" }, { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" }, { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" }, { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" }, { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" }, { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" }, { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" }, { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" }, { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" }, { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" }, { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" }, { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" }, { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" }, { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" }, { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" }, { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" }, { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" }, { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" }, { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" }, { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" }, { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" }, { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" }, { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" }, { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" }, { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" }, ] [[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 = "myst-parser" version = "5.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "jinja2" }, { name = "markdown-it-py" }, { name = "mdit-py-plugins" }, { name = "pyyaml" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, ] [[package]] name = "nodeenv" version = "1.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "packaging" version = "26.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] name = "pathspec" version = "1.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "platformdirs" version = "4.11.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/50/bb/ebc6636e1ae41314f796ebb7215fd28febb45f9aac72f2b04cb74b5071dc/platformdirs-4.11.4.tar.gz", hash = "sha256:f3373be828247211d0febabea97e238c3dfde8a60b3c90c32756fb52cb21556d", size = 34079, upload-time = "2026-08-24T14:53:49.676Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/28/be/0ff05fcd2938fb58ad9219bd54135968342d214737e012d62d43f06a2dd6/platformdirs-4.11.4-py3-none-any.whl", hash = "sha256:e34ff91a24bcddc6d939b878bdf3f5c437c9c46fe9e212b1bf455fdf1ee57586", size = 23741, upload-time = "2026-08-24T14:53:48.406Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pre-commit" version = "4.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" }, ] [[package]] name = "pre-commit-uv" version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pre-commit" }, { name = "uv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8f/4f/c60d3549065b1d6d9242e81d37910a824bfb2a1df12e0a79c928d81885c4/pre_commit_uv-4.3.0.tar.gz", hash = "sha256:7a46537c0c2cd346dc6f3b6d64c7c45250189d5e3d5a17f626ec3e3b2503bf46", size = 7473, upload-time = "2026-07-21T13:09:43.33Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/8b/86164aa971db50b752948756417b2e039f42721114afa2a9972543b7186b/pre_commit_uv-4.3.0-py3-none-any.whl", hash = "sha256:04cd06f2402e21ab48f2560ebea6eea03fbc46d428af8ceb862060b047f91f42", size = 6210, upload-time = "2026-07-21T13:09:42.382Z" }, ] [[package]] name = "priority" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f5/3c/eb7c35f4dcede96fca1842dac5f4f5d15511aa4b52f3a961219e68ae9204/priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0", size = 24792, upload-time = "2021-06-27T10:15:05.487Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", size = 8946, upload-time = "2021-06-27T10:15:03.856Z" }, ] [[package]] name = "pydata-sphinx-theme" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accessible-pygments" }, { name = "babel" }, { name = "beautifulsoup4" }, { name = "docutils" }, { name = "jinja2" }, { name = "pygments" }, { name = "requests" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/8e/add936feaaa9dade7d5b87c6852566d85e518b38ad32224dea32ef958984/pydata_sphinx_theme-0.20.0.tar.gz", hash = "sha256:0da172d41e19a66de875f4002f7054b385372ec65763852193791e658d50bb4a", size = 5004756, upload-time = "2026-07-09T09:09:14.693Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/80/08/28e2194ed1c3c3a3e86e0600e2dcc7f21dcd670bd31f3efab2e3e2cf0cbd/pydata_sphinx_theme-0.20.0-py3-none-any.whl", hash = "sha256:56744483c9d72c783e075de716ab95d486108b69605df7528078090b73f11f69", size = 6201166, upload-time = "2026-07-09T09:09:12.899Z" }, ] [[package]] name = "pygments" version = "2.21.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] name = "pyproject-api" version = "1.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ab/bd/2f985c12bf33fdd8637e3a8f9418d6806f177601dee7c4924d0b2bb28650/pyproject_api-1.11.0.tar.gz", hash = "sha256:b8807d85a293e6c9f133e6575946fed45f1d42b22d58c780b33aa2421a799549", size = 23787, upload-time = "2026-07-21T13:09:33.744Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a3/6a/2822ee12bafe87af8f6cc620f7d1f126e77f82ce56ba888b94a9af5a979c/pyproject_api-1.11.0-py3-none-any.whl", hash = "sha256:860060c8832dce983b5eec6f41c4c43eb3ec06ff7332387a63acdf5ca27b68d8", size = 13275, upload-time = "2026-07-21T13:09:32.559Z" }, ] [[package]] name = "pyright" version = "1.1.411" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, ] [[package]] name = "pytest" version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "pytest-asyncio" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]] name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] name = "pytest-sugar" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "termcolor" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/4e/60fed105549297ba1a700e1ea7b828044842ea27d72c898990510b79b0e2/pytest-sugar-1.1.1.tar.gz", hash = "sha256:73b8b65163ebf10f9f671efab9eed3d56f20d2ca68bda83fa64740a92c08f65d", size = 16533, upload-time = "2025-08-23T12:19:35.737Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/87/d5/81d38a91c1fdafb6711f053f5a9b92ff788013b19821257c2c38c1e132df/pytest_sugar-1.1.1-py3-none-any.whl", hash = "sha256:2f8319b907548d5b9d03a171515c1d43d2e38e32bd8182a1781eb20b43344cc8", size = 11440, upload-time = "2025-08-23T12:19:34.894Z" }, ] [[package]] name = "python-discovery" version = "1.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/8f/3c92c45737f654f2488ab3662b7604a55d3d35146d37c9ce80f5c95b95a6/python_discovery-1.5.3.tar.gz", hash = "sha256:e500eb24025fb7c4876c1fdcfbafd9028a10c71b661aee38cb6fb0de594518c1", size = 82477, upload-time = "2026-08-24T14:48:46.396Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/30/12/823d9a321904ccfd2969a24b84fdfd1e6614c707ec569c62879bf1dbc6c5/python_discovery-1.5.3-py3-none-any.whl", hash = "sha256:8305296358f1aa2ed302a25b84be7df84fef8ca47c7dce2da63cb7325333044e", size = 38290, upload-time = "2026-08-24T14:48:45.305Z" }, ] [[package]] name = "python-dotenv" version = "1.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "quart" version = "0.23.0" source = { editable = "." } dependencies = [ { name = "aiofiles" }, { name = "blinker" }, { name = "click" }, { name = "flask" }, { name = "hypercorn" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, { name = "werkzeug" }, ] [package.optional-dependencies] dotenv = [ { name = "python-dotenv" }, ] [package.dev-dependencies] dev = [ { name = "ruff" }, { name = "tox" }, { name = "tox-uv" }, ] docs = [ { name = "myst-parser" }, { name = "pydata-sphinx-theme" }, { name = "sphinx" }, ] docs-auto = [ { name = "sphinx-autobuild" }, ] gha-update = [ { name = "gha-update" }, ] pre-commit = [ { name = "pre-commit" }, { name = "pre-commit-uv" }, ] tests = [ { name = "hypothesis" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-sugar" }, { name = "python-dotenv" }, ] typing = [ { name = "mypy" }, { name = "pyright" }, { name = "pytest" }, { name = "types-aiofiles" }, ] [package.metadata] requires-dist = [ { name = "aiofiles" }, { name = "blinker", specifier = ">=1.6" }, { name = "click", specifier = ">=8.0" }, { name = "flask", specifier = ">=3.0" }, { name = "hypercorn", specifier = ">=0.11.2" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, { name = "python-dotenv", marker = "extra == 'dotenv'" }, { name = "werkzeug", specifier = ">=3.0" }, ] provides-extras = ["dotenv"] [package.metadata.requires-dev] dev = [ { name = "ruff" }, { name = "tox" }, { name = "tox-uv" }, ] docs = [ { name = "myst-parser" }, { name = "pydata-sphinx-theme" }, { name = "sphinx" }, ] 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 = "hypothesis" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-sugar" }, { name = "python-dotenv" }, ] typing = [ { name = "mypy" }, { name = "pyright" }, { name = "pytest" }, { name = "types-aiofiles" }, ] [[package]] name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] name = "roman-numerals" version = "4.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] [[package]] name = "ruff" version = "0.16.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" }, { url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" }, { url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" }, { url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" }, { url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" }, { url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" }, { url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" }, { url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" }, { url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" }, { url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" }, { url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" }, { url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" }, { url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" }, { url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" }, { url = "https://files.pythonhosted.org/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e", size = 10021687, upload-time = "2026-08-20T17:43:52.281Z" }, { url = "https://files.pythonhosted.org/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c", size = 10567657, upload-time = "2026-08-20T17:43:54.78Z" }, { url = "https://files.pythonhosted.org/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" }, ] [[package]] name = "snowballstemmer" version = "3.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, ] [[package]] name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "soupsieve" version = "2.9.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" }, ] [[package]] name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alabaster" }, { name = "babel" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "docutils" }, { name = "imagesize" }, { name = "jinja2" }, { name = "packaging" }, { name = "pygments" }, { name = "requests" }, { name = "roman-numerals" }, { name = "snowballstemmer" }, { name = "sphinxcontrib-applehelp" }, { name = "sphinxcontrib-devhelp" }, { name = "sphinxcontrib-htmlhelp" }, { name = "sphinxcontrib-jsmath" }, { name = "sphinxcontrib-qthelp" }, { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] [[package]] name = "sphinx-autobuild" version = "2025.8.25" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, { name = "sphinx" }, { name = "starlette" }, { name = "uvicorn" }, { name = "watchfiles" }, { name = "websockets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, ] [[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-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 = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, ] [[package]] name = "termcolor" version = "3.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, ] [[package]] name = "tomli-w" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] [[package]] name = "tox" version = "4.60.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "colorama" }, { name = "filelock" }, { name = "packaging" }, { name = "platformdirs" }, { name = "pluggy" }, { name = "pyproject-api" }, { name = "python-discovery" }, { name = "tomli-w" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ec/ae/05210c695efa7349af65aeb988604b15520eade3b6e2d43bdf44ce69b4b3/tox-4.60.1.tar.gz", hash = "sha256:2611b77ebe06fa571ae2f4176eaf644115d08891befb9c7cb705cb038d5ab38e", size = 303833, upload-time = "2026-08-25T05:39:00.934Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/d8/5ca808e8b2d98ac55e9141309c63b273caec25eb5bdaf3d7bf6d809903bc/tox-4.60.1-py3-none-any.whl", hash = "sha256:6f23a6417ad761b17c047e14b6e278968c09eb9b1fc00025269f82ec8327bc75", size = 226880, upload-time = "2026-08-25T05:38:59.194Z" }, ] [[package]] name = "tox-uv" version = "1.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tox-uv-bare" }, { name = "uv" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f9/d7/3fce976b9e295218a2d7e541e0f1e9b1259cb893de7a8d277952a66a798d/tox_uv-1.36.0-py3-none-any.whl", hash = "sha256:5f81b39be3fe4e14c6b9bb7ba637ed97d34efa6214efd5f525d95ee9559a99ac", size = 6564, upload-time = "2026-07-21T13:09:54.316Z" }, ] [[package]] name = "tox-uv-bare" version = "1.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tox" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/df/9f90a59de8c87cece6e691eee53dc1cb0a824d73aea8f380ae2f6ecb71de/tox_uv_bare-1.36.0.tar.gz", hash = "sha256:d9b0a2fd0f74fa65d9597108f8a0ef7abb08651c9196a998a6073b781b45cfd0", size = 32548, upload-time = "2026-07-21T13:09:56.435Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/0a/6dc462e4fb543305283a6157c80f43e3d12ca4702da6ae6521d541c6b55c/tox_uv_bare-1.36.0-py3-none-any.whl", hash = "sha256:ba397dd0396df95a75744d4e42a50ee27207c0ffcf277b62ffba9c3de455a939", size = 22489, upload-time = "2026-07-21T13:09:55.389Z" }, ] [[package]] name = "types-aiofiles" version = "25.1.0.20260518" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/df/42/f5b9b90162d2196f016b87228d6bf43f2c2c0c6501bfd5415001b3eb68bb/types_aiofiles-25.1.0.20260518.tar.gz", hash = "sha256:c0c95eb78755d4fa7b397d4f0332c632714dd7cd0d17f49b96e31d4d7a8d8c76", size = 14891, upload-time = "2026-05-18T06:05:27.804Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/3d/7a9ed9faafeae3aa3b5bc22fa5b979ff9cf3c83ecbe919b58eae07795b8c/types_aiofiles-25.1.0.20260518-py3-none-any.whl", hash = "sha256:f776bdfb4bec17f743d9ef042e61edf03bdcc7821fc08556fba9b63d873fdea9", size = 14377, upload-time = "2026-05-18T06:05:26.871Z" }, ] [[package]] name = "typing-extensions" version = "4.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "urllib3" version = "2.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "uv" version = "0.12.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7c/b0/3085b844fe59aa319a3f94a5cca9938fffecc82705aa9c2762a749f7095c/uv-0.12.5.tar.gz", hash = "sha256:442a21d181faae21742aaaf6d2091a0d27755d3eac344061a9a00c90169b7524", size = 7101936, upload-time = "2026-08-14T19:56:57.693Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b8/4c/6412d4a618230db699118b362ec41c54795f93992b43c53e225bd0213501/uv-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:2bd62134e56af35b9cf017aaf8ae41a605d6501dd49afc35b70b544a45dd8354", size = 23310055, upload-time = "2026-08-14T19:55:51.357Z" }, { url = "https://files.pythonhosted.org/packages/bd/ec/d76387b388fa21620088b89b9c67f2596a707add585104e0cb5e8abf55f2/uv-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1a06c8bc4d43b5f6c1e3f2ae3d0f6455b07515f762516f95e52e6c0cbccedf15", size = 21401335, upload-time = "2026-08-14T19:55:55.371Z" }, { url = "https://files.pythonhosted.org/packages/6d/bc/81ab953b7261ae6be40874b1f283a10873871e02eb353d354614dd8da96b/uv-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d87156bc174d94fae890bb7a261e2867140abb9fe1e9de81a5295e582fb9d0f5", size = 19290641, upload-time = "2026-08-14T19:55:58.998Z" }, { url = "https://files.pythonhosted.org/packages/7d/13/07585043c10e648820bf826474dac46864ce6691da5dc52fee43c5c7523a/uv-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2d65b7b3bc3fd28678f62aa7fb5d90f106ad9782c1354af60b6cecdf9ea9ecd9", size = 22245569, upload-time = "2026-08-14T19:56:02.729Z" }, { url = "https://files.pythonhosted.org/packages/3e/6d/310f8f56f8d001b4000112a09d7b7de80fb2024a90208fabb9ddc457c123/uv-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:712624b62e25c84e5a10fc6aa144d8a81b685fdc067a54a7ca4367d75d2cf791", size = 22745152, upload-time = "2026-08-14T19:56:06.426Z" }, { url = "https://files.pythonhosted.org/packages/92/da/7922b67eec5ee03e94333c5841b682c335033ee80acac17c3417bd752656/uv-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9656ac7a00fd4314980fb0f790df1c1f3fa9cbcf9af9c6f611b19448b9da687", size = 22787947, upload-time = "2026-08-14T19:56:10.149Z" }, { url = "https://files.pythonhosted.org/packages/62/55/5dbaed832a4b36809ef8a07c8e56e9fee0dedb0aa0454f6d232b6e468f2c/uv-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:568485b44e848eb3693f85d6b00299ccd8fc4d26902030dbf24f549c276db9ca", size = 23367616, upload-time = "2026-08-14T19:56:13.768Z" }, { url = "https://files.pythonhosted.org/packages/11/77/baf761d12bb66efb01706e3bbb5926ed0d13cb0a40539a661fcfffd46de4/uv-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd08c82831b0033330f8eeeb0d90f938a4d999f25569bee68a975c736142d795", size = 24586263, upload-time = "2026-08-14T19:56:17.57Z" }, { url = "https://files.pythonhosted.org/packages/c3/a8/76c1031c4834c959bb8a8059c9feabeaa77488ce8b6a3529d6d929ae81cf/uv-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:edd9ff6154b891146a342c143cd29b330ad97ac6a4b20ff4a99a20a4da84ceca", size = 24160655, upload-time = "2026-08-14T19:56:21.568Z" }, { url = "https://files.pythonhosted.org/packages/93/22/dacc9a0bc8604187a1ba954a3aef8329e4104eb0af772d2c3c634893bd9b/uv-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e195ccf1ed60c8bb24a6447ce306441a4181d54b602407e09bc56e963911c15", size = 23657089, upload-time = "2026-08-14T19:56:25.144Z" }, { url = "https://files.pythonhosted.org/packages/39/98/e8f9c071622f2cb4072d8b587d27b27d23cf0d3ebf8b3687f5af6030f587/uv-0.12.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:58abfb0f658b39a834307a11223bc170294ea214263b4c99ecc7663720d43544", size = 22379954, upload-time = "2026-08-14T19:56:28.789Z" }, { url = "https://files.pythonhosted.org/packages/73/95/4c3f060e95f7cbe9177b4ab361f0cbfc4ae22e5a49b22e73eee9f0d0a6ca/uv-0.12.5-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:6ad2c455f1fe4d2962f6fd7ccb3b1f61c61856681c9d99f40e170b2074353fa3", size = 23318163, upload-time = "2026-08-14T19:56:32.504Z" }, { url = "https://files.pythonhosted.org/packages/a0/96/ca0497ef8912ef48dbbc9982a8b4212260c34d56bfd0d45fe67b31942121/uv-0.12.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a05b497c2a948c8600f4c831a89852b4d2514b7f561074225cc9edd0cc4811e2", size = 23470437, upload-time = "2026-08-14T19:56:36.525Z" }, { url = "https://files.pythonhosted.org/packages/60/e7/8bdc37669a6cd2b46a2ec08ccbb58c61395ec84a073e199f5a4a64bb998f/uv-0.12.5-py3-none-musllinux_1_1_i686.whl", hash = "sha256:7817f8e957960f9ddc452ea353f283c0d6393e2e31b400276485adced5b1f371", size = 22545803, upload-time = "2026-08-14T19:56:40.606Z" }, { url = "https://files.pythonhosted.org/packages/37/cc/01e39e1dbeb838a6b3c26bf97c867d6f366459b22a38bea691af8c6c94c0/uv-0.12.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:dc14e4f81a99b585a891350c60d1ff4557d54cb3c3c81fa45fd4e0dd512ba752", size = 23874113, upload-time = "2026-08-14T19:56:44.193Z" }, { url = "https://files.pythonhosted.org/packages/0a/38/9053599a73a351d1cd34195c7a48c1db4d4d51b57b543607fad7ecf9354c/uv-0.12.5-py3-none-win32.whl", hash = "sha256:39bb102766c95571781a7b4c611675ea213e08df5c680f3936279b3c0d1f6c3c", size = 20744641, upload-time = "2026-08-14T19:56:47.689Z" }, { url = "https://files.pythonhosted.org/packages/ce/f6/a9af9311c7f5640ca2bfcfdedb7aca37fa6d1d9f5c981fb50c5be02b7477/uv-0.12.5-py3-none-win_amd64.whl", hash = "sha256:455c3e57602e2141e66e2f0bf685898c9c5e5a70377d14c9a71554a3baf3ddbf", size = 21621812, upload-time = "2026-08-14T19:56:51.126Z" }, { url = "https://files.pythonhosted.org/packages/bc/fb/e1266399f755f97a0783de379f2fed6dae0a2a240db32fe5a2eb976fec8a/uv-0.12.5-py3-none-win_arm64.whl", hash = "sha256:bea86f27a027e0e3af908db4bdd4f1ceef3ca2bd47673b5ccca7f550e325b1b4", size = 20381876, upload-time = "2026-08-14T19:56:54.883Z" }, ] [[package]] name = "uvicorn" version = "0.52.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, ] [[package]] name = "virtualenv" version = "21.7.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1d/60/fc54e876e34f94dd0cf0185aaecfd4bfa906653f003d9b2fb21428642fca/virtualenv-21.7.5.tar.gz", hash = "sha256:a73c4246fba3c8901ff9717399f466e00eeca5a3834981f1a6ebb4f1e94de2f8", size = 5346743, upload-time = "2026-08-25T05:39:16.14Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/d8/401141bf45637be916c86d325bd821c5838c7eff83294b934cd94e774e4f/virtualenv-21.7.5-py3-none-any.whl", hash = "sha256:e36ca889510ab6cb0b1dca93c59e5431dd4422a3c88f487358d470c90af8c07a", size = 5324697, upload-time = "2026-08-25T05:39:14.229Z" }, ] [[package]] name = "watchfiles" version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, ] [[package]] name = "websockets" version = "17.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f7/96/e01084f83a64bcb3a27994bd0cb0db68ff29d9c6707fae37ec19b18ba990/websockets-17.0.1.tar.gz", hash = "sha256:5baa9bc0dfbae8c507e51c8cf1b6d4628086f7a87bbd3a9952bd5f035451f1cc", size = 183298, upload-time = "2026-07-31T11:31:27.665Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ba/a8/79c577bc2f874ee22f6f5ccdab97ba9ce6b96806be3fcc3a6d8490f88a21/websockets-17.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:55b12e47dcee83673a40d07686cfb6f9d6dfc285976ade9463f61d2bef3fad22", size = 212593, upload-time = "2026-07-31T11:29:56.518Z" }, { url = "https://files.pythonhosted.org/packages/db/99/e1cfaf419bb3b2fcfd6792a846f1d936293132b0b9a56530ced016c83c7b/websockets-17.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c118a6b0e25bfc9a6802075d748fa6321714ffbdf3c88d29d9a0e3c7386c75", size = 210280, upload-time = "2026-07-31T11:29:57.768Z" }, { url = "https://files.pythonhosted.org/packages/a2/ef/cc994494bf7d97e41833f6ff55c24f535e4d527a10370b9631737e9c2f00/websockets-17.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:734d20364dc2cfe03674883cafcf580b6e431c5ce42b476312b9285310230cf9", size = 210538, upload-time = "2026-07-31T11:29:59.021Z" }, { url = "https://files.pythonhosted.org/packages/87/32/fbf2d132f63ba3e67f675bccf333469786a24e0418969ce1d8e6ff9e6f02/websockets-17.0.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9493314a99e599163c854fb5900ad7f7ea38c5cb9d9103aa30b3c6b8181c01fa", size = 219925, upload-time = "2026-07-31T11:30:00.298Z" }, { url = "https://files.pythonhosted.org/packages/16/50/64eee3d25a47fe744a9490e0627cc373dca096755db740f91c28bd61cd35/websockets-17.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:18ded646ce98cdd3c0235825b3252f1df55765ba49b616bb10282f758667b4d0", size = 220206, upload-time = "2026-07-31T11:30:01.52Z" }, { url = "https://files.pythonhosted.org/packages/15/56/10ed4bc4dd75f204e3c62bd4898e44a8742a27773c80b188cfa7888aad2d/websockets-17.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1bec5d6a19f5fbe87e4940739cfc65e7bb53d8b353e1029b8037a1653b321bc", size = 221445, upload-time = "2026-07-31T11:30:02.788Z" }, { url = "https://files.pythonhosted.org/packages/bf/be/bb14328614c068ab09569962fbf218fc00413ce3febc6d2684c764b6f37e/websockets-17.0.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:872273e629ca7e3d35f16a2dc6ede84e1d5c831e616b8277de6e4f83114e7c58", size = 222887, upload-time = "2026-07-31T11:30:03.943Z" }, { url = "https://files.pythonhosted.org/packages/32/1b/4cb0eec2fee310007104687493175af190019f705940c864f9c523fe9f6f/websockets-17.0.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1df81d174c1561292de9e40b141cafc04f69077272f6c352afe1d743e20810df", size = 222072, upload-time = "2026-07-31T11:30:05.258Z" }, { url = "https://files.pythonhosted.org/packages/6c/9c/14e6391de777ddb39c439c450deb551406d445e25a5877d6fa25c49d4544/websockets-17.0.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:759adeb5b0c5775b563254ec63b5b79089fc0045b479143a0b1b8c0ebaae1253", size = 220826, upload-time = "2026-07-31T11:30:06.53Z" }, { url = "https://files.pythonhosted.org/packages/cb/57/96e94e384442247bbed5d3ab67381c7257355c2d66b62c3ad33a17f5d385/websockets-17.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d99db29b5444e3982f1ce2ba8a833508ad44b2f1fbd0bd99e81d825c0b461", size = 218107, upload-time = "2026-07-31T11:30:07.766Z" }, { url = "https://files.pythonhosted.org/packages/c0/8c/9c9dedd14c3919435df9b35cdee7111268c751252b87652f3a6a4f56e760/websockets-17.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:02ed63bf26dda9fa27df730a41f6664586c4ee05972c8fb667ce1725b3fd13d3", size = 220889, upload-time = "2026-07-31T11:30:09.035Z" }, { url = "https://files.pythonhosted.org/packages/94/4d/ca73c2ac82c00f50c529784bacb323e42da4816333211bc1543d90c9cf11/websockets-17.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:eab6de8a98b9a7772cf686d00b4de439fc7efb8ab05ae106ef227291d06f87c5", size = 219486, upload-time = "2026-07-31T11:30:10.289Z" }, { url = "https://files.pythonhosted.org/packages/5b/da/fb37ac09dcd7c69dd73bac979ed393df35f78a3c232e293d1ff3bd586d24/websockets-17.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2a855b6dfe21c4d3420be265ae031829ba8ba0be0ea350d9f7c3ef30ae63ebe2", size = 220258, upload-time = "2026-07-31T11:30:11.605Z" }, { url = "https://files.pythonhosted.org/packages/fc/04/9693f191d968a93f37326a17301a101d49580889c688f466699f89ecdee1/websockets-17.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7002d5f9e1c3ddd991cdfdbfee18cc8c8b196b2445022892badacd6cb338bbbc", size = 221358, upload-time = "2026-07-31T11:30:12.858Z" }, { url = "https://files.pythonhosted.org/packages/cf/29/ad0d85c01db5dcf22898d51648bd2c25af0dd0a4a41c550b11acddeeeba7/websockets-17.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c395bda8e7d8f51a02e80261fb57127979e5c472675d9a96b2860619ad47da48", size = 218921, upload-time = "2026-07-31T11:30:14.064Z" }, { url = "https://files.pythonhosted.org/packages/18/3b/bf8e855e495dcca63f2b8aa019cf2ada3160e1fa66d833c7417f3b1f7f38/websockets-17.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aadc298969ad229d8e3029fc5cc751fdad286696230f9cf014e90ff9cd8e6ea0", size = 219871, upload-time = "2026-07-31T11:30:15.358Z" }, { url = "https://files.pythonhosted.org/packages/3b/db/c7abd6639a93a40279cd1ddc57e09e1c4f8381c4cfccdb775aa5aac9770a/websockets-17.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f11a398d8170b7ac5000baf7f258dcda579ef3ea744e0cc6a165e0dfbc0d3198", size = 220154, upload-time = "2026-07-31T11:30:16.96Z" }, { url = "https://files.pythonhosted.org/packages/f6/2a/25a9f8f2e5a6ef34e911d2f55d9f756bdeb92b4c28cfb77b8430bbc73cb1/websockets-17.0.1-cp313-cp313-win32.whl", hash = "sha256:846a4a8b0833e3cad57523d9e3bd50ec8ea05ab9d06c582f82a1340ba096af5f", size = 213038, upload-time = "2026-07-31T11:30:18.434Z" }, { url = "https://files.pythonhosted.org/packages/81/2f/ea1380f72bb11b64fc5bc7ae0d42de5bbf3e6dc13b965706b2a1d4e17cdf/websockets-17.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:409d93efcaa14f7a99592c5baaef5ec6ca94fba0f5aec1a86f693977c69c9c1c", size = 213348, upload-time = "2026-07-31T11:30:19.693Z" }, { url = "https://files.pythonhosted.org/packages/e6/c7/b956ed9151c3c74530ebc62d716fbfdbde7507a6acc6423a64f9ecfb6b8a/websockets-17.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:90246fa9e6cb192a778ce6ce024057ec54317a894db7899c922dcdc1f4cbf6a5", size = 213282, upload-time = "2026-07-31T11:30:21.045Z" }, { url = "https://files.pythonhosted.org/packages/98/dc/cadab608924ac605647031472fb1f8792d7d4ea07565ba1899ec42028e0d/websockets-17.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:53b90c00bc6201ab6695c7ff51a04d0e425514c37515e9eeecd2c1b978ac6c0e", size = 212640, upload-time = "2026-07-31T11:30:22.436Z" }, { url = "https://files.pythonhosted.org/packages/16/7a/b034d13ca181211bbd58bb50835cb196a7784cd505b5a2079d4d03374f9f/websockets-17.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f33a649bfcb8312524173cc4bbafa7dbb236e18eee9aa31a1d324ca0ddda28c", size = 210332, upload-time = "2026-07-31T11:30:23.608Z" }, { url = "https://files.pythonhosted.org/packages/2f/4d/943ede39b53744768edf1ed84a3f9401527388228a3d6c1249c02c3d6bd7/websockets-17.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cddc675ec31bca65473321f9a9794e488b43b3b8de5d02c8ef4810c5d5792163", size = 210546, upload-time = "2026-07-31T11:30:24.932Z" }, { url = "https://files.pythonhosted.org/packages/cf/e3/88dc159d2ae66743c669443246243f28d873b0c5e58271b8cc1ca0440334/websockets-17.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b3ff0ad440ad52dda64138f16895f66403f40192365e39b1010e889f289746b0", size = 219928, upload-time = "2026-07-31T11:30:26.221Z" }, { url = "https://files.pythonhosted.org/packages/fe/f2/ff27eaefa15851a5cf7f004ab827a022bf2d6632cb520f89cb100db7e84b/websockets-17.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:72d7f2a5aeb4e82daa4ee18f125b4277f427033359be5c745ad709608446cc2c", size = 220279, upload-time = "2026-07-31T11:30:27.49Z" }, { url = "https://files.pythonhosted.org/packages/19/2e/a5166149f363d2449c1cb2dde6486a245521979509d53b87a09f3e79662b/websockets-17.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fd88365da261c53d3e943fb37e0d0721b9cde119f6b2e3fc84369b6ab234d63", size = 221525, upload-time = "2026-07-31T11:30:28.872Z" }, { url = "https://files.pythonhosted.org/packages/9b/b5/f46931269b3ff3bde65d27c65ddb22f9bb8ce92ac2c6c4df0910128f6219/websockets-17.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ab9f962a5b64a5c3c845d556b7dc4e6fb683f7b67179f8205e814bb2e0213ffe", size = 222897, upload-time = "2026-07-31T11:30:30.164Z" }, { url = "https://files.pythonhosted.org/packages/42/f4/deccf3439f35df953ec35e13fe07986821c5f1ab5785d69614283bdb9034/websockets-17.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c07f145d0b9e90cbd96035f31fb79199aef4da1872854e36ebeb258e3d57594", size = 222129, upload-time = "2026-07-31T11:30:31.489Z" }, { url = "https://files.pythonhosted.org/packages/35/a5/e1b57a59da92ade37fd021567a17b518ea8267b28e5530075844cdb525fe/websockets-17.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9f7747d3daa41a11f25f7cca5dc988fc51da97b311bed4c9d843860f79779283", size = 220875, upload-time = "2026-07-31T11:30:32.805Z" }, { url = "https://files.pythonhosted.org/packages/f0/30/e7d0889c790a854156de424575fd67af79ddbaed9ff3157ae863dfd1c1dc/websockets-17.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2abb1ba0a5133b7d2ef3c1c9f4b0c1e8a101012dce0b594ab2b2888d9a64820e", size = 218160, upload-time = "2026-07-31T11:30:34.512Z" }, { url = "https://files.pythonhosted.org/packages/09/2e/43db785d6ed9ae7594fae7b62bbc9cb4dfee2b015e06a1005f1e5ce283b6/websockets-17.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f3fd9a1f87f8f0f3f8e9f9bd0195f7516562d13f5b178db8c5784d1f60b60bed", size = 220951, upload-time = "2026-07-31T11:30:35.806Z" }, { url = "https://files.pythonhosted.org/packages/eb/f5/4ac3cab3d5e8a830657a822f64a8910e3803229c6783e59c3fd9a3487427/websockets-17.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2bc14b481e05e331811108daa1aeb41a5e237a5564ef2f02ec5a356a0f102f78", size = 219460, upload-time = "2026-07-31T11:30:37.273Z" }, { url = "https://files.pythonhosted.org/packages/03/0e/c3a4020673ffc17c82cf1a467835038a196a555d3b4f2a50f0f063cf8ccc/websockets-17.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57d2ee9b24b404ce75f3814f92073c0ed88106c950148d2427fe8d25ca254d1f", size = 220248, upload-time = "2026-07-31T11:30:38.527Z" }, { url = "https://files.pythonhosted.org/packages/7d/87/e47a6a278cc1dfade38444c893ce18322943c25d4b780a74450d9d164be1/websockets-17.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1b363bfd72a52c0658a3154a4cff219f15a474b35a235057d38853bf151acce7", size = 221421, upload-time = "2026-07-31T11:30:39.879Z" }, { url = "https://files.pythonhosted.org/packages/da/8f/473d5fc4e3836e375b0233c6ef26777e6e5e3f7bfc84ccd524eae4090ed5/websockets-17.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:10b1587c599fa0f2c89154587c80e0fda98ade6c9fa8c0260a2823fb1800b685", size = 218975, upload-time = "2026-07-31T11:30:41.192Z" }, { url = "https://files.pythonhosted.org/packages/d4/b9/819ec2dcdf69031d7e9cab11247f3a6ff9bbc8c7c53ada1dbcb9055b227b/websockets-17.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d7d72843691f50b91127c50688df10cb72ec6f4c4b1d7e2c11ab33b16acf8e51", size = 219925, upload-time = "2026-07-31T11:30:42.524Z" }, { url = "https://files.pythonhosted.org/packages/85/b9/6c0da301f6118502e079cf92f4e864adf28e56b3f8c0f6085076ced7b876/websockets-17.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90973a3a00f23afdfd1c9b06fb84289bf0220f247ef8a62501a1967c7af54f7b", size = 220218, upload-time = "2026-07-31T11:30:44.04Z" }, { url = "https://files.pythonhosted.org/packages/55/f9/cba32dc9dd856565263d6272f594255bd0e2781deb8cd982c026a54760ad/websockets-17.0.1-cp314-cp314-win32.whl", hash = "sha256:599b03beb77633bffc095334338fad79cafc2b01fbd58953838130a9ae967d7b", size = 212626, upload-time = "2026-07-31T11:30:45.579Z" }, { url = "https://files.pythonhosted.org/packages/fa/95/91cdd8c192287d7ea741f37cf7d64fdc1a14410f06f73805e428a1a590af/websockets-17.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:81ce19c6046ace11da7001781be7317bb1dc389f399af4b2ed962190f76f9add", size = 212969, upload-time = "2026-07-31T11:30:46.983Z" }, { url = "https://files.pythonhosted.org/packages/8e/fd/8c98a1e431960661c5769ab1a4dd66494e87ab02d791cc79e51e0d9a289f/websockets-17.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:efe0ae052a8d023b87198921e8a7ce1dc7768816bcd2fbc20df171ac73a04891", size = 212850, upload-time = "2026-07-31T11:30:48.304Z" }, { url = "https://files.pythonhosted.org/packages/13/c1/142f5186ee7dc3beee0426b998a79e223e067b7689afcaa95890d64aa800/websockets-17.0.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ab56439c9f74c52770690c7b2f616b3bf775cb3920453ee355ac765c032d8bbf", size = 212967, upload-time = "2026-07-31T11:30:49.688Z" }, { url = "https://files.pythonhosted.org/packages/02/de/4b03ed316c9dee180365286c298219809ff247be39beeaaf9958b21167ab/websockets-17.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:20a92f78ac8250984ed459faa9ca48c285adbfc0038ddc3fdac6046990a9c9ed", size = 210504, upload-time = "2026-07-31T11:30:50.987Z" }, { url = "https://files.pythonhosted.org/packages/cd/d8/ad2b3e8f867e1e8cac3077e2f33ffb60b71bd763d6cfc71bd916f113c3bf/websockets-17.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6a434e59962a4fb9016bea327e1d14d6cd67670ecfb8942b4f4a0c24036634ce", size = 210702, upload-time = "2026-07-31T11:30:52.261Z" }, { url = "https://files.pythonhosted.org/packages/48/18/7a77a82ce9d6f831c07b176da3942f7e71acd0f115f3ecdb1d00a040eb01/websockets-17.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2503c7e2a5049a12d5dac917a46d5d52591283a766165b8176bb167560421b38", size = 220290, upload-time = "2026-07-31T11:30:53.582Z" }, { url = "https://files.pythonhosted.org/packages/f5/af/43c3e3c3ea7ba4693c2181743f3221957df28bededadcbd9fc8a0661bde0/websockets-17.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:28012a54510fe8301bb893ef143cec30a2780a2d3bc20b7bbdf4379d7a63945d", size = 220573, upload-time = "2026-07-31T11:30:54.966Z" }, { url = "https://files.pythonhosted.org/packages/1f/bd/ed48eca15725743ee7e2dc172e15c61de29e85ec98dace7b14257f366836/websockets-17.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22bd00f8bae2bccdb5dbe41e20f58ba44ca9fff0b4b561aaf39099c35da762ed", size = 221747, upload-time = "2026-07-31T11:30:56.762Z" }, { url = "https://files.pythonhosted.org/packages/99/50/838deb7937a8225c4925dd4a977eafea473fabf444178a99de0bc7e92bb0/websockets-17.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e98ec9ec61cce5bc4b8b218322ad090b0994eb060bb04da704c62ef0a3d864e6", size = 223891, upload-time = "2026-07-31T11:30:58.127Z" }, { url = "https://files.pythonhosted.org/packages/f8/e9/657fb70c6eb6bcd01adfa5d2b06496e9911e1c1a8813d353b8c00f7591cd/websockets-17.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e387adb0c692c6b5571bdeafc8ac9d1901ea30f10309134780b16ecd35e6605", size = 222317, upload-time = "2026-07-31T11:30:59.416Z" }, { url = "https://files.pythonhosted.org/packages/07/4c/82cb722afa5428fed981331210c4c07600570db01bb1620579f655b5adaf/websockets-17.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd1470d2c53fe53269bf5619da7725d30dd9b9693f1689f7a85eab8dea734442", size = 221047, upload-time = "2026-07-31T11:31:00.757Z" }, { url = "https://files.pythonhosted.org/packages/90/84/bd6d67d6bc65f0de0cb50de55dab42f256a9876a351c4736522eb168fda0/websockets-17.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884af729b8ab50486acd94d9768c2b60914bf39b579ebba0a5cb73bfdfd61fd2", size = 218626, upload-time = "2026-07-31T11:31:02.48Z" }, { url = "https://files.pythonhosted.org/packages/96/cb/6a372c8553976f0d8f97f5115826ed47e34b3be6b8bf0d0249af249a7416/websockets-17.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a60fa1a25cca1bcc2bf87b8d6be37a741f0a3239fb5e9cfb7a37173b68ffcf87", size = 221299, upload-time = "2026-07-31T11:31:03.795Z" }, { url = "https://files.pythonhosted.org/packages/bc/31/f966e8472337974f74d788b3ef6c6f3b8b9a5f201efd16a843c91d269fa5/websockets-17.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e8208f2729cba030ff872a92064c97584eeb9502f53d32a05a0f05d5a17ca6c6", size = 219789, upload-time = "2026-07-31T11:31:05.08Z" }, { url = "https://files.pythonhosted.org/packages/57/34/404e83a6cc7b0efcac810b7041bffd72ff76900e6fd0aa45a26c92fb2ffe/websockets-17.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:54cdcaa56f5d3eafd57058f0fa4a3de93a310b43a3c4699f06efc4c0bd054a5a", size = 220678, upload-time = "2026-07-31T11:31:06.635Z" }, { url = "https://files.pythonhosted.org/packages/e5/70/8946188c2a68d67251859b589a3634918cf7867bf0b891347a5ecaa43d30/websockets-17.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4d41c0a1d47a478bc432b3b9068097bee1ce0c5b19327ea6f75c2ab34ab1f2fb", size = 221697, upload-time = "2026-07-31T11:31:08.023Z" }, { url = "https://files.pythonhosted.org/packages/04/16/ee73fc2083a2938ac6209f4ec804960496835b20a0068dbcfe8424957c04/websockets-17.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f991247276797d0c61ab7770bc9791eadc16f683b4d83517f624932adc1a8bab", size = 219390, upload-time = "2026-07-31T11:31:09.378Z" }, { url = "https://files.pythonhosted.org/packages/94/6a/d5f88033c69932af6cdaa72da62516ade47c257e3bf69f4c0ba5f40e12a2/websockets-17.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:733e3cc7171fa1b899edbe725ef9382d0e960657dc1fd933f3281ae910c01dab", size = 220161, upload-time = "2026-07-31T11:31:10.915Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/6183dd2c0370287ecf4afe0bb33aca364208e5e7b0e1a286adcaecc0c78b/websockets-17.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:810cb3fb5fa6e447216f4e82d9a85cb8aed0929ae3538153ddfe8a6e3121a58d", size = 220591, upload-time = "2026-07-31T11:31:12.289Z" }, { url = "https://files.pythonhosted.org/packages/26/93/70f6516d85b9744f7eac224c4b1b9ef4e84133f80b53be02080cb1c3e663/websockets-17.0.1-cp314-cp314t-win32.whl", hash = "sha256:17ac37716c0244e82c9e384c41653c090b1864c6610224ca3857e7f7b58fce10", size = 212755, upload-time = "2026-07-31T11:31:13.883Z" }, { url = "https://files.pythonhosted.org/packages/f1/2c/9d9c1da5a7ea9af307b386d25f64d1dead4729644198d2b92e36db5dfd41/websockets-17.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bb31f42ea095ea826463c770829aa188a86c9a5c976b1467cbbf583c811de833", size = 213094, upload-time = "2026-07-31T11:31:15.367Z" }, { url = "https://files.pythonhosted.org/packages/5b/24/a585e7573e128070605d003b5544729bcd58d9756c7e99d550818ca4b916/websockets-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dbfae8e75b342e31fc6fd1a8bbb393b7cbb91d6cfd581650300a94381e7b7e2b", size = 213009, upload-time = "2026-07-31T11:31:16.776Z" }, { url = "https://files.pythonhosted.org/packages/09/ce/3929538b2b9918f5eee623fbf3346893973191f6df93f19bbda097bd7bb7/websockets-17.0.1-py3-none-any.whl", hash = "sha256:c6be9cba65c65cc76dfa3d4619e359ff02a4476c74e179b215236c11a0b32345", size = 206718, upload-time = "2026-07-31T11:31:26.037Z" }, ] [[package]] name = "werkzeug" version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] [[package]] name = "wsproto" version = "1.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, ]