pax_global_header00006660000000000000000000000064146750177120014524gustar00rootroot0000000000000052 comment=d91223136eaf8b0061ebd904c2b09b5ab5546db0 aiohttp-session-2.12.1/000077500000000000000000000000001467501771200147405ustar00rootroot00000000000000aiohttp-session-2.12.1/.codecov.yml000066400000000000000000000000711467501771200171610ustar00rootroot00000000000000codecov: branch: master notify: wait_for_ci: yes aiohttp-session-2.12.1/.coveragerc000066400000000000000000000001461467501771200170620ustar00rootroot00000000000000[run] branch = True source = aiohttp_session, tests omit = site-packages [html] directory = coverage aiohttp-session-2.12.1/.flake8000066400000000000000000000014441467501771200161160ustar00rootroot00000000000000[flake8] enable-extensions = G exclude = demo/,tests/,examples/ max-doc-length = 88 max-line-length = 88 select = A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,B901,B902,B903,B950 # E226: Missing whitespace around arithmetic operators can help group things together. # E501: Superseeded by B950 (from Bugbear) # E722: Superseeded by B001 (from Bugbear) # W503: Mutually exclusive with W504. ignore = N801,N802,N803,E203,E226,E305,W504,E252,E301,E302,E704,W503,W504,F811 per-file-ignores = # S101: Pytest uses assert tests/*:S101 # I900: Requirements for examples shouldn't be included examples/*:I900 # flake8-import-order import-order-style = pycharm # flake8-quotes inline-quotes = " # flake8-requirements known-modules = pynacl:[nacl] requirements-file = requirements-dev.txt aiohttp-session-2.12.1/.github/000077500000000000000000000000001467501771200163005ustar00rootroot00000000000000aiohttp-session-2.12.1/.github/dependabot.yml000066400000000000000000000003121467501771200211240ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" aiohttp-session-2.12.1/.github/workflows/000077500000000000000000000000001467501771200203355ustar00rootroot00000000000000aiohttp-session-2.12.1/.github/workflows/auto-merge.yml000066400000000000000000000011401467501771200231210ustar00rootroot00000000000000name: Dependabot auto-merge on: pull_request_target permissions: pull-requests: write contents: write jobs: dependabot: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v1.6.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} aiohttp-session-2.12.1/.github/workflows/ci.yaml000066400000000000000000000056751467501771200216310ustar00rootroot00000000000000name: CI on: push: branches: - master - '[0-9].[0-9]+' # matches to backport branches, e.g. 3.6 tags: [ 'v*' ] pull_request: branches: - master - '[0-9].[0-9]+' jobs: lint: name: Linter runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: 3.9 cache: 'pip' cache-dependency-path: '**/requirements*.txt' - name: Install dependencies uses: py-actions/py-dependency-install@v4 with: path: requirements-dev.txt - name: Install itself run: pip install . - name: Mypy run: mypy - name: Flake8 run: flake8 - name: Prepare twine checker run: | pip install -U build twine wheel python -m build - name: Run twine checker run: twine check dist/* test: name: Tests runs-on: ubuntu-latest strategy: matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: 'pip' cache-dependency-path: '**/requirements*.txt' - name: Install dependencies run: | pip install --upgrade pip build twine pip install -r requirements.txt - name: Run tests run: | make cov python -m build twine check dist/* - name: Upload coverage uses: codecov/codecov-action@v4 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} check: # This job does nothing and is only used for the branch protection if: always() needs: [lint, test] runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} deploy: name: Deploy environment: release if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') needs: [check] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Update pip, wheel, setuptools, build, twine run: | python -m pip install -U pip wheel setuptools build twine - name: Build dists run: | python -m build - name: Make Release uses: aio-libs/create-release@v1.6.6 with: changes_file: CHANGES.txt name: aiohttp-session version_file: aiohttp_session/__init__.py github_token: ${{ secrets.GITHUB_TOKEN }} pypi_token: ${{ secrets.PYPI_API_TOKEN }} dist_dir: dist fix_issue_regex: "`#(\\d+) `" fix_issue_repl: "(#\\1)" aiohttp-session-2.12.1/.github/workflows/codeql.yml000066400000000000000000000044501467501771200223320ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ 'master' ] pull_request: # The branches below must be a subset of the branches above branches: [ 'master' ] schedule: - cron: '43 8 * * 1' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" aiohttp-session-2.12.1/.gitignore000066400000000000000000000013371467501771200167340ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ coverage venv .pytest_cache .mypy_cache .python-version aiohttp-session-2.12.1/.isort.cfg000066400000000000000000000002261467501771200166370ustar00rootroot00000000000000[settings] line_length=88 include_trailing_comma=True multi_line_output=3 force_grid_wrap=0 combine_as_imports=True known_first_party=aiohttp_session aiohttp-session-2.12.1/.mypy.ini000066400000000000000000000015611467501771200165200ustar00rootroot00000000000000[mypy] files = aiohttp_session, demo, examples, tests check_untyped_defs = True follow_imports_for_stubs = True disallow_any_decorated = True disallow_any_generics = True disallow_any_unimported = True disallow_incomplete_defs = True disallow_subclassing_any = True disallow_untyped_calls = True disallow_untyped_decorators = True disallow_untyped_defs = True extra_checks = True implicit_reexport = False no_implicit_optional = True show_error_codes = True show_error_code_links = True strict_equality = True warn_incomplete_stub = True warn_redundant_casts = True warn_unreachable = True warn_unused_ignores = True warn_return_any = True [mypy-tests.*] disallow_any_unimported = False [mypy-aiopg.*] ignore_missing_imports = True [mypy-aiomcache.*] ignore_missing_imports = True [mypy-docker.*] ignore_missing_imports = True [mypy-psycopg2.*] ignore_missing_imports = True aiohttp-session-2.12.1/.pre-commit-config.yaml000066400000000000000000000041541467501771200212250ustar00rootroot00000000000000repos: - repo: local hooks: - id: changelogs-rst name: changelog filenames language: fail entry: >- Changelog files must be named ####.(bugfix|feature|removal|doc|misc)(.#)?(.rst)? exclude: >- ^CHANGES/(\.TEMPLATE\.rst|\.gitignore|\d+\.(bugfix|feature|removal|doc|misc)(\.\d+)?(\.rst)?|README\.rst)$ files: ^CHANGES/ - id: changelogs-user-role name: Changelog files should use a non-broken :user:`name` role language: pygrep entry: :user:([^`]+`?|`[^`]+[\s,]) pass_filenames: true types: [file, rst] - repo: https://github.com/pre-commit/pre-commit-hooks rev: 'v4.3.0' hooks: - id: check-merge-conflict - repo: https://github.com/asottile/yesqa rev: v1.4.0 hooks: - id: yesqa - repo: https://github.com/PyCQA/isort rev: '5.10.1' hooks: - id: isort - repo: https://github.com/psf/black rev: '22.10.0' hooks: - id: black language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks rev: 'v4.3.0' hooks: - id: end-of-file-fixer exclude: >- ^docs/[^/]*\.svg$ - id: requirements-txt-fixer exclude: >- ^requirements/constraints[.]txt$ - id: trailing-whitespace - id: file-contents-sorter files: | CONTRIBUTORS.txt| docs/spelling_wordlist.txt| .gitignore| .gitattributes - id: check-case-conflict - id: check-json - id: check-xml - id: check-executables-have-shebangs - id: check-toml - id: check-yaml - id: debug-statements - id: check-added-large-files - id: check-symlinks - id: fix-byte-order-marker - id: fix-encoding-pragma args: ['--remove'] - id: detect-aws-credentials args: ['--allow-missing-credentials'] - id: detect-private-key exclude: ^examples/ - repo: https://github.com/asottile/pyupgrade rev: 'v3.1.0' hooks: - id: pyupgrade args: ['--py38-plus'] - repo: https://github.com/PyCQA/flake8 rev: '5.0.4' hooks: - id: flake8 exclude: "^docs/" - repo: https://github.com/Lucas-C/pre-commit-hooks-markup rev: v1.0.1 hooks: - id: rst-linter files: >- ^[^/]+[.]rst$ exclude: >- ^CHANGES\.rst$ aiohttp-session-2.12.1/.pyup.yml000066400000000000000000000001221467501771200165310ustar00rootroot00000000000000# Label PRs with `deps-update` label label_prs: deps-update schedule: every week aiohttp-session-2.12.1/CHANGES.txt000066400000000000000000000074131467501771200165560ustar00rootroot00000000000000.. towncrier release notes start 2.12.1 (2024-09-25) =================== * Minor typing fix for aiohttp 3.10+. * Dropped support for Python 3.7. Started testing on 3.11 - 3.13. 2.12.0 (2022-10-28) =================== * Migrated from `aioredis` to `redis` (if using redis without installing `aiohttp-session[aioredis]` then it will be necessary to manually install `redis`). 2.11.0 (2021-01-31) =================== * Support initialising `EncryptedCookieStorage` with `Fernet` object directly. * Fix an issue where the session would get reset before the cookie expiry. 2.10.0 (2021-12-30) =================== * Typing support * Add samesite cookie option * Support aioredis 2 2.9.0 (2019-11-04) ================== * Fix memcached expiring time (#398) 2.8.0 (2019-09-17) ================== * Make this compatible with Python 3.7+. Import from collections.abc, instead of from collections. (#373) 2.7.0 (2018-10-13) ================== * Reset a session if the session age > max_age (#331) * Reset a session on TTL expiration for EncryptedCookieStorage (#326) 2.6.0 (2018-09-12) ================== * Create a new session if `NaClCookieStorage` cannot decode a corrupted cookie (#317) 2.5.0 (2018-05-12) ================== * Add an API for requesting new session explicitly (#281) 2.4.0 (2018-05-04) ================== * Fix a bug for session fixation (#272) 2.3.0 (2018-02-13) ================== - Support custom encoder and decoder by all storages (#252) - Bump to aiohttp 3.0 2.2.0 (2018-01-31) ================== - Fixed the formatting of an error handling bad middleware return types. (#249) 2.1.0 (2017-11-24) ================== - Add `session.set_new_identity()` method for changing identity for a new session (#236) 2.0.1 (2017-11-22) ================== - Replace assertions in aioredis installation checks by `RuntimeError` (#235) 2.0.0 (2017-11-21) ================== - Update to aioredis 1.0+. The aiohttp-session 2.0 is not compatible with aioredis 0.X (#234) 1.2.1 (2017-11-20) ================== - Pin aioredis<1.0 (#231) 1.2.0 (2017-11-06) ================== - Add MemcachedStorage (#224) 1.1.0 (2017-11-03) ================== - Upgrade middleware to new style from aiohttp 2.3+ 1.0.1 (2017-09-13) ================== - Add key_factory attribute for redis_storage (#205) 1.0.0 (2017-07-27) ================== - Catch decoder exception in RedisStorage on data load (#175) - Specify domain and path on cookie deletion (#171) 0.8.0 (2016-12-04) ================== - Use `time.time()` instead of `time.monotonic()` for absolute times (#81) 0.7.0 (2016-09-24) ================== - Fix tests to be compatible with aiohttp upstream API for client cookies 0.6.0 (2016-09-08) ================== - Add expires field automatically to support older browsers (#43) - Respect session.max_age in redis storage #45 - Always pass default max_age from storage into session (#45) 0.5.0 (2016-02-21) ================== - Handle cryptography.fernet.InvalidToken exception by providing an empty session (#29) 0.4.0 (2016-01-06) ================== - Add optional NaCl encrypted storage (#20) - Relax EncryptedCookieStorage to accept base64 encoded string, e.g. generated by Fernet.generate_key. - Add setup() function - Save the session even on exception in the middleware chain 0.3.0 (2015-11-20) ================== - Reflect aiohttp changes: minimum required Python version is 3.4.1 - Use explicit 'aiohttp_session' package 0.2.0 (2015-09-07) ================== - Add session.created property (#14) - Replaced PyCrypto with crypthography library (#16) 0.1.2 (2015-08-07) ================== - Add manifest file (#15) 0.1.1 (2015-04-20) ================== - Fix #7: stop cookie name growing each time session is saved 0.1.0 (2015-04-13) ================== - First public release aiohttp-session-2.12.1/LICENSE000066400000000000000000000011271467501771200157460ustar00rootroot00000000000000 Copyright 2015-2020 aio-libs collaboration. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. aiohttp-session-2.12.1/MANIFEST.in000066400000000000000000000002341467501771200164750ustar00rootroot00000000000000include LICENSE include CHANGES.txt include README.rst include Makefile graft aiohttp_session graft docs graft tests global-exclude *.pyc prune docs/_build aiohttp-session-2.12.1/Makefile000066400000000000000000000017031467501771200164010ustar00rootroot00000000000000# Some simple testing tasks (sorry, UNIX only). setup: pip install -r requirements-dev.txt python -m pre_commit install flake fmt: python -m pre_commit run --all-files test: py.test ./tests/ mypy lint: fmt mypy vtest: develop py.test ./tests/ cov cover coverage: py.test --cov aiohttp_session --cov-report html --cov-report=xml ./tests/ @echo "open file://`pwd`/coverage/index.html" clean: rm -rf `find . -name __pycache__` rm -f `find . -type f -name '*.py[co]' ` rm -f `find . -type f -name '*~' ` rm -f `find . -type f -name '.*~' ` rm -f `find . -type f -name '@*' ` rm -f `find . -type f -name '#*#' ` rm -f `find . -type f -name '*.orig' ` rm -f `find . -type f -name '*.rej' ` rm -f .coverage rm -rf coverage rm -rf build rm -rf cover # make -C docs clean python setup.py clean doc: make -C docs html @echo "open file://`pwd`/docs/_build/html/index.html" .PHONY: all build venv flake test vtest testloop cov clean doc lint aiohttp-session-2.12.1/README.rst000066400000000000000000000065501467501771200164350ustar00rootroot00000000000000aiohttp_session =============== .. image:: https://github.com/aio-libs/aiohttp-session/actions/workflows/ci.yaml/badge.svg?branch=master :target: https://github.com/aio-libs/aiohttp-session/actions/workflows/ci.yaml .. image:: https://codecov.io/github/aio-libs/aiohttp-session/coverage.svg?branch=master :target: https://codecov.io/github/aio-libs/aiohttp-session .. image:: https://readthedocs.org/projects/aiohttp-session/badge/?version=latest :target: https://aiohttp-session.readthedocs.io/ .. image:: https://img.shields.io/pypi/v/aiohttp-session.svg :target: https://pypi.python.org/pypi/aiohttp-session The library provides sessions for `aiohttp.web`__. .. _aiohttp_web: https://aiohttp.readthedocs.io/en/latest/web.html __ aiohttp_web_ Usage ----- The library allows us to store user-specific data into a session object. The session object has a dict-like interface (operations like ``session[key] = value``, ``value = session[key]`` etc. are present). Before processing the session in a web-handler, you have to register the *session middleware* in ``aiohttp.web.Application``. A trivial usage example: .. code:: python import time from cryptography import fernet from aiohttp import web from aiohttp_session import setup, get_session from aiohttp_session.cookie_storage import EncryptedCookieStorage async def handler(request): session = await get_session(request) last_visit = session['last_visit'] if 'last_visit' in session else None session['last_visit'] = time.time() text = 'Last visited: {}'.format(last_visit) return web.Response(text=text) def make_app(): app = web.Application() fernet_key = fernet.Fernet.generate_key() f = fernet.Fernet(fernet_key) setup(app, EncryptedCookieStorage(f)) app.router.add_get('/', handler) return app web.run_app(make_app()) All storages use an HTTP Cookie named ``AIOHTTP_SESSION`` for storing data. This can be modified by passing the keyword argument ``cookie_name`` to the storage class of your choice. Available session storages are: * ``aiohttp_session.SimpleCookieStorage()`` -- keeps session data as a plain JSON string in the cookie body. Use the storage only for testing purposes, it's very non-secure. * ``aiohttp_session.cookie_storage.EncryptedCookieStorage(secret_key)`` -- stores the session data into a cookie as ``SimpleCookieStorage`` but encodes it via AES cipher. ``secrect_key`` is a ``bytes`` key for AES encryption/decryption, the length should be 32 bytes. Requires ``cryptography`` library:: $ pip install aiohttp_session[secure] * ``aiohttp_session.redis_storage.RedisStorage(redis_pool)`` -- stores JSON encoded data in *redis*, keeping only the redis key (a random UUID) in the cookie. ``redis_pool`` is a ``redis`` object, created by ``await aioredis.from_url(...)`` call. $ pip install aiohttp_session[aioredis] Developing ---------- Install for local development:: $ make setup Run linters:: $ make lint Run tests:: $ make test Third party extensions ---------------------- * `aiohttp_session_mongo `_ * `aiohttp_session_dynamodb `_ License ------- ``aiohttp_session`` is offered under the Apache 2 license. aiohttp-session-2.12.1/aiohttp_session/000077500000000000000000000000001467501771200201535ustar00rootroot00000000000000aiohttp-session-2.12.1/aiohttp_session/__init__.py000066400000000000000000000236171467501771200222750ustar00rootroot00000000000000"""User sessions for aiohttp.web.""" __version__ = "2.12.1" import abc import json import time from typing import ( Any, Callable, Dict, Iterator, MutableMapping, Optional, TypedDict, Union, cast, ) from aiohttp import web from aiohttp.typedefs import Handler, Middleware class _CookieParams(TypedDict, total=False): domain: Optional[str] max_age: Optional[int] path: str secure: Optional[bool] httponly: bool samesite: Optional[str] expires: str class SessionData(TypedDict, total=False): created: int session: Dict[str, Any] class Session(MutableMapping[str, Any]): """Session dict-like object.""" def __init__( self, identity: Optional[Any], *, data: Optional[SessionData], new: bool, max_age: Optional[int] = None, ) -> None: self._changed: bool = False self._mapping: Dict[str, Any] = {} self._identity = identity if data != {} else None self._new = new if data != {} else True self._max_age = max_age created = data.get("created", None) if data else None session_data = data.get("session", None) if data else None now = int(time.time()) age = now - created if created else now if max_age is not None and age > max_age: session_data = None if self._new or created is None: self._created = now else: self._created = created if session_data is not None: self._mapping.update(session_data) def __repr__(self) -> str: return "<{} [new:{}, changed:{}, created:{}] {!r}>".format( self.__class__.__name__, self.new, self._changed, self.created, self._mapping, ) @property def new(self) -> bool: return self._new @property def identity(self) -> Optional[Any]: # type: ignore[misc] return self._identity @property def created(self) -> int: return self._created @property def empty(self) -> bool: return not bool(self._mapping) @property def max_age(self) -> Optional[int]: return self._max_age @max_age.setter def max_age(self, value: Optional[int]) -> None: self._max_age = value def changed(self) -> None: self._changed = True def invalidate(self) -> None: self._changed = True self._mapping = {} def set_new_identity(self, identity: Optional[Any]) -> None: if not self._new: raise RuntimeError("Can't change identity for a session which is not new") self._identity = identity def __len__(self) -> int: return len(self._mapping) def __iter__(self) -> Iterator[str]: return iter(self._mapping) def __contains__(self, key: object) -> bool: return key in self._mapping def __getitem__(self, key: str) -> Any: return self._mapping[key] def __setitem__(self, key: str, value: Any) -> None: self._mapping[key] = value self._changed = True self._created = int(time.time()) def __delitem__(self, key: str) -> None: del self._mapping[key] self._changed = True self._created = int(time.time()) SESSION_KEY = "aiohttp_session" STORAGE_KEY = "aiohttp_session_storage" async def get_session(request: web.Request) -> Session: session = request.get(SESSION_KEY) if session is None: storage = request.get(STORAGE_KEY) if storage is None: raise RuntimeError( "Install aiohttp_session middleware " "in your aiohttp.web.Application" ) session = await storage.load_session(request) if not isinstance(session, Session): raise RuntimeError( "Installed {!r} storage should return session instance " "on .load_session() call, got {!r}.".format(storage, session) ) request[SESSION_KEY] = session return session async def new_session(request: web.Request) -> Session: storage = request.get(STORAGE_KEY) if storage is None: raise RuntimeError( "Install aiohttp_session middleware " "in your aiohttp.web.Application" ) session = await storage.new_session() if not isinstance(session, Session): raise RuntimeError( "Installed {!r} storage should return session instance " "on .load_session() call, got {!r}.".format(storage, session) ) request[SESSION_KEY] = session return session def session_middleware(storage: "AbstractStorage") -> Middleware: if not isinstance(storage, AbstractStorage): raise RuntimeError(f"Expected AbstractStorage got {storage}") @web.middleware async def factory(request: web.Request, handler: Handler) -> web.StreamResponse: request[STORAGE_KEY] = storage raise_response = False # TODO aiohttp 4: # Remove Union from response, and drop the raise_response variable response: Union[web.StreamResponse, web.HTTPException] try: response = await handler(request) except web.HTTPException as exc: response = exc raise_response = True if not isinstance(response, (web.StreamResponse, web.HTTPException)): raise RuntimeError(f"Expect response, not {type(response)!r}") if not isinstance(response, (web.Response, web.HTTPException)): # likely got websocket or streaming return response if response.prepared: raise RuntimeError("Cannot save session data into prepared response") session = request.get(SESSION_KEY) if session is not None: if session._changed: await storage.save_session(request, response, session) if raise_response: raise cast(web.HTTPException, response) return response return factory def setup(app: web.Application, storage: "AbstractStorage") -> None: """Setup the library in aiohttp fashion.""" app.middlewares.append(session_middleware(storage)) class AbstractStorage(metaclass=abc.ABCMeta): def __init__( self, *, cookie_name: str = "AIOHTTP_SESSION", domain: Optional[str] = None, max_age: Optional[int] = None, path: str = "/", secure: Optional[bool] = None, httponly: bool = True, samesite: Optional[str] = None, encoder: Callable[[object], str] = json.dumps, decoder: Callable[[str], Any] = json.loads, ) -> None: self._cookie_name = cookie_name self._cookie_params = _CookieParams( domain=domain, max_age=max_age, path=path, secure=secure, httponly=httponly, samesite=samesite, ) self._max_age = max_age self._encoder = encoder self._decoder = decoder @property def cookie_name(self) -> str: return self._cookie_name @property def max_age(self) -> Optional[int]: return self._max_age @property def cookie_params(self) -> _CookieParams: return self._cookie_params def _get_session_data(self, session: Session) -> SessionData: if session.empty: return {} return {"created": session.created, "session": session._mapping} async def new_session(self) -> Session: return Session(None, data=None, new=True, max_age=self.max_age) @abc.abstractmethod async def load_session(self, request: web.Request) -> Session: pass @abc.abstractmethod async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: pass def load_cookie(self, request: web.Request) -> Optional[str]: return request.cookies.get(self._cookie_name) def save_cookie( self, response: web.StreamResponse, cookie_data: str, *, max_age: Optional[int] = None, ) -> None: params = self._cookie_params.copy() if max_age is not None: params["max_age"] = max_age t = time.gmtime(time.time() + max_age) params["expires"] = time.strftime("%a, %d-%b-%Y %T GMT", t) if not cookie_data: response.del_cookie( self._cookie_name, domain=params["domain"], path=params["path"] ) else: response.set_cookie(self._cookie_name, cookie_data, **params) class SimpleCookieStorage(AbstractStorage): """Simple JSON storage. Doesn't any encryption/validation, use it for tests only""" def __init__( self, *, cookie_name: str = "AIOHTTP_SESSION", domain: Optional[str] = None, max_age: Optional[int] = None, path: str = "/", secure: Optional[bool] = None, httponly: bool = True, samesite: Optional[str] = None, encoder: Callable[[object], str] = json.dumps, decoder: Callable[[str], Any] = json.loads, ) -> None: super().__init__( cookie_name=cookie_name, domain=domain, max_age=max_age, path=path, secure=secure, httponly=httponly, samesite=samesite, encoder=encoder, decoder=decoder, ) async def load_session(self, request: web.Request) -> Session: cookie = self.load_cookie(request) if cookie is None: return Session(None, data=None, new=True, max_age=self.max_age) data = self._decoder(cookie) return Session(None, data=data, new=False, max_age=self.max_age) async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: cookie_data = self._encoder(self._get_session_data(session)) self.save_cookie(response, cookie_data, max_age=session.max_age) aiohttp-session-2.12.1/aiohttp_session/cookie_storage.py000066400000000000000000000051061467501771200235240ustar00rootroot00000000000000import base64 import json from typing import Any, Callable, Optional, Union from aiohttp import web from cryptography import fernet from cryptography.fernet import InvalidToken from . import AbstractStorage, Session from .log import log class EncryptedCookieStorage(AbstractStorage): """Encrypted JSON storage.""" def __init__( self, secret_key: Union[str, bytes, bytearray, fernet.Fernet], *, cookie_name: str = "AIOHTTP_SESSION", domain: Optional[str] = None, max_age: Optional[int] = None, path: str = "/", secure: Optional[bool] = None, httponly: bool = True, samesite: Optional[str] = None, encoder: Callable[[object], str] = json.dumps, decoder: Callable[[str], Any] = json.loads ) -> None: super().__init__( cookie_name=cookie_name, domain=domain, max_age=max_age, path=path, secure=secure, httponly=httponly, samesite=samesite, encoder=encoder, decoder=decoder, ) if isinstance(secret_key, fernet.Fernet): self._fernet = secret_key else: if isinstance(secret_key, (bytes, bytearray)): secret_key = base64.urlsafe_b64encode(secret_key) self._fernet = fernet.Fernet(secret_key) async def load_session(self, request: web.Request) -> Session: cookie = self.load_cookie(request) if cookie is None: return Session(None, data=None, new=True, max_age=self.max_age) else: try: data = self._decoder( self._fernet.decrypt( cookie.encode("utf-8"), ttl=self.max_age ).decode("utf-8") ) return Session(None, data=data, new=False, max_age=self.max_age) except InvalidToken: log.warning( "Cannot decrypt cookie value, " "create a new fresh session" ) return Session(None, data=None, new=True, max_age=self.max_age) async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: if session.empty: return self.save_cookie(response, "", max_age=session.max_age) cookie_data = self._encoder(self._get_session_data(session)).encode("utf-8") self.save_cookie( response, self._fernet.encrypt(cookie_data).decode("utf-8"), max_age=session.max_age, ) aiohttp-session-2.12.1/aiohttp_session/log.py000066400000000000000000000000651467501771200213070ustar00rootroot00000000000000import logging log = logging.getLogger(__package__) aiohttp-session-2.12.1/aiohttp_session/memcached_storage.py000066400000000000000000000061661467501771200241700ustar00rootroot00000000000000import json import uuid from time import time from typing import Any, Callable, Optional import aiomcache from aiohttp import web from . import AbstractStorage, Session class MemcachedStorage(AbstractStorage): """Memcached storage""" def __init__( self, memcached_conn: aiomcache.Client, *, cookie_name: str = "AIOHTTP_SESSION", domain: Optional[str] = None, max_age: Optional[int] = None, path: str = "/", secure: Optional[bool] = None, httponly: bool = True, samesite: Optional[str] = None, key_factory: Callable[[], str] = lambda: uuid.uuid4().hex, encoder: Callable[[object], str] = json.dumps, decoder: Callable[[str], Any] = json.loads ) -> None: super().__init__( cookie_name=cookie_name, domain=domain, max_age=max_age, path=path, secure=secure, httponly=httponly, samesite=samesite, encoder=encoder, decoder=decoder, ) self._key_factory = key_factory self.conn = memcached_conn async def load_session(self, request: web.Request) -> Session: cookie = self.load_cookie(request) if cookie is None: return Session(None, data=None, new=True, max_age=self.max_age) else: key = str(cookie) stored_key = (self.cookie_name + "_" + key).encode("utf-8") data_b = await self.conn.get(stored_key) if data_b is None: return Session(None, data=None, new=True, max_age=self.max_age) data = data_b.decode("utf-8") try: sess_data = self._decoder(data) except ValueError: sess_data = None return Session(key, data=sess_data, new=False, max_age=self.max_age) async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: key = session.identity if key is None: key = self._key_factory() self.save_cookie(response, key, max_age=session.max_age) else: if session.empty: self.save_cookie(response, "", max_age=session.max_age) else: key = str(key) self.save_cookie(response, key, max_age=session.max_age) data = self._encoder(self._get_session_data(session)) max_age = session.max_age # https://github.com/memcached/memcached/wiki/Programming#expiration # "Expiration times can be set from 0, meaning "never expire", to # 30 days. Any time higher than 30 days is interpreted as a Unix # timestamp date. If you want to expire an object on January 1st of # next year, this is how you do that." if max_age is None: expire = 0 elif max_age > 30 * 24 * 60 * 60: expire = int(time()) + max_age else: expire = max_age stored_key = (self.cookie_name + "_" + key).encode("utf-8") await self.conn.set(stored_key, data.encode("utf-8"), exptime=expire) aiohttp-session-2.12.1/aiohttp_session/nacl_storage.py000066400000000000000000000050531467501771200231710ustar00rootroot00000000000000import binascii import json from typing import Any, Callable, Optional import nacl.exceptions import nacl.secret import nacl.utils from aiohttp import web from nacl.encoding import Base64Encoder from . import AbstractStorage, Session from .log import log class NaClCookieStorage(AbstractStorage): """NaCl Encrypted JSON storage.""" def __init__( self, secret_key: bytes, *, cookie_name: str = "AIOHTTP_SESSION", domain: Optional[str] = None, max_age: Optional[int] = None, path: str = "/", secure: Optional[bool] = None, httponly: bool = True, samesite: Optional[str] = None, encoder: Callable[[object], str] = json.dumps, decoder: Callable[[str], Any] = json.loads ) -> None: super().__init__( cookie_name=cookie_name, domain=domain, max_age=max_age, path=path, secure=secure, httponly=httponly, samesite=samesite, encoder=encoder, decoder=decoder, ) self._secretbox = nacl.secret.SecretBox(secret_key) def empty_session(self) -> Session: return Session(None, data=None, new=True, max_age=self.max_age) async def load_session(self, request: web.Request) -> Session: cookie = self.load_cookie(request) if cookie is None: return self.empty_session() else: try: data = self._decoder( self._secretbox.decrypt( cookie.encode("utf-8"), encoder=Base64Encoder ).decode("utf-8") ) return Session(None, data=data, new=False, max_age=self.max_age) except (binascii.Error, nacl.exceptions.CryptoError): log.warning( "Cannot decrypt cookie value, " "create a new fresh session" ) return self.empty_session() async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: if session.empty: return self.save_cookie(response, "", max_age=session.max_age) cookie_data = self._encoder(self._get_session_data(session)).encode("utf-8") nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE) self.save_cookie( response, self._secretbox.encrypt(cookie_data, nonce, encoder=Base64Encoder).decode( "utf-8" ), max_age=session.max_age, ) aiohttp-session-2.12.1/aiohttp_session/py.typed000066400000000000000000000000001467501771200216400ustar00rootroot00000000000000aiohttp-session-2.12.1/aiohttp_session/redis_storage.py000066400000000000000000000065311467501771200233640ustar00rootroot00000000000000import json import uuid from typing import Any, Callable, Optional from aiohttp import web from . import AbstractStorage, Session try: from redis import VERSION as REDIS_VERSION, asyncio as aioredis except ImportError: # pragma: no cover try: import aioredis # type: ignore[import-not-found, no-redef] # noqa: I900 except ImportError: aioredis = None # type: ignore[assignment] else: import warnings warnings.warn("aioredis library is deprecated, please replace with redis.", DeprecationWarning, stacklevel=1) REDIS_VERSION = (4, 3) class RedisStorage(AbstractStorage): """Redis storage""" def __init__( self, redis_pool: "aioredis.Redis", *, cookie_name: str = "AIOHTTP_SESSION", domain: Optional[str] = None, max_age: Optional[int] = None, path: str = "/", secure: Optional[bool] = None, httponly: bool = True, samesite: Optional[str] = None, key_factory: Callable[[], str] = lambda: uuid.uuid4().hex, encoder: Callable[[object], str] = json.dumps, decoder: Callable[[str], Any] = json.loads, ) -> None: super().__init__( cookie_name=cookie_name, domain=domain, max_age=max_age, path=path, secure=secure, httponly=httponly, samesite=samesite, encoder=encoder, decoder=decoder, ) if aioredis is None: raise RuntimeError("Please install redis") # May have installed aioredis separately (without aiohttp-session[aioredis]). if REDIS_VERSION < (4, 3): raise RuntimeError("redis<4.3 is not supported") self._key_factory = key_factory if not isinstance(redis_pool, aioredis.Redis): raise TypeError(f"Expected redis.asyncio.Redis got {type(redis_pool)}") self._redis = redis_pool async def load_session(self, request: web.Request) -> Session: cookie = self.load_cookie(request) if cookie is None: return Session(None, data=None, new=True, max_age=self.max_age) else: key = str(cookie) data_bytes = await self._redis.get(self.cookie_name + "_" + key) if data_bytes is None: return Session(None, data=None, new=True, max_age=self.max_age) data_str = data_bytes.decode("utf-8") try: data = self._decoder(data_str) except ValueError: data = None return Session(key, data=data, new=False, max_age=self.max_age) async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: key = session.identity if key is None: key = self._key_factory() self.save_cookie(response, key, max_age=session.max_age) else: if session.empty: self.save_cookie(response, "", max_age=session.max_age) else: key = str(key) self.save_cookie(response, key, max_age=session.max_age) data_str = self._encoder(self._get_session_data(session)) await self._redis.set( self.cookie_name + "_" + key, data_str, ex=session.max_age, ) aiohttp-session-2.12.1/codecov.yml000066400000000000000000000001011467501771200170750ustar00rootroot00000000000000coverage: status: patch: default: target: 90 aiohttp-session-2.12.1/demo/000077500000000000000000000000001467501771200156645ustar00rootroot00000000000000aiohttp-session-2.12.1/demo/flash_messages_example.py000066400000000000000000000034261467501771200227420ustar00rootroot00000000000000import base64 from typing import Awaitable, Callable, List, NoReturn, cast from aiohttp import web from aiohttp.typedefs import Handler from cryptography import fernet from aiohttp_session import get_session, setup from aiohttp_session.cookie_storage import EncryptedCookieStorage def flash(request: web.Request, message: str) -> None: request.setdefault("flash_outgoing", []).append(message) def get_messages(request: web.Request) -> List[str]: return cast(List[str], request.pop("flash_incoming")) @web.middleware async def flash_middleware(request: web.Request, handler: Handler) -> web.StreamResponse: session = await get_session(request) request["flash_incoming"] = session.pop("flash", []) try: return await handler(request) finally: session["flash"] = request.get("flash_incoming", []) + request.get( "flash_outgoing", [] ) async def flash_handler(request: web.Request) -> NoReturn: flash(request, "You have just visited flash page") raise web.HTTPFound("/") async def handler(request: web.Request) -> web.Response: text = "No flash messages yet" messages = get_messages(request) if messages: text = "Messages: {}".format(",".join(messages)) return web.Response(text=text) def make_app() -> web.Application: app = web.Application() # secret_key must be 32 url-safe base64-encoded bytes fernet_key = fernet.Fernet.generate_key() secret_key = base64.urlsafe_b64decode(fernet_key) setup(app, EncryptedCookieStorage(secret_key)) # Install flash middleware (must be installed after aiohttp-session middleware). app.middlewares.append(flash_middleware) app.router.add_get("/", handler) app.router.add_get("/flash", flash_handler) return app web.run_app(make_app()) aiohttp-session-2.12.1/demo/login_required_example.py000066400000000000000000000052761467501771200227730ustar00rootroot00000000000000import base64 from http import HTTPStatus from typing import Any, Awaitable, Callable from aiohttp import web from cryptography import fernet from aiohttp_session import get_session, new_session, setup from aiohttp_session.cookie_storage import EncryptedCookieStorage DATABASE = [ ("admin", "admin"), ] _Handler = Callable[[web.Request], Awaitable[web.StreamResponse]] user_key = web.AppKey("user", str) def login_required(fn: _Handler) -> _Handler: async def wrapped( request: web.Request, *args: Any, **kwargs: Any ) -> web.StreamResponse: app = request.app router = app.router session = await get_session(request) if "user_id" not in session: raise web.HTTPFound(router["login"].url_for()) user_id = session["user_id"] # actually load user from your database (e.g. with aiopg) user = DATABASE[user_id] app[user_key] = user return await fn(request, *args, **kwargs) return wrapped @login_required async def handler(request: web.Request) -> web.Response: user = request.app[user_key] return web.Response(text=f"User {user} authorized") tmpl = """\
""" async def login_page(request: web.Request) -> web.Response: return web.Response(content_type="text/html", text=tmpl) async def login(request: web.Request) -> web.Response: router = request.app.router form = await request.post() user_signature = (form["name"], form["password"]) # actually implement business logic to check user credentials try: user_id = DATABASE.index(user_signature) # type: ignore[arg-type] # Always use `new_session` during login to guard against # Session Fixation. See aiohttp-session#281 session = await new_session(request) session["user_id"] = user_id raise web.HTTPFound(router["restricted"].url_for()) except ValueError: return web.Response(text="No such user", status=HTTPStatus.FORBIDDEN) def make_app() -> web.Application: app = web.Application() # secret_key must be 32 url-safe base64-encoded bytes fernet_key = fernet.Fernet.generate_key() secret_key = base64.urlsafe_b64decode(fernet_key) setup(app, EncryptedCookieStorage(secret_key)) app.router.add_get("/", handler, name="restricted") app.router.add_get("/login", login_page, name="login") app.router.add_post("/login", login) return app web.run_app(make_app()) aiohttp-session-2.12.1/demo/main.py000066400000000000000000000015111467501771200171600ustar00rootroot00000000000000import base64 import time from aiohttp import web from cryptography import fernet from aiohttp_session import get_session, setup from aiohttp_session.cookie_storage import EncryptedCookieStorage async def handler(request: web.Request) -> web.Response: session = await get_session(request) last_visit = session["last_visit"] if "last_visit" in session else None session["last_visit"] = time.time() text = f"Last visited: {last_visit}" return web.Response(text=text) def make_app() -> web.Application: app = web.Application() # secret_key must be 32 url-safe base64-encoded bytes fernet_key = fernet.Fernet.generate_key() secret_key = base64.urlsafe_b64decode(fernet_key) setup(app, EncryptedCookieStorage(secret_key)) app.router.add_get("/", handler) return app web.run_app(make_app()) aiohttp-session-2.12.1/demo/memcached_storage.py000066400000000000000000000014031467501771200216660ustar00rootroot00000000000000import asyncio import time import aiomcache from aiohttp import web from aiohttp_session import get_session, setup from aiohttp_session.memcached_storage import MemcachedStorage async def handler(request: web.Request) -> web.Response: session = await get_session(request) last_visit = session["last_visit"] if "last_visit" in session else None session["last_visit"] = time.time() text = f"Last visited: {last_visit}" return web.Response(text=text) async def make_app() -> web.Application: app = web.Application() mc = aiomcache.Client("127.0.0.1", 11211) setup(app, MemcachedStorage(mc)) app.router.add_get("/", handler) return app loop = asyncio.get_event_loop() app = loop.run_until_complete(make_app()) web.run_app(app) aiohttp-session-2.12.1/demo/redis_storage.py000066400000000000000000000017121467501771200210710ustar00rootroot00000000000000import time from typing import AsyncIterator from aiohttp import web from redis import asyncio as aioredis from aiohttp_session import get_session, setup from aiohttp_session.redis_storage import RedisStorage async def handler(request: web.Request) -> web.Response: session = await get_session(request) last_visit = session["last_visit"] if "last_visit" in session else None session["last_visit"] = time.time() text = f"Last visited: {last_visit}" return web.Response(text=text) async def redis_pool(app: web.Application) -> AsyncIterator[None]: redis_address = "redis://127.0.0.1:6379" async with aioredis.from_url(redis_address) as redis: # type: ignore[no-untyped-call] storage = RedisStorage(redis) setup(app, storage) yield def make_app() -> web.Application: app = web.Application() app.cleanup_ctx.append(redis_pool) app.router.add_get("/", handler) return app web.run_app(make_app()) aiohttp-session-2.12.1/docs/000077500000000000000000000000001467501771200156705ustar00rootroot00000000000000aiohttp-session-2.12.1/docs/Makefile000066400000000000000000000164251467501771200173400ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/aiohttp_session.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aiohttp_session.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/aiohttp_session" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aiohttp_session" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." aiohttp-session-2.12.1/docs/_static/000077500000000000000000000000001467501771200173165ustar00rootroot00000000000000aiohttp-session-2.12.1/docs/_static/aiohttp-icon-128x128.png000066400000000000000000001533251467501771200233660ustar00rootroot00000000000000‰PNG  IHDR€€Ė*ßgAMAą üa cHRMz&€„ú€ču0ę`:˜pœēQ<bKGD˙˙˙˙˙˙ X÷Ü pHYsHHFÉk>€IDATxÚėŊu˜ײ.ūÖjųd|†ÜŨŨ-¸  ,AA’ x€H  !¸ģģģģÃĀø~ÖŨĢ~ĖėœMBdßsĪŊįÜß^ÎĮņ/ņn¸á† œp‰Ō(ŌT•ĒRUTFeTF4A´@ ´ æÔœšgũKÔAT@T@~äG~"˙ū8˙ęP˙Ŋ82 ô6nãv˙€đÉ_ō—Ü“{rO  âá~ČųĄŧ%oÉ[ŧ—÷ō^NāNāb\Œ‹q=ŽĮõØÃöāNāáe1@)”B)”@ ” īč;úŽĸ(Šĸ(eŖlDDDBĒPI'tzI/é%Ũ¤›t“Ņ!:„y˜‡yô}A_ā}ŧ÷QĩQyy˛žōīņoxÃ`08ëTŪŽíØŽQ…QY$+X°KåRšT~ōŦH+Ԋ”™>—ŸËĪŅmĐŸá3|†UX…Uä&7šEqQ\Ī$\LÃ4LË"ŲOđ >ÉzúyœĮų,VéË}š¯ôIŸôq"'r"įá<œ‡§ņ4ž–ɐ´ŸöĶ~ZH iĄrQš¨\eDQFŠĻTSljcâ˜8F9(堓t’NŌjZMĢŗžX5Pūđ‡˙ŋ?>ũ˙Tz…Wx…ÕXÕ܎Ûq;ÖYgķb^,×ËõrŊų‹ų‹ų‹ÕĐjh5´ēZ]­ŽÜ•ģrWņ‘øH|¤ŠĄJ)Ĩ”RJq+nÅ-Ūo‰ˇÄąEl!&&Æ,ĖÂŦ,eæm\Ā\Ā0 Ã0VYeUž+ߕīĘëōēŧnąŠXEŦD+ŅJ´ęXuŦ:|˜ķáLö;ÅNąSŨ¤nR7)ĩ”ZJ-ĨŠREŠB¨5ĸGôˆaf`F–B倎3Ā˙‹Ã€ĮqĮŅ=ЃŨėfˇ$ÉAVmĢļUÛōudĶ*dœãÚ2Ęˆōˆ59ëŽ~ĸžō%ųÕxR”‡Z5ę¨ĖÕ7ŠĻjŨ bJ==ģi…ƒœČ\˙Š7܋ŊØkU°*X|ážp_¸ŅÚhm´æá<œ‡kģ´]Ú.­œVN+§D)QJ}JŸŌ§™ļ @@@ü›ū{ <đ`>æc>§r*§Ę|2ŸĖg ´ZŊ—Ŋ—Ŋ—Í[æ-ķ–úĨúĨúĨí í í ^L/ĻC’ô/?ą)—#ŦvŪôô§fhÆũø æĸԕ¯ę›'ĶZŊ|Ī\˜ĻŋŒ‘įRg<åĨž ‡ø]Ë( ?ųĐHAy[ž…|"d9UŸa6õĮ+QJ9„4Q@ E{X¸KëM_LŊíQĄODe{zd;ĩ`íœßĢu‚Zäę¤ö眺ēúŌ?,ŨuŽļšĖ•˜hpëĀ*šĢ%ÃgkjĘuĨÂpû­šJėTüv†ŽĀ<ĄO˙bOîáîY­ŠVE÷D÷D÷Ds°9ØŦO×§ëĶuKˇtKY­ŦVVS{jOí3ŨĮ˙ŗŧI˙Ā„ s1sy¯âUÖ8kœ5ΘaĖ0fxV{V{V‹]b—Øåô8=NVJ+Ĩ•Ęríũš š`¤y~ō։Í}§ģĢÆŊúsĨĸ§\H—2l/ iãhœačÔ &iÔĒĸ´ãģĀ*”C<ˇ0›ųÃ%ū Ąŧ0ĻŏFŠŲ;u 䤰@ĐØ‚ Ô§ k?vfˇŌÜÖ Œ(ėúĮw@>´ ‡ĩßģŽŊFÔ•`Ū‹zNķx8MÎĨFz\@WĘa¯ũEDt­™ŽžšI#䂧{ áķIĨUâkh- ËGŽâ!WÍ`ŒQ†ûfš%dnc<_SīËōŽ2Ų/j÷"Z–ũÉyĒĀÎÚīÛN„Wd€¨ å˛ũ‹¯p'pÂ(e”2JšÜ îŽæhŽļEÚ"m‘Zĸ–¨%Š $‚ČCō "*ĸâŋā?7ĸčĖø¨ 2Ā,`0 xŠ{Š{Š[w­ģÖ]G]G]G]ûnûnûn\Æe\ūÃŲ^ā!.›9Ũ]’׸E÷Ŋ’Ī“đ´Úšr׋Ī΅jŲ]]^ĩÍTËŌXĩ™ō>‹\¨Īé!ÉԐgōKčGdËī/Ųė( ‰|MēUŲ5ŒM_ØĢE01kp Đ?ī3€hŪO6‘¨cKī´&Ĩ ŋŦ˙WŲ)3HõmIéČ.™hÄ!ƒuy†*Ē͜])õQˉ žîΝ_úJéåąĮ¯ŗ–'ŌVĒEŌkDwRŽąÍ ÂØ"KQ}^ĖÉ0q;‘ÄRĸ:GŠ#9¨Jũ,äˇ"AŪ{¯”+Ũ9äŖūåb Ōĩüŧl¨œõ0ˆTs¨k9'ûžHŊGš”ņúF¤g¯8P4ÍCoUĮaŸ•Ú@ދLöŪ¯Cļ[āā`g,ˆá¤Š?ÛüØ˛_ | Ļh1ž4Äã.ü ƒĄ  ŪâÃԖbų>ĘŌĢö)Í{Jŗ NŪÛŦŲ⭟lÍÂĘék#‚KwöSdwƒ—ęĸ€Ņ9š#ųPúĨhŲAvp9]N—ĶÚjmĩļÚØØhĩĩÚZm‘Sä9é9=§įY Œ3Ā_„̆b(†ĘОĸŦhļF[Ŗ=Ų=Ų=Ų3õ{gCgCgCûzûzûúŦĀū ̎ģdŌŽôÔßĩģà Ú_’ą˙Å$g Ÿ;0ŲæŗvÄPˇä€Į×xŋ§pr G‚‘ŧlBŖ@0ÄkPpˇ’]Íkķƒå˜´Ēš:õ {Œ¤7‰ŧ€ŋ?Yen‘F¤L€Đå`/úķN/>å S´ŊčW sķŖ e„\õboJ%¸o|ÃÉr‘uNŠ~Ļ–d—é6&_Ņŋ…âØ´‘=Ęc}€kØúë;gūÍ0I……tøÃã„âÔXŊîŧŌŪ'l—×/ĩJÜޏ¤‡1{Ŧ™ū#ķ6r$–\ôör˙ÜÅv6i­|įĖŌįWõ#~ďŪoŠ7%BĨĐNh'lĩlĩlĩ2]ĖY`žˇđŪú7ü–(’„â(ŽâÖYëŦuÖėhv4;f暏‚+¸Đ& M@å°rX9üGĶđ ĖoģŽ&5ųcúG7Ļm<(ŽĮDŸ;ä8%?M]ŦGQ+\œW†‹ŸÅ*ĻØx]zˇ¸–…ō €`ähŪ4ŧHƒĀût˛ë¤šÍOåCß $`–#ĩ0˜Âh5‰ö´•}˙ÄH tXXÅõČ_ ֋`¤ŗPö-Ô%īģ Ãčë€ëųnķág-/f_LũŗųĄ•á †Ų”Î dC9 € IĮHņNÜ `NÕ¯øÛ!Š#>āƒëo8ã)zĐIzWāØ,g¸$ΒÕÍ^ÆpYnĒí~aĨf2!ônqßĪå÷uŲâwą@ąšiĒrX˙菾ŠÖTkj†ĖqˆCœ}Ŧ}Ŧ}ŦšŦ&ĢÉb­X+Öf!Ļū¸P˙o3@ĸÅ/øŋ=dŲ#SÕq+nÅ­čšõÜznŋF~üQgęLß8Ãuė÷U‰Ÿō AʝĢ?oÖyĒŲŨŊŪ:1÷"û"˙Â!AbĻĨ¸Ÿ!)mäŗÜ4éîũŊæ~ų"„—[ŋHfM6‰AĐi*! E0äkĪP@|›ërW–ŪīŲPL–sLŽ%h¯˙â7 åĖuŪ­Čãˇœfų>J˙#h/1$:Ō.¤s° ¤bęZÛĪôiރÕŋ§J9>ސ›ş¸ŗ‡÷EO>š÷úæĻ~ Ę%ÆC }@Âø-!/j-k“ÍjénÂouŖ#$ķfvØūrī-¨"°–B•zōk˜ú” 'ę¤āĪ1Ūąę`‹hīŨa´áîģōą.Åš'T˙)¨gŲĄĘčBëįŋ‹<(ų&gC&*ÉuÁuÁuÁČcä1ō8žw|īø^‹Ô"ĩČLdkfœ4h˙ˇđ˙ô*Žâ*oâMŧÉb‹-övņvņvņ”÷”÷”wZNËi9T‡ęPŅíŅū [\Á ô|ŸVũVü|WâVÕīšwŋŦī\ŦuÁ>5{LSJ¤˛ÚhgW.æ˙(r­ žQ HTΝX­;jÚĖ€oáoĩ˛6r”÷ŸîæÉįÆ7ŽŊņgų™ÜiÂA[ÄHč˜A% "ų‘Š<ØJĻ­¨‡ ‘+k}ĸÔ-t­ÍYņ2¸IĄ|4EũÜž ƒå§FuŽOûųå@ë^Ôũ#)Ž’‡rāŽ™ņŨ M)R[´Ëßû­Íô˜–ā!>yq(šOůŧķ3D3> §øĖÖ€…ŗ€žĀo@ 4¤“:xGŗOŨ‡¤åáfYÕĘ ,å˙Ë/Šä AVûxŠ{3éÖUœŽE‡Í#$^Š„ŅœĸDØFŠyzī“§]#ž8–ßøîįÜR÷Ú8R¤pÃ}tFh›øÚÉڀPŋ4ŋ4ŋ4ĪĪĪ×Q×Q×Qû&û&û&ŊŒ^F/“OȤ„ŦŒˆ˙_H€S8…S™ā^sŊšŪ\ī ōyƒŧŊŊúô čĢGëŅzô sS÷Ŋ˜™Üųü{K^I~°ų‡~SՕĻÃļD)„žtÃŦĮ”gq*§`=ŋ×áĐčœ9’‚ԏÍOĐÜŪ3đ6Ũ)KWstĢAũ_åj‡æyoaޘ<ö!sŪÄīĻ`Š+4.“e5ŗžlK‰ęˇ˜ZtnģMzĪ’G:Įâļ:Øņ‡ĘNķ7˛°lų4öØQî”x˙[‘•k JÅ6ģv†ÆĨ~uŊüX.į ž¤ôár”÷ģôųp ,KŪŧųä.KN4ˇV¤õŽVėEžGk—Ė Ëä[Ī'r.aūEΏ4 āĐ”ž|ėĸ &CĨmŠ?izåˆAė ė_€đ_ST˜|īĐM.I­ÄoOsŽgŒ6ۊÚ"$×ÕjE‚—T<ß}ŠÖ/°WÎ˙h3|||2g4ÎhlnnnķŗųŲü”ÅĘbe1žáž!!ųWÄAä<šŖÍŅæhO!O!OĄLë7đUāĢĀWZ´­Ŋ‰ôw .Gt—‹USŗ_ܰ°Ŗ’6čፀļŽĮd˜]ų^_˙žƒx‡ōĶ4°ō@+I6ĘNƒą^›īĖA ˇŪ(~ßjbœ‡ƒŠĄÕoŧI ‚¸LäņÖbäLĩ°{•xMē­Ėų4s‘ĀôlšAĻ‹=ĸ˛ĩ†É­oa™Iɟ@ŅīfĪ‹Šbœ ž@Ew.)Lä´öÚSÕ‘ŽvSfZ{2ļ?ĪØ_;!GĘų5/VjŌw”3&÷äJSĐ?žūXŊƒŪAī ‚E°N/–^,ŊX&ŧÜ^Ø^Ø^XXÂ%P% aûKdžúŊ¸÷2?0?0?pûģũŨūæ7æ7æ7={öP'¨Ô7ߓ5 #­ûÍģ6ē2Žw[vŌqšŋM‰pĖĐĮ“C 4ōúÎS~÷to9NÅblāü5ՃāBi˛‹‹æöˆēžv`¨˜ €áđŠ×’†ëCŲ˛•Ø‡d,Uq_Q­š_A Ž[ā”y4Ė,[JíZāD“ĄŠĮ¯xļÎ˙ęđiw넡rŌšS3ŖÄ&;~+lØDŸd’79Qíä #ˇ{ôXÉ+ųVámØ=Å ʑúûŲ‹…î Ąã' €ŧ%›,Í`÷1ī"öÉ\@ĄĪĨ#’ltINTîy>gCä>`  jãŲôŅáûØĨ đ sū‰ U aßŖŽŪ@9ydE÷0ßZnčŽK“ƒTgXŲ=ē,-Vģe*,Ģioˆm>0˜Ō{¤÷HīĄĨiiZšŨewŲ]Ⲹ,.gfĀ!øŸĪ×p ×x,åąfwŗģŲŨ­š5ˇf5ŠEƒ= z¨ļR[Š­ŪācžhŒvŊJĻ+ÅWÕ4Æß˙tĶ5ŋUZšųŽmŌo‰.<ÅĒ F î rī0FŗÛœmū  ōČ ldŖõō)„ōĐŗ”ŨøŪĒĀ…b¯yį ąØJvÕÄ^-[XNHä NdĮYͅä|s„ΏސÛQĻáČŠŨh™vŲoéŋŧwpÛ­Ģ×Ŗ—ßOžĪ=n§/iŒ˛„ ˜$āėüø•Ėî^”ü€Ŋæv÷8Q–Ū ‘“l("CĐWvŖŒ ĀĸR¯OA>˛‹ĖØįéc| gø 6(ČIeŒo#gĐ_e0RúOž/đ‹Č&Nڎõn&û(\õˆũ€rä˜q`˜îž(‹Ũr5MVn{›YŠdFw#‡vJ›PdAģÁJų‹Ũ~ĨĩQë ē\ hĻ&§&§&ëččØė ö„, úS<ÅĶ˙jÛāŋ’žâ)žōR^ĘKÍąæXsŦg‚g‚g‚īŧīŧī|;ČäV¨Ôoؚ(¯Hũ6éÕųŽ‹ J娎Ī?ŸÍÂBŊŖxn  âT$ā%$Ę Ų,ˇ,ËĻ;Ã÷3,>Å@!$›ŌŨW‘}´ÎˇŒTXYۜ×G:ŽAĨĩb, ŊpŽüėũE)qP6—“ŧ}ĨÃ7TŧëˇģnČD'„Ļœũ_ÛküũČí×Äė‡ŧŊ,yhō)§Ãåáarĸ:p ëžīĨ|åV’vņ~ž)¯ACv”$ÍđŪeFzí`Č,˛øíZ‚`‡Ę5y Ī;F!ö™%ŦųҜ4ęfUbVæz–ÃD~ ĀBļß +˜ ¸đlÚĮÁ‹ŲĢnŨ’ãHÜ9Ø X `Ą!"¸Í%}Ëd6eBú¯‚!"_ž:MO…čUfô^ĸ„ØŽ–|ƒ4Ø`n07¤åN˝–ÛÖËÖËÖË6Ā6Ā6@YŠŦTVfåO˙—e#ü× ē“‘ŒdŽáŽąLË´Lī\ī\ī\o9o9ošĀčĀčĀč?$ũŽî‰É—¯œ:ōÃPԉ*ĩwDāÛö XŖo͇t–ûČcœŒXÄ@BBpQėSn‰CdĶLå'’Pœtō›ÔËp‚‘Ч HĖß\m(“ëY•Ą˜÷S>†Šžō6ThÜ:"}õųĒÕÃãã™<Ëjüŋ¸'iĀ-Ėžžpņ=ũŨ›XėÆulÁ:%Aß@%ĩxĮh(@:*ZÁlĸĢo ’ūd-ņH‡IOh=AÍ •Rq šķ{ qҘĨų4‚ß8ƒ €áâX戴d“ÁŪ „Ŗ?i0ŒHöÁ°ž0P*'ábč(÷”ûô âĸų(°•}ĸPTú$ž=5ø‡ųVoOËÔ.o0C;¨ÔūÉūÉūÉŪ‚Ū‚Ū‚žĩžĩžĩÖIë¤uƒ1ƒ˙įH ŦLpŦĨXŠĨxũŧ~^?W¸+Üi[e[e[õ†[G{*§$ž8ũãĒķ"úP¤)û3_^_^_^×r×r×rįYįYįYÛÛے7čú%}Å2:$ŋ¸ôūę|BÆ<9ÛØī;Į@ŊŋzļZ{Š˙ Žŗ?!ü!ĐĀˆA ļŨ–—‘ÁÛ͎,í)/˙Îq‰Ÿā0Г† ŖŗL¸’œĄHĪËWŠ÷ËØwf9€œ˙úÉPÉ]-šŨí^47m5ĪĨ\`oú“Á`!?}éafĀ‹}`œ!§(ĢL¤Šĸöeˇ¤ÜPąÄV€ņˇtb†¤ŧ č$^‚•ÖžjØíč ėāE<RXđüzŦ¨đ`G*"ØD.ÎAūĻžÚš3x*`(ČÄ_ĨŧáÁ.H˜°8ÖĒĸmËŪ ėåĀy4­’ÚęNũĩ‘É#âOŨˆYYyp÷Žâš~ËīĩŖPOĐôësësës7ģŲ͙fą6[›­ÍĻrTŽĘeUCúoĮ{ą{eYAV0ŸšOͧîžîžîžz#Ŋ‘ŪČYŲYŲų†ĘaNđž—üÖ5ŋMíņitÂņa~Í9*Ŗ˜z7ÛsRutVÚąIh€ßƒË~ĢûVƒÂš­Ãpɕn¤zy@TŌ#_Įô§ę  ‚\0¸Œsœî+˜:‹‹qÁĪm‹|Đ&Ãu;eëË9žė/ī\ĢĄĮF/ûˇâ—ržëĮ„Ēfë•–‡ęCĩžj LË{šū%’ū]L_ŦŒŲtž)ĮxCRīAĨkâ4ihŒŧ[Î0Ks˛k]BoqU- ¨ņú׹íoŦÅ×đŌę›Q˜}č†NHÔúˇB %‰t §ęHA?˛āĨR4:$bČĮš=‚ņĮøBŦ Ū÷āöRÚiRÍiļl`EđCÆö0oYßŅÍlWKöˇķŽhž ĩīMŲtÛ/Ÿ§UBŒé1wŌĢ$Ÿŋ–ÛY9$ų:Ĩ'ę@ÛkŪ?G~G~G~ëSëSëSw[w[w[šFĶhšzMŊĻ^Ŗ‚T "AúīÁ1ˆA oá-ŧŊŗâŦ8Ī%Ī%Ī%tEWt ذ-`[Ļ/čĩģ rNëvĘĀÛw•ŊžU8ŅĐ˙–’Ģž3¤ûQ/ą™a¯ž×Ų BÔ€ ˙qn+`d#›57íVa×T˛á¤õiz - L§\\ˆũÃķȃFd— Ƈœj~’q‰>ōse?+R‹™,ã[îr‰ô 7î/Œwmžģ6ÛOÖÔĘĶ?ēmX \m-Roاž6_}iYƒņĶît27ŪŠšųŠ^Â5éY~JĨoĐĨî’Ž. +ÕÆÔÂcß~J­c÷]Ũ} ˇķ€š¯ĩ‡oģĻ$Lä^=í#Ō՚ļGĘ­3L$ŋQĻ)đ >ę"VÖõ6į͒Ũ‹0]äS/āŠbØ+ã†r׿húŽĒ /č.˜Q,&RĖą\…`pI”aŨ GmŽã]ĘJ”gU΂eUW~€Šë< Œ<” ˙ΆtŽļ6Ў3…Žˆ‹ÅŧŖËž÷VnÖQ&åģ'=ÕÜöˇ‚ōúEŠ‹îæéŖŖûžŒNŠhsŽ .Zē`ĢŪ¸G”(TũŅũũöûí÷۟Ō"ĨEJ ī'ŪOŧŸˆįâšxŽĖTf*3q§qúŋ‡ ‰HDškĖ5æ{ØÃnˇÛív‡t éŌQMWĶÕô×~Ō’î?>ÂŗáFŪuŽZ]âĢuŌÛ<LCذۃ!NĒyG­ lˆ.j€8ūģ“/Ķ7ņiÜÚˇä]Ī^žkŦ€‚´ƒtŊEĀö({ėxŽCo\Áø‰ĢÃį-–\ÖœËÛB)ļĒÛzĻW3.?āåō] ˙"‹X!Ö^9Ėŗûė{xKøã˛ų•ŠÍʓRĀ™'ۇČŲĄÆM˜rŋĄsđ“æÆČĪĪ™ŧFž7 81•l¤Ûŋ ¸ĘísG9÷qėWĸ—pÁ‹(\ÄUš@Ŗ°—TÆš!íKŪĘėm†4_\ÅąÂ&qUKķŠĮxdāÎĀB:^A@ ؐaT}PŌŧī‰*æ”ô­xáOīķqî‹ö†"ēĄĻąßƒ2ŲøŸ•i˛¯œņ"­lÂûuåœĮõöžËîW§ŽtŖŋĻĄ Đ,e6Īĸibŋy\-ü"­ŠlįŋÎŪĢt™Nļ*V§A–KöŸ ŗ‰æDsbjĄÔBŠ…‡ĮáąmąmąmÉŦgnč†n˙÷$ĀĪø?g&čš]ÍŽfW÷E÷E÷Eŋ~üŧô3AˆåcũîqWŋˇ=oĀfŅŨ¤.JßDŊyŦL`Ē“HXĢI÷5s§CØWäD:ōœŖ†Pėf'`RĘBöņ ķšÉ‰SˆdĶ r÷Ä^=&5Pžˆãë˙qC"^ŗ™{4Ĩ‡õ);A\)ÔŖÍz^ōøÔÎą2=ĻĪŲŠXLk ų ­ĒМs7ĐĮheŽ{û\+ôUļs?°ŋ ÍFĪs;ę••]ĸŸŌWYI[čGô⟌å%ņÂæ žH zˆU,ŅÁ|=Ŋå‹.ä|Đuû$4Č]v~Čß­éW4‹ˆ[~žīô$N‘šŦD6}ŨŨ9%m LáSŠ@ā*¯Æ-„ŌįX.:k_c“áxŸWōWžt<–k%ŠĄˇøΏ4ēđsë]ī <í]\U&Ųķã9íqDAîŖ0†ņ:’õŒ‚(hļtE8ቔ sh.W¤=ĸ?B´Bęj'Æ9FŖˇų•w ũ¤îąEŠĨ'uũ‘_EĪ9éd×Ëë—NĀßq3čûh™: ĀMė![ČŖę Ģ]ēΝŗVZIīöãí{´Ûõ˛Ûė—ŗ-Vķ5ŖŽQĮ8;;{ž÷|īų^iŠ´TZj[´-ږĖ"_ȁYöá˙1 đ/đ‚ģswînŒ6FŖ3veėĘØ…îčŽî!C&†LÄFlį׏yDÆõ„äħo.xäėë÷Ŧ€}į‡˜ĄT5ãHˎėFAj@ @*b Đ>ĒGē}~ĐrvĢn}€kØōë„ų1ŠVyW/öa¯~†Åßđ[(˜Ĩ2’đėjAG {ĩ˙ēB0ū‘XƒPø˛õą¨ūv…BT9įŪZdķ쟝NæZI+ī<‡?åíɎžj$Ø*loĪ>ū˜ŧä@0’‘~EY´€fīđ-<Ú*Į$ā6Î Épŗ ÉxAEi&Uļ !ąäR_Úrna9‚JN”Bsvx:$•“+dtŸ9Ũ:ŠûÎĨq¯{[6đrwŗÄ{”Ãftâ4[„6ŅMŅߊ—ė3yąh;‡éü–œ„ũžÔØö|]nô´ÅBt_ō§ę]į]Œ°UšˆųŽøėąŧĀQ.GMėR†Ų'!CTPŽ! ÷ųt•Ÿ›ņ4Ҏ2åK|펌MĨŅžV 6tõvK>†˛ą7•B^ō šĀąō'RŽÂ¤ĸ(ęōAКųåƌ¯ãUޘ|ņŅ`™|šŲÂ^Xâ6Šˆė^ô}Nŗu+ՏŸúĒf<Æd™K–@omˆ wQÚ[ŖEv^bf¤ūČdußîbwfW> o_Ĩ‹ü(÷ÅÆ]­›‘fũ^æ‡!ąeæšÍ}sšė+s,3+ëÃ~2ËéŗÃ+šļÖ9Ū3Ųī“üŲŦFŲĒW9i9s}õÖ™;wģˇZ°74W顚ŋčˇ˜× ĶĖ Œī5–üŌÜė™&Ī?ĢwR`>8 6'ێú߅ÄzōāF˝ßEqŧđPczÆB{ ĨŲC+”úô—ÔŨˇļîŒã͞™9â5z[…UXå_ŅŋĸEŗŋŲßėoė5ö{euY]VĮ6lËr üPã0Ë&˛‰lbĻaîąîąîąÎËÎËÎËę;ę;ę;ŋŋ)cwÔįfYk^69ŋ+h…>~ Ú)íbZņ×2Ÿš‚ēŅnW~g˜ԈŊōsŖ&)fĪPö霚AHC‘Ų9Ŋ?›<ŅSL‰ô-€sop’„‰'Ōiq ‹ü7)PÄ̎i^žW-ŪĸŎa,ķ^?0ŋæ™PN‘Ē”Ë ejŲ ÷اô4âH žj˛ÁO…AˇÄT6õÍΘbŖØāZV­‡×m,jĮ-dˆ¯¯2 šj‘ĸīĪ`äF~ėY•ÎŨŒ*s`ˆĘ~ ģ{?bãū‚mu)>eâã§btŅÜmRå WÛōÚä—qgŽĮámūøĨy/ãēŪÜ]tĢåÖđöUÖķœ€Đõų;e‚=‰ pîÄûx4 /?’ÛATͧŖ-Ö"Š”,JA`¯ĢĻņīä\Vnhh€ÅT˜Š ë[ÃDŋ°CŲfˆėŖ & M}ßļ 䘚Ũ#Ŗn4_ũ˛ķĢ&š8 ĄīÕAÚqt7›¸ļ!זĀEÉŦŨJōq\ˇæĶNķfę~?Û÷‡%§L™tnNÆą¨ã…Zû×(€š¯[øņJŧo˙ÆūũO€'Ā îPw¨;˛jeģČEŽ:ņ¯¨@&L˜|žĪķyķ‰ųÄ|âÚáÚáÚa,7–ËÃF„AßŌˇôíkĀÂ)G^äI :ūåˇĢljžzöf›Ä\ėLšužŖy2 CĀōä'|Ž4ĩ?ČŅ.-”ĮĀŪĪ_Îgƒˇ~ŠŨo°“¯čŧxħ)­YhZSä~ĮģÔQéiŋ# ßJZ˛‹øŌSÂN'Å9Æī|YÎVžOv>Ž%ŗiÕļ%‘ĒUsaŸ}uĀ#HTĮ{øķl,/žã$ Ģ”Eĸ¸ãhØ+ëœŲPŪJĖķhĪļzK†š4$‹n$ –ˇ[īáŋ=gMĒ]2˙ģšQŨÜå™-ž­?:“3øœšXÖ˛ đĢ-ŋ~ˇčDŽĻ? É`§ąß៸ŧ¸‚%˜Ëī˛ß@`1L‹~Ę,û3´T?ˇĩE6>lĩÃ9ŗƒįöĘ[F8xŋõŽŨ¨†IŽķû"Č^>4MĄ‹ĸoĸˇĖEwŸN9•Í .•Ē;ʍĮŅQyļ[ }GĐĪ|+Ŗ5rđŪÎ’Ō™*Ēi”Õ1ˆŌퟄDxēq’ŖLÚ#ĪZ\MnXë‹ÅÚÅā yĻŧv.ÎæŲ<;9 9 9@̤UŌ*988dÕö›„I˜ô_˰‹ŦšVMĢϝĨ¯Ĩ¯eÚģiīĻŊØ,°Y`3{={={Ŋ×~&ī[]äéÔųŊqđéã7ƒf$ø¨ZZģ›š—YĪ}64ÅôĖâ_„u€§¸MūÚ`G9đų†sšÕ)Õ Úũ‰_\“—r9ĘĨÔŅ|t°˜ŊÛZj¨ Ø-sŪ:ôSŸ5šg$ĀF+ÄæŸ& ¤Aá;˜GœėŧíPČiö(ÔÍ?ä饞ᨨĸív6ųŽēž•į]/~ u1ü7qîxÜÁÚ!šqW­œ˙hü¨UōĨēųƒŊÂt˙˛‘1Ļžr;N>´W m(› ~žg 8o‡Å÷°Ր—ž+*hs„Ī~4ŦRŨī§đ*Ęûœ {ÔK ŗ?ŖŸDk5'R¸ §ōûōšq…˜s=MŦ9žÔ䧲tÆÔ؅˛ĢGOĒîb…:@të[ē”Vāi_2ŧ)ž’iŋˆ‰ķî„`–ņYÚ/T‡&Ķ3Ä2üDÛ­¯<Œ÷ƒîļĀCū -KVŧx„´DŠ5æÃ4Á>,ed\ŅĢĐĩˇUŗ0wŨœCĘāĨQ^cīŪŧ?dĖȘ‘1Ãī‚ßŋ zŊ‰Ū„öĐڃėȎė˙ûT  d ƒc9–c­|V>+Ÿg€g€g@fŲ)û6û6û6ÔC=ŧÆŽĻĪZŸ/hôŋŧëYĐxĩ‘gÖ¤ˇLȏPíįNô×EČ[6qīŋ×BŠ3|r”ģ? ̇=OÎT ØôF˛Đ`ō*ŽKšD mí,<ēU;Ŧĩ 8Ī•ßYhZpčļN!Īū)Æė÷N_ Đ >îj<åŗŌģÎo#ŗÜÕß:âĘ1žÍ‰ģ ´ ;CwF‡°…ûd­EÅ­ˆĨ^üš:Ə/”í‰øäeųĶ‹Đ“­å¨|Ģ›îįŲ ´é&‡ĨŦŠšËzƒRKŅõø šƒœō[Ÿ‰J˜l­5Ág”AŽiqÅžįNļÅ"ã‹ôž4œ\LŅfvÁtīįŠOP”O[!†Ō (”ė"ŋo,œĘeŗƒŧlģ—Ģ ĢŠéĩÍķÜV˜ ]Ņgéb’žĪųYûŽųŽųŽĨK;–vĖ˙ŧ˙y˙ķúV}Ģž•nĐ ēÜȍÜ˙9 ā…^öą}™Ę÷#īGŪ2̝é“õÉúä7ŒĻĪ _+DŊƒûPí]’§n­Ũ1Ų r=)VFęįdSĘ9‡°GėĶGx% <˛#6ŗYÚ'ė’Ĩ<[ü‚HŌåá´ėV–#+˛ˆĪÂaFaÔAų‘öü˛Ų;(Ĩ>ąUâãķíh/õųÎtj#ō)o ģmŧß/ŌáūÁHåģčĪ_@˙x”ū¤!˜ûB§`o5öā@šîT“Íœ$ŋaŦ.¯(ū;ˆØH1‚‰b°‰/Œ–ėē•–MÜÅHˇōĻ bˇ:'h'<ÃjüˆBT…׊ãČgHჲ§2Đ¯>¯ 9Xô)ץ!Ԃ×yĮųøŠ4Œæ8ōMéö΁A%ÔŅz?ûOx;¨Æ/é>=įÕVok‚ė÷^ü+9ĒÕĶÃlą0Ļzd|šQ ‡Š"9iA⇁*uË="—]‘ŋ@ž ĸjČÜĐqd(ĒÛ¨ĩqÛ3Tŋüuîī(YÍo{â{øö8œˇJeܓg0kqÖ9=Û:š\f8­Œéx^Ã2N°œŧKDŠĨč°ÖĪŪyŒˇ\ũaÃ!;‘|R™mtā$”•ë„ÍĖ“XĘŽjīDVņ:ŋ¸u1cxTđ™g(‚†¯‹ŌËúeũ˛ōļōļōˇ‰ˇ‰ēZ]­ŽVžPžPžĀr,Įō˙ŦÁŦ‘Õd5YÍ杔]¤Š­ÎģlPk­#$qSrM ¤Ĩų?kG­mŸ^•ĨîåÜԄÖĢļTPôPë{‡'j¸­3•ÖæÚ_˛×¨āZ 'JPû?0Ŗ ‚čcķ3ö`žŲäbƒÎŌm˛ŲĘĒE ræ'TU+ÄãD­Ƌ+ęÜBNėãpĐg˛H`ł÷åhuŽŊ7°*y?A?Đd-+QŨqÎ~ÉžZ-é<˛-eIã+>*[™Ív1Û^4̎ŸJcíęéë ˜ôS˛?­!Ö*äÜ :šĶ ĒDͧIŗ\öŲŪRŠTĢ_Ŋ˜Ö¤ÆŖĒ]čt@[g å4$j%ũˇ†ŸÄ ´7ĪûŽÆÅŪl‰™Ķ˜ÎR ô´N4Íķŗō2Ŋî‹wq=­AÔ5ę “TÄ)ƒlĄ¨jöķ:P‘Ÿ˜s!Å^Ŗ5û0„@"– [53Î*M•đ´Ëöꔀɮ^÷îŲīŦ”§uå=J{ņĀfY{Ú=ŅĶYŨYŨY=­VZ­´Zæ|sž9_Ė3ÅLJ¤DJD(Baū›q.ą¸œ)gʙžqžqžqJMĨĻRĶvÖvÖö†¤ŒNŋ8>ŸŪMZuw–VũyúÁŽÔ9¨Tž8:ĒÔd°emJmØ´Œô dŗ†šȎ>Lđ¯ÎGF .r{RÍȔNlđžBŠ=€`ęĖoôĩ‡fÍMÉÂa.DŖķĖǟDSũžĖžBFܛš9ļá¯&ŸļÉîâ K¤#™"1”ĸŠ‹~Ɲ} fĢÁđá).ũú™Wr‘†<ōm0>ō/˜p¸h6Õåg˛)”nãÕīfȇ—¤ŠS†“ –¤Ŗ€0z‡ 9͎3wĘXD”ÅIe•ŪÛĐ y-ēŗW^ŌŽƒ]N%Į´–Ĩ}Ëđ —qHāsĘŊnxz‰=ę-{HĀ t3<ëx/Vûbnfģ{Ë÷ÍĸÉKĢËļ[ÄĪ&'~„Â<†gĸ°((( LUĄ 2Qé‡Bâą8›ĩÎēÆ%žO}ųÔLÛ2|kkoå%­—fX͟YŅŊø6UĨ$cĄVõŸ’Ŗ¸Ú!hBūŒģXČg0 ŠLPŧÚžōN;ϊ‡ú œâŅ<„gS†ō#N)ûô ĒmŊbƒōÛĀHD(pNÅ~J{ÍéI‘ļŖJ'9:Ļy$3Ú<Ũwö ĩåt§îԝ"T„ŠP_s_s_sYYV–•3ëÉū¯ÂÎâ,ÎōTžĘS͙æLsĻ÷Š÷Š÷ŠķŽķŽķ.Îā ÎŧĻ÷×öŽK¯ėxˇôîÕö2ĘxkĄ2Ķh{‰~Úgįz1,ŋŖÕĒ 6w恅Ķ<nHķaōį¤ĄĒĨ@ŲLß)€ÜčCyÄ3 Ō:™~€3 Ķoe—ō°×â´PHĢ+Y´ÎųĒFēŌĄP ™įŪā͇dq÷ëŖüĻbXKMŲBžgÎÃnÜĮNúNŒŅnQ­ąŗ4lˆ@)0,¸õXĢx  1z˛‰r˛! …%ī ¸aaZCęąjiôˆEܯŋICArPEĢ;$ĩ4ģHËĘMŗ˛Ō2' p˛5?}~æÆ7¸,nie…$ĖãũŧŽČ•~Ž\¸,=D%–ŧœOrgŲS~!*ú‡å,Ŗo ôËy/°“īɅx×ęåÍvĨæåhsÆĒAkg"âųĖ/Q@L# Pm*  ō˙Ą˙P Õ(–‰YȁG¸&GŪŽģëëŋ|Ț(koôWĪßâDjGŖ('ōĸŧ–3hRž4ŌūCČNėD/vĄ<ĨãgYŪņ",ë;Wgŋ˗ų ËŽrŽ<‰ÍÂ#zãÅ{Į!‰×šË‘ĖŗÍHæeæ $ķmŲ“-v­œ6ķ$–ˇįQB­žîū‹j/ų:fŧŽ˙9„C8dococoã‹ôEú"­ÖVkĢuf÷ÎŦâĘ˙˛ ÔũĐOÉ dl463[pÚįÛįÛį˙ūįîgŅw/ÎÁĐDy÷Û@[n § á0+3ÅŊēŧ;ŗ˙Råcę–+ĻŪ&vE•ÜŨ”GŲɆ|ėéGēš>ũ8 Ú‚üŊĨ–ФĖbƒÛÉü)ŗÜßxyũhRdc€ˆ [_ČMä8[Ą§Tīl\“Įx])ĶÅ(ےĀQX Äé=™āAø§ĒřUœTšJj…œŸŅ ķ”w2ˇŗÂ|ŸĀ‰ĸC ęX‰âĘ`0Ļgų‚^wAl(>áGēVLŊōŠFw0<€@_Lƒ%.‚ Tä6RQę7ÎÚ@چk\ČÚÁ{äõ ;Fā->͏°6É-Ęû*´ĩ÷ .ŠÛ|Ā,ŒÖаLV=uģŖl¨QĀļŌHåSžzŧÉų"āešenąļ;Ņ41=1R$‹G`8áøNį:× A(‰Č%ŧÂĮΟN|&¨­ŲwtŖŒžŊģŽV ŋÎŧĸ–ōX[Ŧiuō,õÁ[!5ūô…¨ËÕÎp:\+גZk'čŽ:ŪöAGÁ2 ­ĩBˇB*æ8¤ōAK FŽņ]ā{æ$Ī3 ‘ylW1%pŽ{ĸëŨØîV1ãnø!/ĒŧîbÛØÕÛÕÛÕ;— zU¯ęĨ’T’Jĸ!ūƀøC/P RÂÅš8÷ōōJû>íû´īĩ2Z­LĐæ ÍA›_S“ČļæĮņŸī›=aąöū‹3‡“ũ;8vzŋ']<#9†Ëđ@šp!o°xRĸY¯\2ņzįš?ķ2ĪŊ„ģĐņîFmĩgļ^…ØKKlÅČfUJYČ>ßîø0î`õŸĒܨp"“­¯člHŨŗÄáb ģ^’•ÔßËuŌ[Å×ŖEJ ŊŸČįŦē‘>¤íJ[ö!˜ËKN”AGé2ēķrΉ”–ü‚”ĢH§ÍžFėC'ƒ˜(˙'ä’ Á¤ņûœÂuŪˇ’Ŋō+YƒlÂßÜÉ>QÜ3 Æęßgc‘“gã0Jra­c`aŒŗ[‘#š–¨ė÷­yŌūux59>Ûڒåš!Åqūž›KæNŽūū_åÉWu" ķ[ŗ„ŲÂÛgãęmÕqüČļ“Ų!~_a˙‹ü¯ŋ7Gq1´FĒi—´÷ę%ëĮÖ §qŠĘō^lų<Į’ŋ|ҁSyšFĸ•W>”īø6ĒuR2@œŠĨG‹āŠOĮŦõî1č)ų,øã=Ü@ åW–#Ljw¨r\Ÿ€ŠJ‚m-gSømL_â+ŽŨķ-ļ-ÎŅ(ėƒÂĢ÷ ė4WÔz-ķ´]Zģ´v™‰¸ū—ũ/û_VUUUÕŦÎKlÁl‘ä9ĀŦoÖ7ë›If’™x3đfāÍ7¤ąÍJ:øøCĢpüÜkCũwęwš0đš/;ąaŅqęÃĶōG%ķžänw÷QZîō ë˛qŋéÚ¯ ĶsĮ&76ŨtĨ~ONõķ Ü ŗH qŸˆĀ_Íb•psI™“ÆûŨĖŠ_Ņ‘]NĘ1b6Åū‰Ģn˜TH_‘û1Ö÷ųŊC%iŋĀ>ÜÅÎŋ NŨÃ>zĨfØsÁĨúy“Y5Ĩd .ãcXđņXL%x °Ië)?ŲôũĒ—œŪ<ŪĢ úČWŒé\€Ģßx¯ą8…2â38(N„"Îz?­?W 豜Ē>´`ōĶQj 'į˛ö˛ éÚvŋBŲ'Q’¸§ÔáEØČĖz1žØ#œt­æ­"„ÆS§"īŋxę˙yÎ\CԅÍúŪ:l:Ÿãr}n\õ~Uƒj:3ė…ČĀEĒF_Y}3&Ęq)yŸ4ÁrĖ’3¤M.1 [ŖĸîžD^׊¨ÅôD$ߐb6pUB sJúI"ĪA(B !Đ?#…ž…(˜˛‰/ ‰˛úTm­yŪ“ŽĘēË7'dIļtÛÄl(ú:˛}ˇ}ˇ}wʗ)_Ļ|™Yg6ŗ‰-%S2%#Á¯ {pGîČål9[Î6ŽGŖĸŋč/úk.ÍĨŊ!JęížĐFË-öģ¨‘ę|MĢaéS9īa‚€Š‘r?ī|vrŽđ+u°J×b>>Ķ…§¸÷žJ… ãÄj˜˜kô‡ŋ•|†“­ŧ Œ{Ú}÷ą(+°å㯸05Ô§Ū~ÅꚐõįWÎĖdĮ̟¯Ķ2šŽųčdä tí–Ŗ, \ĀĀ_W§g F<ųĶ}úUĩxģI%ĖcÉ6ž‚ęc(0‘™ŧā/ĪČ@ĀŌŪW{ĀÁkŒļœŒkÚ0Úk3ŗÔßßd 7P‚æˆŗ(ŠVp ƒ‹vŅVŽ4 Ļ_įYJ)=Õ9ZÜááVü 4Ĩ ŌĀ~)čg\Ä&\âŲŸųøņÕ¨]´%% õ§Sejö@ā˙ÖŦīšAS8éųøį>+ũŲ­§ãÄô"ģ õGE^Ë߇rČŪĮėmąåŗto§ í2ú;kmAãr΍´Ęˆī(íA8Ī=„ ô€e"X‹ÂœæSR¸Ē:¸ģY}™ū4ĻŦģņ“ 'Ø&fCŅϝô õ…ú‚(€ 2Č 5§šSÍŠlSļ)Û2›äū1$"‰Ŧ°ÂŠœ!gȆ×đ^û4û4û4zFĪčŲk bßWˆ/åŏWCK”{VO2ÔÂØ€žĘ˜ĀiÜxuö訃üüĨē×ōĘÔ9sPHŪ‹§°ņ¸ĶV ŨQ˛¨a‡Ëk˘!ŲŽÁÂ6ēĀ7$Îķ÷”î(ÁU$ĩ{Q>“:ęÕNŲâéåSS`áĻc)€ęâ(PĸÚ üĘX]ÜŅÜÅŖŋ)(°ÁƒĨØÅí<šSrc¸ņĖü Š~ŲŠHÆÁ?ÉJûg×mn äxĪ$¸č-턯ū`Ņ|ŅáŗŌ <å‹xNR]‚x}BĀ0š†÷ ģö‹ŽRˆåéęZėá6Ö=|](Ѝë'áåJxGú[KæËĪĢĔ§6Vc̤Č)čÛŲ˙š Qô)žxJyKĘĐW-cķŽĸį ĮŅD‡`1VáŦƒ2ôąū ĒB•“‘‹fđWĘnUĨĮZC{^ŠŖú ÄČFdĐ]´áÆP~×ŋž@pÃĶL3Æ3Åö‹ŌŅ*āęņ,ņBųQYŲų”˜§Ÿō;™õãP ĨĐĖÜbãžqΏgĢcĢcĢŖ4Sš)ÍūŠöc?öķ~ÂOĖ-æs‹™ÛĖmæÔõĀ7œĄŪI­ŸTAåÔVßÖÚ"6m 2ĸv`QMŊĐ"u€˜_5oųúĢögWŖØ‹÷ĶVģ'ė^éy7ąs•{#ûĀÆp˛‰Uâģe„v ‰ŖđņvSßÛ•¤ōęĮg’nĪ[3°Ģ@C-jGí ũ8'ˆĸJŠT|ŸĨ?ņö‘!{H`"åf ^ũŲ29¨&ķPŖ„{7wՈŋĮŧÂjCvĩŸŽ°G|¨&˙S֛Ɇ\Pą–'@1ë§´bŸĩÅŊ…lĘęv+‡íCÁxų›Ŧ´L܏ į'¨Ka˜€`˛–ę]ƒa<͸ˆb"ã6Å>^‹Ũ|Qt“čņ@YōãüNV““Ņ3=*ã%Ŋ‹s(ÆqČ÷_Ö¤(Pd#Ų•SŌdzVpsL‡…$ôÃĨÚBŊaq/¤â&ī3&b6˯đž9ŲĄŌ† •ļŅ ¤bāâRŋąô2Gǁšŋ9[Ŗ\_hߍΔ|X—B‡xīĮ/{¸ÖœYîÃ_?ÄÛÛī1ī1ī1šWî•{9ŒÃ8ŒŌ)Ōá˙°ŲënĐ/ņ%ž”íd;ŲÎÜcî1÷ˆŽĸĢčšŲO÷ jsŌËw¯=P‡‘ąã”í0cĐx‘š0ŨO!p _ hYĸSĮ—T‚ŋqg‹;ÁĻ?xJ!á—*.aõķœgÃjíũ đ˜bŊ1T:ĘëÆŦƒ”‘ũTšÅ”š}kšOe;æēš|Įå 5hŠ"\™K›ãėzĐä2GxÚs€YØŗ väAŊ_Aŋš’@ rķ ŽĀy]ÆĢ¸•ü“įË’ƒ­÷@žÆŽÖXĘĐāüUŧ~ĩä@gŌŦRذZ¸ŽĀÂ#>Ķ tí •wYe€,H°ëĩĢēR¨*’¸1ŌA`¯õĖ;9Û7@$ę°ā Á™Ëã‡ą/Īw W!ņŇR4Ą=00„Á rėÚ”=ÄŪĄ4ŅojdG1ĨĐūcä{÷o)ÃķŨëã5Øi“B} ô%ÎIėĨ ^ā.?Åt–ŧԘŸū6¯á™VéȆ2,ŋ^Møā Ļ4…ŊĪS;1ܛ’`Cę ‚Š°×œíũˆķSŸ ęũē–ž'5Ą°×,͆™˜ō3,ü$ŋ!ĩfŸüĖĒ2+ēkƒđ'ūIŸ6Ā0aĀ?8‘+š ķSĪR>`CwLãĄx.ƒ 0ĨËg¨Ksņßqcë8?’{ĖFnTĸ—Ę;J5ô đųWâ—č‚6ĀYû B‘$ }p# Ą'ēÁŽTŠĶŦEæÜâp™ēüĐW›BQÃĒģõĀXÜŧ‚Ág t÷‡Î§Ŧ „gY¤ō?vÕ uI‘ŗ|SŲc=ķ~I]Tw9Ÿ6CÍgŪ5Ēž}Š&×3ô.|íOĐ :ĄS‹ŠÅŒÅÆbc1Í_ķיMÔߤ=Ā<`ɒĨÜ!wČfˆb†l ذå *kßô*1y~Ō˛Ûu‚Á#1=ø D=šɅø!„ųš;Š4}´V ,§ąŌņz*ĄOzöčË\āJÄšām…?ƒģ÷âÛä¤o…0Ã܏ y:{؋Ü4„™\LĢôÃ(×wŠĨī÷3‚Ō8ÆOGōõ}-BŧT_Ē/uīsīsī“#ä9‚ílgû?7Ļų‡Č,\~‰/ņ%Ģ€UĀ*Āš3wÖîi÷´7´ ķŨŠßzį„hiŒŠĸœ•Đ‹rā[Ø é­č=cCŽ6JBŗÆ{ktžNt„–(ŽTN´˛y*qØĢqį6QtvĢōM|ˆüиÚČl8ƒŲ\žĖ˙^‹–|ÎlŸîä}ĪÍ §˜+Æ´fBkEI}¯ß~XtH´h MÉz č­‚I;ékžhÂՖûpSŗ59č%t”VBās”{xâ~naÜÍčˆĒĻĨĄ_ŠĄlXĶōĐLōŦčí˙õ)÷H%]ž°Ŧ|iI`ځaŖ5ŋūæ"€‰ųČZæ€Bu0 ŗ´3ķJj¨`–ɸZķ Ņ=Š!7ŽrS_­/˛Q7ŽGÅhŖŦFÍh§5ߜå*Ÿ\ Lûčņ‘6Ā^\L,Ô ČY,ä\ˆFųø6G˙o—*TX<… 1÷—š |žc{ŽņTžŨėä…Ü@^°B<ņ){hŸö€ŅŦ`?4ĄŋųQZ Š“"‚P€ļŌDšĨ]됃oYuĀtŸĸ~Ũąķtē5Ö[‹ 9Î(ЊΤ. Į7P•Ģĸ0Z‹ę掤yžÛ‰ũîŊĄFŦkÁ2‡Ė!sČr…\Áåš<—ĪjÉõĖÆlĖÎü‘5ߚoÍ'ō'õ•úJ}ƒáč ŽŸyģƒ‚F'úŽ>äđÁÄeaP„Éü5{Í*îPyŖ•  ,ꂰƒS6ëšgžtĮŸŧšWņ->eg )FŪ˛}‡ NÅæžŋ‚ëÝæ]ŗŠĶāk 4€É÷‰ÔÅöÍėV'kÄá €P}  Ŗ8ĘķŪoeį•ž•é9 ŗ8žœÔŗx´yĮs—÷ē&ÄoCvr+"ôÚ @vØ!qķØkLqO€*aMPoƒ°w˛iÚRö°…éPˆüf†P0€2T—=ÖdßURŦœŪúŠĸ*€"¨aåķ6CEĢŽ÷/3ˇÁ‰`ø‘F'Ņc=?$œ…!ƒŒ^HEn6EĨ2âŒiÆĮžĨė'Úë*æVŧJs†N úŲĒUîtéJŧ‹1^ü­ĒxGņ!č<›įcąļJëHŨĢfT:#ö:—Ú#7(aîķîˇúē—ÆõĀ&$ZĮŠ4 Zl~ŽĶ2Ŗlš“rãSœCŧÂ/“_ųÁß´ģža7Īĸ…×Yqė1+ē¯@"š§dQT&uyaā2M§nüŽÕF{_ļ¸}ˇŪPX? :Ŗ1Ú ļ‚­`ÖYg×q×_cÆÃxĮßņw™õû•ļJ[Ĩ­° ›x­ÛĪąVûž˜ąIįī/ÕJ+_#7ÁmžÜH`"ã×k•‚Åߙ_’jæq bEáVW_˛ŧe&y^XjāŠĸy•yuĒMV”šŨ;FYSrāÉĘÆ †ŊÍ­D˛>‘ŖŒé×(Pd3š“F“äTŌÄ5ĩ! orŦŖÍ`ÁüĩĸÄo¯vĸh5ôģx˛ķö gœÅsĖwŊĄ°Ą2õ`ĀĀ€÷7×\( KZæ|RŽŽ/Ąā2bÉfusVD†@(ĩøÍüķ• €ą™ËĻÛũi'—’Æņr%¤éqOAâ;Č&ɨ„pÁ—XC—É_ 2 'uá5Ū ä…¨Ą~íØ¤<{Ģx.ŖöĩUņH}ëe*îĘæ,MŅîĒÔ}5'Wģf}ž¯|Ū…TTVÍ@˙)g(  ĪđŠop4W)÷ĸt#mzŲúĨ÷ŠâܜŸÃ…ŅÆđ¤ĮÅŽÃ1ߔ”ąh…PSŅ“Ķbˇ#杔#=ËŠĒ4å¸ŧ´Ŗ¸œi–'ÍĒė­IēÕÖ÷ąxFš™ÛmƒÅŗÍŅYTôĪ;éét÷y–VTų A惤m†ō0ĶÅĩ6sų6F‹?…üßåbŧN4&fTĻhķcī1öxV&Ÿ€Žat@ŒūKRHĻųlŧ§ÉOyŦ=ÉJ)Į ąg¨‚§™RSš,6dsŲŦZžF (PØ+O˜ûŠįôrŠéÁy."PyĒ&lĸˇLōž-øŠ•J­C§”ŽÅĩšāp–ãžÖÖ1ĸJĒčĻÔTÛ8Jgßž+}oëŽÍ‚Līē™›÷Ģ%_mˆ•ü@œāE)ĪÂųü…ĮÁ đ^ņ6š?*XøļZ˛åęĻ÷ÕpgNG{äÃ!DYs­îKÆžäw_'iŧĩuIˆÉtOÎ÷åÁãŒ]Ņū¸†ŠXŒĪPƒžŖ 4[!wËļp gI1˸{“;Q’MËã] ÕéœüMĐĐD*áaˇrK|Ĩü ûĨŋ|‘ËJöĻxTø!üŋǍŠĒęqõ¸zÜŧcŪ1īđj^ÍĢą‹ąĶ1͉¯đž"ûČ>˛7ÖëM^ŧ yAĀŲ€ŗgũĮúõû:b .éŪŖ”š‡ž´^î=ZiÚižĮņ¸Šõ`ˆß9ā$,˛!* Zí㸃’Ļæ1='”r#íĶë™QIų%°bî*oØū;x†8cé…I3›“'ĒÆîÍ4_\ŸÁ4[™3Ų#ņš˛Ē+˙ĩÛÎ`>C9uĄÍ'¨…Í„P4 áû›…d˜JOwUÜ_U}ōxũ“īé{™MÖÅCŅ\ÔâTtB[0lŋv•HC:ōDž†PžÅŋđÛZ-Vö*W˛l>õnËeMnŲ嘕=¯hōņpTåîąEî˛öĨ>˛X4¤‡b}IOÄ÷"ÖUáyÅ7˙B ņ3ty“I+rO°_~ė9'r!?ÕĨ2JlTŪˇ]A>s‰ģ{+Äobį ƒÂP‹ÜX]˜ōĄæ|ũ­ō“WÚ˛%šųģ|^¸?pāū đYāŗĀgZ'­“Ö‰úPęŖâ2.ã2Wįę\]ž”/åKÉ#y¤Ú@m 6xC$< ãfÂôWhˆøFė| …ŖÍ‰`4Įˆß™\7îbæ*ģĩĒâ¸ãĶ.ī;›ŠōŪ VeëœgwĒWA`EŧŠ/Xw+Ņ-ĪxEÆGĘå˛ßa@4đŊņsccĢ/wįOüE./  RqâCÃËgŦ­c¤Ú†Däf¯Øoë đ ūéo¤´%rgŨ éĒ÷a7ΰCáģ $HĮ+*M‹¨”°ėsBÚC°äמ“Ęŗ?áũ8ÉvžXä7\ė+ĐŖå"ôÖ æ­ŲŨÃz¤}NEÖJ§!4™ķh-ĩÕō¨§C\ÉģS”/ôŽh{ķ "zi{ÕHŅĮyŅÔÂéîÄ°ŌĄyŒG^Û.ú\Ž|ĨĨh7/ž6ņUöCm>ÃāAr`Ø"~GÛ=Ü;į€ČHŠú¨r ũUeŽrZOđ›čtR}YÅØÆ—„ OzbŌŗ]æÔäg•HąO)JÁTSmGũĨĶ7‰JĨ5>KÅŏú8Œb+šrgs šæģ…ąVIBbpŽBõÍ~Õé¸ūŠßT9ם=) nĢq˙M¯4ƒĄ"b˛Ģ§iĢąÍ2éwâkې E^b=՞jOŲKö’Ŋd° –ÁYųëŨŅŨUÁá܀Č ō‚ŧĀũ¸÷SÎ)į”soHŠí•ņ$v—ōœÖ)njĸ Ņ€ī7Al ’ŠõÄzĒÛũFQ.áSĻ`švŲoĢōĩįzÜ)O‰ž=.- ôtĄë”ĻÖ°•Éē÷bÃõbčųAh•øüN:GŦjėÁ#ã}Hõœ2‰tu†r“=ÆKĢ8%ū´fyfoŦōHÄŖ`ņ-Ģ„y8å(ÚŲž…s´įwYi¯ĮyUÛĄž‚œa%Í}ŖI‡C}—Ŋ¸*fPđøßǛØLĒšĶž×Ô¯ôq`X`Ē æĩ¯åD2ŽĶT[ûŽ„YČ|Ûä\V}_>é˙0ĮöĶ"ÔôķÜõ|į3ĘČ4‡'|)ô6WlMDĒîRg˜53Ū>íZ nÍnŽî8\+rŊzZiŠ<ôŊ_öˇIÍį4č}ģōĸr{Lëšōĸ,ŽÄžkŒ23 Ōģ4ŸbDĐļ ębq¤7"]‘ŗmÎúĀū;•ÍJi%ĪÅwJ./aŒđŦKÜđ´Ģ¯YbŪûŨUyŦ (†Ãô-ŽāŦŦAãSŸ<Ų,Ēy§%…S'zL‰ĩœę[ŧkqžß“ pávŅáVnR íCįqúH4T Ō3]ņŽž•Éßđ ūM āøM„ØBŌÉĸaėQZãsŲÛ뛑Û@Ô}ÍŪĨėRveæ pÎÃyxâAôŠ^Ņ+ģąģyā2JFÉ(BChHfRŲްŧî¯TQVd3MúS´é0h(Ļd%¯ŋvöķuۃ§ę:GJT{Øŗ#ßauW&ĒeôĸާūĩäCīš„Ōé ŖNœŲ¤åŌįÛVˆVō>ˇ–?›~4‚O<Ųŋ¯Ļ>@ÖËȀ)*ûۃ‰üČ  ÂŌ?V“ĄX—äģ0ew^PŽß𠀐Ī1ɐėĨcōéûŦąé[HU^8k˛W™íWĀ|ũr ( ĀlÂ0ĻÁ¨Pd"ŲPÍׄ¯ė'Āđ!?úMkk^āŊO3”Hj§÷ËOŲi] ĀŽžúį8ēĻđ‹V!­u?Ξú"*]žķėŖãßa“ZÕҜFˆžŠSũ^n6 úŪJÚ|-ˆëkĨ+ úĀÁb›ÚRŨ ŋ-+ßēî=¯|9Ō đšégņaŋäÛ'ŽÛÖû¤?Į<ĄÍĖV&ėŲ(›6HļDy™MNäcdÃˤŧ"HIīP]eÕĮITĨM¸Ž#(͟py+ɧ¸ßņėIOKøÔĖ—āžŸK!LcVb7íĄoč*ŨĄ˛ŽuOĮ‰Č´vÊ1åATĨOĐí­]F¨rĨoōŌ"(¤ ŪĄz ŋÔV™ĄÇę˜L)ę0;ķ3uŊÜÆ;î=đŖ2ôîkrĀāĄø–gНÄ*ķKËr āįßū4‘&Ō2ģ Čö˛ŊlĪmš-ˇÅÜÁ’gåYyÖliļ4[Ļ˙’ūKú/žžžÁÁÁt‰.ŅĨŦ™~§¨öDŠģĩú¯ĸŽũäwM?äœOŗøŽÄĐ~uĒˆÃjFĨE]‘ÍĄ…´Ŗ]Jn]enb œāO9Q]ę†ōNhúļ/äa]ųyé-â*͊tú‚&Ũšū+õkk]ē‡‹ËÆaŽÅ¯F@”Šrø™ėö6Ü`H^ë'•‚âdŖÎV6”…ž_ Q„ãđŪXOyŲE;ĮXļ’9簁ŊĘ!i¸üĪŽ C¸ũŲŦŖ™-B%Ÿ=QęįÎũ¸†2VkŠLv3–iŗeāƒđ9•S_-ÚßéōC){Qæ\ßķcĢ!Rí“Ãr8Ô\+ū¨—ˇÖg(EDCĨ+†Ņ ‚<˜žâZ¤ÃHô… Ē(Är6 ‘ī™5=mŧëĶJÄ~nNübģ0*2MYã­—Ęg=Áq_Đx*#öQ”ÛsNŧ•Øō’S)*Ģ[ąÔ?Ú*b&įA4ŲpeLĀaŪjú ĀˏéŠ:ÚļIØíBėTRÅ=6‘„‡ä@)ŧÃo[/}^ëv'mãŨ|Sƒ†pĪ:_ũ`@ōPú‰.fņUs}`\ĘQˇŪÚlUjīü,īc˙3ē9nZÜ´¸iŽ|Ž|Ž|ÎÎÎJĨŌ@`+ļbkf؋ûpîCĨ¨•ĸ8ŠŖ¸×lČV˛™yžûōĨ¯ķčm+ ƒo>$,°™2"!`TNs9—`ŊōB_ÆnÜÄ|lb-šĐäjåé.įP_dxZɑ‹4Oq{Œ’ÛōĶ‚ü‹f_HÛhUÉhī Ā‚ €ôąŌahšŌ‘tåļ(āĸM4tÃIŪįPÅMãH”áë|`DĐG듎=P͟Ō{€  ™Š›ƒ@°ā ;,Ŗ[°,€…bŋÎ`" &üų:˜†ú~‚Š0.ĀD`Ö nÜÆNZ¯ŅēR9-Ėy 6ä€Dų5°M ­ˆéųԚ.@ļä‚>á6¯rŸ+‰4Üā5˜‡Ÿi>–[ĨŊ5ŨJķîB1ä4ˆv4_ 2ÆÕÁœ¸ä Û¸ŋˇuōzäķE”X­nPÚŽjBšĄ–Æ_ gCJņ#Sē=~b}jšį᝖peKüæIoFzĄØežvŽ“Iy|]ŗ÷zoĻũđǏĢXÂŪGëĶŌ^,š>4íęĶ—į{5âÖŪmЌ0ˤíPâĢ*DB‰K~Š)#Ü׊›ĩņvYÛŅŌĘë…y‘ dGĒB×ÄĪĸ¯–â?Žę’ŋfņx˜šcėÁMėĀ%J_IĨ´TĮ<Đ¯ųâ2kW3ŠŽÚų担ÔÅĘĪ?û*¤mæV˛­uã5›ų]Ŗk™ôĖķxĪã1<†Įā6nãļĀĀîÁ=¸‡\$ÉET–ĘRYĖÂ,ĖzM–T‘ņĻÅųĖYŽjDi¨ˆüôĄo+*É=,䇹‡fˆNęXĒĄ÷vv& Ŗ!Xø@ū@ĨœW"Vĸ˜­Ü΍ZüšĢiôHmîhÃdY3˜g{j&]¤÷GÂmœ Åģ}ˆä‘á˙”hnˆE2ԑ@č‰j1Zé2€‘ ǘmeŊ0?–U”ī?fP`ƒÄ#ū–Y-e>Š<ÆW@ŽŦAyHä1€o,$€K¯U úĮÕB16𖙃TzߨdĩŠv!† Ķ”_tĶû5§ÖÔWŦg/zđ”C+¨)FåŲx v&Ũŧ{ąņuŽ =ôęū&Ļ=âCØā€›ĸPS0Ęü$ĩrKģkE]tŋˆâmŧŅ0žēŝų„ŸĨo|š ‚:c ĸųâˆúÔorøy›y=OĩEVžæŧŠ5jŪ“/k]*œQ$*éÔÄôSO wĻįx2üD—ķ™īô>Ÿ˙ĢØKSĄ§kOŠQŧÚ{FŊŦ^_ˆ "PĖÃ%ŸLžŠéV¨į'ņ˙ä{"&$dŋTIiÁÍÜ:}}TŊŠžĖ‰ÖÖãԓ´ŪšîMAuTF¸ņ—ČĻ~ėø Ôoõ‹ėÆŨŦŠßVÖ~2,¸Č†ĸhHĩõA~]é[ņž:*“ē˛(M ŧ܃BÔס‹šS:jr^ķ+W ĒÉWæëîŨ‰˜ˆ‰ôˆŅ#>ĀøžÆ×ø÷p÷TŪĮûxú úpKnÉ-ŗÎūVh…×z÷ōĮŧHŽäÖF#fŅڇI}ę+ âžö^$č‚~Ō¯ U ŖJköávˆEé4Jũ #úUŨ„­ ›o ?ĨÕĻöú ˙“Ôßī.†÷ģņĮđ™supxņ•Y6XĸŸÔlģ•Ŋj´rˆlj„ōģëÖy¨ÃWŲ'ĸŒë°0į¸˙ VNßąˇ›ŋÃĖžZB;“í>€ÂhĀôMa/ĀŸ4iŗ ā, ôų‘ŽâjNÆzņ˜’Tģũ>ĸUÍžĀ‘ø‚sQ€ZĐą ãķšĸp4娓9ŧ*nɕTRP›ÎãĨúČŪžōĢ/ŧŠė2nģĢQqt’“p’Z{ņûæŧƒj^4ĨŌĸ–^Ky,n‰ũt’÷f 6×'”ŊxÃ*åKŗB } úÛj—ēà ĶBĸ×+…ÕYbĢēĢ*ü’ŋâã8…Ÿą÷q“ŖpD`8Q=*… J h—5‚ęŅ@ä§Uĸ:ÉžŽHsÍ}ū”ŪKūėzšV(gą91Uä‚įFī´ęˆbˆææ|÷UZj]÷ŧG“i´ŊwāÉÚiúXœTŪĨˇuËo7…â}rŗ˛Ō{_ŗģ؇h€tPYD×ôc~.DzzĨ NÆކ¤ÁžRD¨LŖ›´LĖâEÖ$#ŒOsGk m¨ĄMĻÉ4™ėd'Vc5Vg6v!ëŧuŪ:ī õ…úBSRRŽp„˙ž¸ĸ$ŸHqcü8˛—?_—Į 6’Į)”†Ęn.å(I‰ęˇ~ߋ4ĮŦēTÛ¨ [Č@,,.,ŠQč÷ÅĪĸUØâ’•ņÅŖ*Û°OvąęŽĒ&g{“Skp3ŋy‘D™Ķ˰•twßŌh‚Šf–ąøĮŖ8r’MV’īnˇoT<÷l`—Øč3ĀHÍē÷Īʏp*ÍŲIÕũsŠxdûŊHpŋ„ pfîiĐ_Ä bĄās}ĩĄuNˇČCh™ØŦÁß*ŗ1*īWÃ‘`ÄeLæšĪ›ÆF4-ȁhÍŨŦãsįĄÄiØÉå\'!1Õ;ŽOa?˛ §č/”ņŽ~đ*įloárĐTŪ‚ŲÜWe+Õņyîüuî‡-ã¸ŅĢڗļá3ûiŋÉh§fw”‡>QZÃF}č $"P,¸@¸5°ķP^Uo!Õ 2D ņiFcJwų?i.>÷•/ĢĖEĒYU|ŸīŊ–Ÿ!ÉúŌũ)Ÿ^t¨"ÚĘžĩ´×úÂ{ ÃQ SIĮ Ĩ4~Ûųˆ–Øî=õ\ū?ąq¸ķįiF¤ 9ųZđjétM:ĘfĮŒę¤ĶI×>ާ!ŧ1ænĨVúũFÆá“Mœ~J¸õ˙×Ē&p'°X+֊ĩūėĪūŦ ŌiƒÔL[:tč8ŠŖ8š),2ŊC¯}ۃŦʡ°Õ:mVĨ6r˛ŧ‰ŗæ3ë {đ-_ĄŌ´Ã.õ/Ùn"•ö˛…s˜ÆyžEAjĒŖĻ…¯*—€Ÿ_.<ķ)ģ­wŒĮÁ¨€VŌ††æ“´;(­rž¤<"—ã3~&Ÿš@Čņk5É7ģ`ö‰EJ(…č+e}¤ËIÔŒ÷ôĖ,õ鯂T&*ņr¨Vzz9?ą~‰ķ˛÷˙f¸ņôĨlļÁšh‰¸ĻŊĪ&^a}ąĄz<gāĀ—ÃOÅ<ā< T§N™¤ĀnÜÁR”§Úz_mŠ×”.#_úį0ÕsĐĐ9č n §ŦjŪ8nÂ-QūĘHûĪԋ&Ēߊ‡ÔŪZ­XAyOđ}÷§fû¸ÎĮŽË§Ę]û}žĒĨ&q-{€ Šjg‡ŲÄ]­-ōā0WE yÂŒæY×:įģŸú@ŒļšeĖÅIUs‹›-w   4„/ā]Vcڟ}Št(ØģíD؈ }€âŪ'|FD7˜8?NÁyësZ­Ôŗ_ĸ\ÚcįP0rfar˙"ˆÉ’‘LūxB‡EIÍpFÉtë|ęö"@Š˜Tj‹ĸĘđAoįBrÆ>mEPō!ōĄZ ?æĮüXEĸ…„ wqwqWqõ īÄ+`Âô60o"ÁÛÚwRŦˇ&@Á,ž‡zēZ——Éķ–ƒę*@Ų Bōž„VaëKEŒ÷|Š/¤%=[Điq@0˛CĨˇĐģŦ=ž#<ÉõyZ,­ĸú=ؔ‰Á åéô ((Ą˛S6äa>O† c3š€5él ˆ-ęËײŌūjDā|æÛž} o÷4; ęŽŋՃ„ÜÅ!°øRÍÍķô!V"æÆVj•íÃŌĩŅĖ/*r-Æ<.ŧ37{dKĶ BK÷k:Ga~ĘĪåMÎáiv’ļĢ@bŒö#L<ācȋ:Č@¤Ņ9}8ŋ4ĖĒØ¨\ļLhŧ}'LĘ%&HG4ŒĨņ8ėė-Kā÷˛F@Ĩ|â7ūFđ‘`Q.8ųßũŒņhbä˛ZBhy—„Ņ/H€Kā‡6ļw‘ éôĻæ­§ų4ŸĻŠT‘*ĸ6jŖ6áŠpĀ".ÅĨ¸ÔŪâ*/Ēã#ņō[ā C LnNšDSe6§ií`Œš-c§öˇå#]|­ŒA€^%€ņuHūĸ&Ļ?›tđÆĶ|Ū oŠ!  ''â÷ãŌŽģqŖ°Ūw'ŖĘZŌĄ(§ĩĩ߅e>IīD?ü‰â‘#Čfy3ØgŨIkC6ęoŋEéđ,”B+æŸBÂQ–lŧ×* ļĘēCÚÆß“NuÄöqC1€ß$Uū'’_`Ųô$į8eŠw&l…‚kãģđúcÔS÷ūĒoúe4„ âb&€ƒ¯•mbØņbOtJ[DxF$ĨRŠúŠís6íŨÕY°Đ„>‚„ĐA}‹:¸‹žÜ+­î~ŧHY¯ĮeåŠZ–Ũ)go÷Ļ:ōއáVKچŌ4ēŽ,…Ä tŖ3R΀   +4DB@GN $lčmdw×@l÷DŖ,‡('PÄÜ&ĮP_kŸ/7žį}Ļãž7=ū€2J-îҊē=¯}ĸ%¯õ.N^ °‚üĩūö5œ,ž*š¸ˆ‘œžĪíí‚]$ОBX‹ä?ú˛dC |Â?XĨ}…yŗëĶØ-'˜ŖÉŽŽÔOp4=TÃøGúŨ&Ē)¨o8îžÂWø QĄfbœā€CE!BĄŦ†aƓy2OÆS<ÅSäEŪ_KŊNE7¨ŊP‡/á>ÎQJƒĒ۝ßB#T•ĮʓæLîiæÉ¨ƒ4-6`}’훲ĮpĶ5!&ī狯Ĩ@āÕÎäkØŠ+ÍDO”%ėô„¤|Œx”Esŧ´{ûŖ‹2Ė> †đd•é͍BPTv˜Ą˜R–ÂÂsŽÅ†•ĶģŸTå˜m,{Ä]x’…¤ßĨÃk $cTŗ‡{ģx…•  üˆ ‡°›åAVwŊ ą°oĐY ĀuŪCēĒØk°W­Ļ˙DĨÔ.vjįėSĢÆÆMšTŸīģŽŋrtFLPá§~ÖĮÖQŨxŊlé‹āÕb›ķ~[žD y×;ŸTsšm.{TiĢāļf–Ûe#ËēŠ„Â8ˆ|Vlw> ßJŪÂqĐ6mūãJˆĄéânËĘÆ;đÚō>ĸ ‡—ÆPc(¨G  ÁĮ¯ ` Ē '#Ԛč9‡h*Ûgč 9QÉowâ VStĒ,~äæ…bÂųnZžgSh~DÁ*SŲõ$~o09•CÚ:¨zag4TēNÁŦXzFQQķŦ÷)j’}4€›Y7č7;jCgāŪæn[âŽđRķk%é"cŖ+ >{ÛĀ ˆŌhƒķâĄRųč qú7Ęņ]Üe?öc? Ā ČėpGŨ((ÆhŒÆ´›vĶn9EN‘S2;@žö>ÛD˛:ŒžS¯ÛŖŲĮÅy^)ôhxÕĨļK`Äã)čįĮFq×gx_ r^CíĀŲųęcSÜčĢ×AȃĻi(„fŧĪ:dŦ•KŌgÆÚø˙hĩc+ōž›cŊ/Q–;ĐnHŪŽžƝޞ”Ô@HÅĀę—ö){eQ¯h Lî+ĮĀ2ģ:AAœŋ™!+€Pä}Ŗģ­e ė`¤Ã`u6Ē’JyŦŲ’Qøw3Ā+<„ ĘJúK§šH‡ixŽ6õšņ0Īæ„â˛kĖĪį Ú.VüSœá?f 8`â!Žņ9—/á0÷6RÜmáÄ;´&—‘ßÁōųšAáxY @ōŊ6ƒ„† k¸)] ˜–;"==ęŠÜÕמ:đ3ãzË'’/S°OMųn™ĶĻĐ~#5u(~6NĻæÂ:ãRZI:kdKŨG/SiĨČîŠ/¨Ŧåv?Ĩė\Ņ\N›čŠĩ˜ŪĄÆ ((„R0aÂÆ3œäK/.ŸūŽÃį—)Eõ´!ū1li–Ŋ{ġj(x†{|“;É9rŠ1>=?īãûriF‰ß놉˙¯ŊǨļžĢv8Ąs¤ibĶäœsT‚d$( I "ĸ€ĸ‚˜@3‚ˆ’PAQ‚äœsÎ9t>Ũ'îPëûŅ}y čõŪwīûŪ{ã-ĮØÃáđTīĒ]ĢÂZkÎ)ÉEíh1 {ŋåKY.D 8€pÄÃ4k÷â†uŲŧÉ!†ũôĨ2Ú،~ەZŋ'xæ=ŧ‡÷Đ×ô5}]P ”„$$‰|â8Oãi|>› ÷įūܟÛpns?ÎR”V/Đw“ +Ęā”îu÷QH| WĄ"ĨøWųÉSb––û{ōŪŊց/‡^ËZ á,(˜#„#‰¯ægw2}ŋ…NôBxŽ7ō5ųšĨäögĐdWs4€T4ž ?ĪÆŌeP}fĢnW0ōøĐ]î"Ԍ-{Ŋų!éÖúāû ¸Pâ7`v'b b,ß$ÅēėÉĩ¤Ž’LÄCĸE3ĮBEĀ€~ˇ ArhˇÃ#R”T:Õ9Õ¤u‘åJ>ÄC¯}Ŋ!Ўš_xĢbüÜÜG.”BģģŊ°ÉEmđ>6ŗüķa0ķ,\äčĘĶÃlŲ‡ĖĪIˇC ¸ ‚t|÷MÄ#–~  šæĒûwČU.Ŧ‚‚Gčk”GHƒą@ö¤%V˙Lē„ëŧŒLtDŠF&¨ÂatŨđũj—ŽĨ1˛ēyMđMG8RŠ.0qMņŧy‘t ĘrQNÚ"žæAšSŽžŸC/äūBi…Ū¯^^ĩĸcaH(@úēPš:rGģŠžlō—ŨWAː¤ Uų¸üĀŽ%ų>L ãõrƒĩ*ĘP+1(đU™€SfĒŋˇœ/kČVDÚ6÷QÚ"2Ôą÷ĸįĮųņ| /…Q…Ņ@ōYŖUtDGtĖრ<ĘŖ<é•^é͇GR}ĒOõ ZZFŋŠķâįCQ‹äûjļŖ0žT¯;jAáÕ˛€D”‡8—FģÂâz‘šĨø|øĘĐuŖČ‰ņ4M$ĸ,9éašĀo˜?zķz˙'>t†‹_‡ (oædxĨĪj@E¨—ČeŋQÎ_ Šë+Ŋ lΒĨ@HDš‚cŽ7°“×›ņž'øuÅvžHNQWĮA\Â8¨3Meč’ĶeC2īSdâI: ōîh¨hBC°ŪĒa^–Ķiw¨šxÍą1âMŦ âåû ‡s8‡âŸø'*OåŠmÖķ7įÂÂĄ•ßúfĨ?ÄĨdģ%DQ)žÂ@†‚"Ôáô+MF'Ë. $á6í,ķ89Í„ā ˜`ļd ÔŒēĀ˙§yb‚Í-dØúÕߛtĩžë"[öˆā°Ė*ûw“\&ßb‹5{’[$iĶ ô-î Ē+ *üuŖkĶēVƒ@_Øü,€ČāĮ…?͝ĢlŌwö ŖŸŦkäR‹…[œŠļ×Ģŗ;ķęŠqbyęģ×Ō'Úč°ÚŧÕúÕ ͇wŖonÄķ~nÁ=äysŧwčaŋežĀgüã3‡˜ˆŗb$Íŋ;E9y„įĐģ˛=uײÔ=á7{ÜŗūÍfŲŗėY…QEÍŠ95§nԍēá'ü„ŸT´A´ÁŒĀ1SĖ3iĸQæsŠ9å QŸ;:î¸üŲV\.Đ;¯@gšųģ`)`âįņ"cd^i¨ĢŧÎžCEä_Uĸ`§ÛÅ&|”\rƒąN-˙ ķ”h,o§>ÔĒÖ 1 KĘõëEHDĄ)žŊ:ŪR—a‹,ų Htü‹™o˜ØÍ/BČĨ˛´āÂŧ<1æí„H˙đˎ‘éZ’tŪ´Ú‡!’•9÷í.D@ĸĩĘ{^ļS‰fŧP˙Ėu@‚Ë ƒ~ŗ˜ågAR1†KķDĩ˛ŧŦÆF4T?°’Ž?€ÃjššÜ\.Ɗ˛ĸŦ˜/æ‹ųø_âËüŋІhˆ†ųu˙bš˜&Ļ)†b(†1Ęe< ķnjp‹ŪBÍÔ^aÛ ĄÛī¨Ī‹Ŋæ10~`õ;ŧĀ•ö+Ų/œ?B$nx¯NûxÛ\/´†tŊ_Ü õÃˆĪžĘߺКĐq.ŠÕ–ģÂx‹ØGÃPJ‡Ģ,ĸ’`szē’+ȄĐn%N‚DeÚ ĀâĖŋķŠ œÄ ˛´d÷6:ĸo K“;5ŗkō;Ō´J‰’hō÷ˆJ‘F5í[ ŦņŋKŅv­`]ō$ ĒĪķo>˛{7¯ô­Mû*iâŸjÜ[Aǁ*ØÆ{”¯BHhE\ëāЉ˙6ęŌĄ?EĨũ‡'§Hĩ>ÎË[/åMĻrŽ_ŠUBs}yÜ@Ė0ƒĄ#xA=ŊĀĮôų“v—ąÆ ‡zE„š”˜#,Ļ>‡Ė›ū§áFEtū “ßNŦhCđyĀJ H¨ĄHŖ1˛¸ĒĩãĶ6>Ei1ŋÉæ5UxIDAT;ˌåØíĒĐ:x—ŗ#QđReä‰oĖĨü §Ëk¤8~HŌØOëNãīî͐š|Šž íqVw:I-/&MU6ēôØĀ06°X Ä;âņ]Ą+t…ĻŌTšš_îŠĸ"*ĸ"ͧų4ŸĻĶtšŽ”PJ(%Ėfŗfb&fæã† `>'üšø(2¯Ø>ëļoúųGŋč;Š+ôtÍeōėĩâ(ΕĨáäPAAQpˆ+Æ\0šXĀÖ*OŌÅ:įU6č¤ú#€llÏ4Š KB"U\֖ã¤u9TeP‘’ĶŽéŸĮļéÛ FÔ%]ļ ›Ë1>,Āu<ų‡Iu7q€†ˆÉŠ‹ēëÕÜßŌ4eššBËôÂîUˆ ŽĖ=Í~œÂt´ßT§  pCå$$ĸ•Ņ™o∕J?ÄĩL ŖAŽä¸š2íĖĢKË"œŽˆ—4 ~œ4š¨DNņąũ ‡h”ĩmûoŌE ˇÉårX~ÂnöŸÄ 9äķ+kiN<įI#xžĐKÆ{đ“EÕąÚŧæ]…WĐmœŋg-8Č!@ą(…–´Vīî.A]”Ģj<­wpŸĨ!ö‰P/ž+_°ß€ŽdÔúĶ=ĩ&9Š—ĩœm|olĢ ”pŌÕĘʋœky¸ ×Î~ųlŽQdUÃĄäwĻGãBĪ,¨čG‡øKšÃmĄˆ8ŗlūŅūŠuÉŖCh_Æû ąß Š˙v–ČÄG¸J—Ŧļv8Ī b ž¤<Šƒ¯Q_üŸõPõˁæ@s –Ŧ%kÉT’JRItGwt‡Š@AZEĢhUūH¤NR'Y%­’VI;ËβŗîëôRę.Ējâ~-]ĪZfgđ×Xu¨tĸhšŅ"û­ķß"ŒŽ ĨIĨÚv„¨•Õ 7 bCV ‡b?7„\œ`CŽĶH•÷‘DûÕō8j|÷ŗ{Āļ.zž‡ÅdcØh&:Q‹hmÜ5č#1Zû^Ū‹ų§nt%bŗÚ@¯?NđOŦXQTŧxJ.å'ų5ûUéÁë[y #ŦUhĮßbų ŦWƒÕŒ%G2x‹9Û{ ϝđ&€‘PčM.›/ÂBi4†Í/ɰuŨ3 ĒÛš\ė ĄČÆ~ģO^kÉęFC˜ŧŸŖĨRZ Á-{PčŽÛĮB ĘQ˙ßĩCđã8˙„MjuŊ*Ũ?ģbāå=˜GŸˆąÚ&j¨=ęŽI¨Y0zzqPČ‡`cTh*,H‡YčÖoŠŅpŌFū:›õ=/ Ėn@kŖ÷Ļž‚ŸËÉJL€(č´ÁÎ!Ė–MXԉM{‹÷'ŌäÆĀ+’0 ÷ (ē #œ\§XkŖ|ÉÚą˜SákĒK÷éWØõėzv=ûļ}Ûž­ÔWę+õŠ)5ĨĻų ßŋ“pĀ‘Ÿ÷%…R”ƒĘAå`>7„ņŽņŽņn~šĐ}Ŧ‹-¸\r^EárâŅ>Ģũ:{Ķ/YÃgíĄC zFų õ⠐Ęŗ›”Á!€sQŒlŪÄĻõPîR•ŽîË×5!ųkŒTqQ÷ÂM~áÁûJ¨tģ„7‚C˛n°åééüÉĀ!Ž ļ"‡=:o ÔîŅÉ`Î*"Ęģœ§đ ­Pō´ETNæ Pk4a wpœÜTMÔÅô–á%ė2æh>Æ-øłqKzx|…į¸%žĻ7×j ۈy-%•>Pīč?đŗ‡o::‡Æũ,ÛßĘŪ^ W•Éĸ:U•%q‰M”ģ'⑯ĖH$EŒ°vŗIn›Áh2šŌPqL°9,¯ˇ-´ÂQSÁœ qĪš*ŠA¤ÉˆĐļ­5šc!ÆĪ„ßGgĮ†č-ü”é÷„ŗ%ŖŒm ¨´8t÷"k#׊6…Ä\!õļá“éĻč'f˛…#8G Ą2UĐ6¸wĐ+=øw´ÃĖūpŖbAu=ßŨ%ÂIĄąæ&6ņ=  @ npHąÄ¯äĐÛ)ŋđíā@kšdU=ãÅéØō2ĄŨ™s '€‘(N%ĘK,ÅfĶ ­q€‰ę°¸ĄŨdũœSĒžĘ1Ŋ#Ę‚Ø‡STÉX~Y=ĀÚIWŠØĨ1‹›ÅÍâųœWJļ’­d‹&ĸ‰h’_ōs9.ŊCīĐ;ųđá|Ŧ$]ĨĢt5x)x)xé°'fx‰R˜?ŦB‚ũ„>6æyq8ãėąšˆ3Ä.(F.ŅĖĒΒZ/Áđ-@+ķ‘šÃÂUë5Ī3Š>3áB4QO{Üĩ û‘+#pÍJĖģHŠ]$ˇ›ôOOînå“ųEXvUOqŌŸ;Œ ÚĨGHĮ"Øđ#ƒĘĶĢTQ¤;‡÷'U|"~eŗą$ŋĶ€ŽUČS7駨™VÜU.ķ#ßO˛8ûĪE“6Öŋ¨~UžZ¯&"ÁEņü8xH_ ,įCö.dVi˙|ē|čøĐ™5yMZ‰¤Ą—x„MčĐäĄ(9é–l™ŖĀø>Ą'ŅÁ˜ĖoÃ˛/{ē‘Ļ–w•aƒöč‰nc…Ąĸ;Ī„n=éŲ ‰åÖ@€JP3ØXÂKéQG˜:–vÍėæšsāPŸ‰ĶJLŋ‹Õ>ƒ#­ĩr}Mjũ8‡Ŗ^Ã(¨):Nn.–Ã1&ŧ™L îΊČWø=΀€ģJšJԔŋ€¨˜Æ*ė ‘ā0˜BĀŌUu2"­›Ö4~Č3÷üņRáíõVĶU}røFŪoŽņĩ€ŽÜ1fĀĻÚ\ž>`ŅP6d‹ĀlŌd{īŖT6DžĶ͜Ę;āĀ|ĢŦũ„Ėkô×ĸÂõ}ҝíŽR0p_h¨z¨z¨ēŌ@i 4PĒ)ՔjTˆ QĄ{ķū–åꋾčKŌŖô¨2I™¤LŌęhu´:ÁEÁEÁE؄MƒøÍ"WVĖûʖÔŲ`ÔĘŪpŠ+!ÃS™"Зn‘JÛy,ņ‹96"đ NüMĒH  =—Ŧæ+E‰vI_u8y˜œ~úw‘ą8cONÁuãjF›|Æ ™:L–ŋ= SW6ų„yŠõąg3Ns5nD#ˆëŧ‰<ĒÛą7U阏 Ža=_CYPĖäQ<ĶO4H?æ”wPH ĩˇÄW¨ÕKUíĖĶÛEۈ¯ŠĻŖ,^|   }ĸLŊŽõž’\Ķ9z-—Œ|ŗd}žÎÕå# hĐA¨‹ã qÅŧÎFHŖÄ=ŖÁ@>‘:¯7K@ą’=)8Če ú”@_ŌėÎū_8d×đuPœzŨũ-!]ôfQīâ§Đ˛ė}°ŦN91¤rŽŲ@:"„4ėĻgÅ>åyęŦocŠÃ@ú)îy*Ĩ[ĒæXHšjœc#¸ÎčĐAˆÂôĒ‘Î*ČL*Rîû˛@:rØ  —ŪU¯‰Ųm™ ÎØŔđ˛EZÁĻ5fQ 뀒îūÖF6€ ˁœeėÍ:E*¯6A=õ-‘OSƒhGŲ2ö˅ö5žŖēSŋę4YũÚõ^ėũVĢąĢC›C›C›ÕD5QMŅ"ZD“|äCi”.ēŊëuQu)•R)U<.kw´;Ú#ڈ6ĸ­:Vë~æļíĸŖēÁą4ŽhŲ÷! NŦ•(C Äŗ41ANDŠģEËĸH\Å&öÚ{åYųŖ<-ĮņPk%Ą Ÿn.ܑĨŋÉÍĪy¯y({9rLŋ¸áT› YÕģˆty;°!\F.õ ĘJqęŽŊ։Š@Å6ŅAöƒv˛+l´•aŖ“ėÃA´m҉ZˆÖrŠĢmR7­|ŲŗŨžĸąz ō™€@ķHÔą2‰iåīņ"*ŠRjs Ãa\%U4ˇËpęYo‚‘wßfĘŋÉ:GS=6ėKž¤Ë¯€JôŽą_aĶē3>•Ŋ đ CļĸĨhŦeGtĀzsuž /5Šũcî`n`2ܨ€îÔ\í~šŪÕ!ÄŦ˙ũ›°„TS(zę7ÂÛPOz^ų&nĀ&­Ŧ>DWSÃ. ˙} đŽâ:‡´>J§‹>ō3~"ˇķÕ]´1zbę'đĶ“ÆdXHBH\ā_áįRÂÃûlĻd§—Ļy'K†‰AE'?<[D–Ų×Ŗ§H.{ąg/LėP7Öxۘåmíx%<6Ž+VŌ â>û}û}ûũ| õõõqNœįō!_đ¯û IH"yČ#*ŠŠĸĸúœúœú\>j&T&T&Tæ”T}ĒRÁÎKŒ–ö‡â]UōM*'KBĄzf8€ŗ| Ā \‚Ām\!y”Ëéüŗü9:šĘ\:VmÚËg”6MvĒË<ûnŦhVūtŸN⨆•W yLVå_xž@ˆ8ĪÁâ$LĖãHxŲe˙ˆl^iŨ‚Gžd–ƒÃž•;€Ēˆ§ÕGŠĢģubs‰õxĒŪ\ÛĪævšx›FŠä3'q9üŖGÅEWũũuĩŒ[|ōFˇŽĸj•ĪĪĢSũĮĄã͏“ōÚ-ĪĻŽōÂiī‚WÍ'Oí[ĐĘzųVŖ]ûŖ#v%=€Ņ5d„Œ‘ũU3ÕL5“–Ņ2ZV}€@Fūöũ@?ˆĸ„(ĄÎQį¨sōö|Oûžö=†°ß$(ĩá USÆÅŧRá%ãáôĻYį´vĸEđ<4TÃlQĒ $‘ iŧÎ^Ëņĸ>+q¤ãëZåĘ;‡Ū¤VN;ūoäŨ@ÖßEĐdĮũRå{ã‰â_öæĶ—?_q†âŠ:íBkũƒ¨0Ö}ŠĐ^ú(ėĨĸ;ņĒ{MáM§‹~ŊõbĨÃzFDYŧ„3Ô!>d—ESėH0?Íkxą FĄšE)^.Žūî^ŋbĒ(§˙'‰”¯8[ÄTÅĻČ EŊ8éĢtĨ ›âiúâ´į ›ŋgŪâf›Į)ÆęéQāŗâũ-˜#úˆÃTéšČƒī*ßSQeœ3į[“CPšĘĶâKė 2ö^īy‘Ļ'@†ØŦL` {đŅ_Āa*’Q“jkĶÂnōë°÷ ,ĮņgS|đ@äāoÛ8[mA+đRčũ;­Ņ@Ģž‹ŽÄĸJ3Zq°daĒķY…$LŽÆÜÆ:7VēRô§/đ˛ˇũõ1ÜØ~8°nøåvä-ŖQ¨)z*ûâ¸^ĻöZÔĪÅFxî7)ę_ũŋúUÛĢíÕöŠKq).ņ‚xAŧP‹ų#Āp ĮpŅQt•x%^‰×oëˇõہցցÖöû€}@ŠĨÔú[Ŋ5ũÔ.Î'Šø}č}įéŨģ\ s\ņŠŽŦpĪņ”Ŗ8D!L‘1v¤t'ÎjŌŪņFĩĐČjTKÛ>ôīL&Åš=ū’úa•ƒ#\ö>ˇˇdZKõ éS5Ãõژ|OöĢé%Ņ9įŊ‡åôĀûGgĄ„˛Šúˆ“ÎÉŅEiŽ"~>i×g“_ä/ŦæĮ˜đXUĨŠÄ|ĘŪ)_āyÅžjįâ'–Å?i1ø^”ā—čv€Ž¨ũ¸*jĸšhE€ ˙ßm#?q”s0wŪeÛŽĒŧ‹ŋĶ>( ŗî–Æyø"…‹qę—(KĮûqÖLG!ԓûŨĶ@˜AīÂĢT×ËđY įŠTPĀ‘ß üÎŽ!FâˇĩŽā†Ų:ô öpÕ°ŠnÁ÷ę_ € 0B~›Wã1zTßqœ|Q¯”Pétė´ō}ÅawųøZœœ™r¤™xķÍM ņœīÂõ^nn„N—Š4ēiŋēž‚ĸ}ážNņÔķl˛ ÉkĄ ÆËöįŽ-Åm>™ ‰ŸÔĘ÷(šĨ[ēC_…ž }åšåšåšĨ<Ļ<Ļ´ÛnRōJëוžá(ô;€•õŪ‰#´l]ĩĘŋ<¨5ښ…ķŲ1Įh’[ôŌAž.ŲØŒp:ĸDƒPm0,üMĢ>?Íŧâ?‹?—•Ų¤LķŠTNÃAžĄL‰3`y?"(^é'ŊNkÅéVOÚAÁĄĩs †*FhĢxˆažâ fŽ˙&YzFDˇ Cū†Ã4’âĸŽ4‡ä­cO`}–’ë˯ÃíŋDšr[+Å!ōˆMđqQéĨbĸš–‡]QEJįQšÔ`Úã(š‡ËäøĪŽ‘…O÷ĩžīŦKŖé—øRĩÖRXōčæa Q[ ?ĐZexî)ŋ#pĸ,O˛>0:įåĖÅúōŌūƟŠcÃdÂũ%ë§p §|W|W|W´cÚ1í˜:O§Î[Äą_áĢģâ?Ü °ŋxī‹ëâē¸ŽÎRgŠŗ>‡Īá Œ Œ Œ3’dãū2Ũ8›™RŧÕæ\ÅŊĀēnÎF6˙`7āā6¸áR“~"Ÿō•ãü?y´x[­čŧƒ„°uÉ'°„÷ ŽúAĐrŊ9  ! ×xÛö^9ß|Ķ—ÄGҜŸ!á‘“ûILLjJīŌWÖüā@Q%ãŌ዁Jgû­įyĩÜķŦũ}ķęYÍNEÎ=‚æįNĖ›*/<˙Å8<’“ræyņRĄ­MæŠĸeíūɸÎÕQŧ­,ƒ—ËĘS`äá"7JÖ¯Á‰ÛÍŠ $QOXhĖ+`‹ķæôä÷pđâ&\tF܀Ёt yrš}Ē8 îá€VÆõH„„ˇų€Ų¡šŸ’¯™3ȉŌånüžÂ¨ÁGå{v9×÷YšĘŋĘŊÖfhčLŸrĀ^c>ĖyF ˙**.̃Dl\dåeĸdš&Ŋ։§“Ÿh4<‹ĪÛōܙ—ŋY(_žŧkea~Ũwôj{¸éĸhƒ+î,ĸGÕ&a&URÖ8œ¸}ķŨÅ9•ųeװޘCƒ_š‹•R1;ÞNIhų*b~—öZh.4†bB1Ąũ3ũ3ũ3ÅŖxõĨžÔõQõ˙šä‡DĶ`ŦÄ(1JŒžMßĻoËW ķ–ô–ô–|@DHÆW.›¤.MjĶāfpPh˛ĩXūj%†Î`1¯ļûq2ŧT žßŠ€˙uķá2vōŗ\‘á2Á \ÄŌEå*ŗ>ĖU Œ hĀB.”ĻN(o}ÜÎdåšBGáûĪž$‹R|‘CŌÍË|Ķ'hoŌsæL™{VûžŽ¯ÛŠų?Láö!ŗäŸŋš<ڝģ.XėØ3’iRÚË;‹‰–ĸŖšĮķ÷s.˕7§o<#ĪM=.ßŠĩ딨ŸØŦ~=Q¸Ėų éjtDų÷Ą öAJĶ|’Ų*¸ ÆIūč.XJąMŲVmŌÅkvg,”Ã@4 Ą•ĸ.0āÅ hú ˇ 6='žp§Ą!Õø;ūŅ.ÅßSŊÍų2Æōep!ūūIuéÁr¨ē‘÷"\¨‹§āå ũ=%Šī”"&æj… ĸ|ŲËŊÚRų„ŦÅäŽ;'öäš3Uŋž%÷Ļ59Pˆ˜_xē"R–ön‚ŽGņ:4ąOKŅR´1T Cé:]§ëų•?Í(Pč6ŨĻÛʛʛʛępu¸:ÜŅÚŅÚŅ:¯J^•ŧ*ö û„}ŸŽPĻō~X¯2J§ÎĄT^Ō2íīdĸ(‘pĨv&6Ä|Vm/ĩŖĖM˙äŲē‚ÕÆ_‡y_ŋąŅxZ Foˁϝs¯‚E{āÚ]VgQ| ‹P›ŋ3Vz_åš<ĖūŒôJ&†„“ÚĐD žë™Ëv0&įĸhJ¯‰\pÆ?ĶŽ}øŲéššÉÛ[OĄg2žaÍ ž‡mæų<'†Ē§˛ˇõ€8T(ؑTB-ž-ˇ[5,¯ÕßÜĒDˇ"_ ëT&§c-œŖËĘ}uĘ2\†Ëp_ĐôQŽ(G”ú‹ú‹ú‹¸*ފĢƒ1ķĮ@üa­ø‹x‘ŽŅ5ēĻnR7Š›1ŽGŒ=Æcņ~āũĀûÁīäė^he•†ę¸ow8c|_ŧm¯Š[Ēę nseã3ˇ3öũs`īČhpj,Îæ|xa ĸ,ąŦž§Kj/9ßp į ī?ž \(‰öü‚VwZ_]Īúz÷7ǃ.%‘Œî´ Ė‚* ĩ€‚P‘ž….×?rŽũHāa™žąäp5yūĖKsŋÁ+žŲ׋Ė2ßõŠ-ž*ǎŽE…Äym)‚č!{!€ĘĮg˛īĻÍĄxš‡+` ŦŦ‰øNÚú°¨īåÎÄĖZ-97æRÅ]|Æu"ųEe?ķd>,wBÅŽãKü2Ÿ’ ĖOŧy%¯•íIG2s{0“õˇÉ¨ÎĪü§imÔî”ōBĢtp@=1¸ø”‡†ÉC7įíōúņ1ŗo˛zg΁âœkÍ A$%‹7!Đk@rK` íĩŪ¯¸¨b‚ P‰\>f'đŧ`ģŒÕ4Â93Ž0LË,! 7Dĸ4EĄąėÁŸ[CÉÁcĄÃv1ĨUᓍ’rTĩ°öīđîđîāeŧŒ—9^qŧâxEŅMŅōÁ^Ä?Ĩũ#+ŒÂ(,ʈ2ĸŒ§Ä)qúÃúÃúݎ­Ž­žĶžĶžĶŗ"fEĖƒÅ`Q€^Ĩ2"K[ūVåŊũŠxļīwM)ĢMēZdÉëęZ"#ƒWÎŊ˙Ũ­Q4§„‰‹Ot“ŋ2õĨ žĪÚj†ž˛ā[ĩž}2oT%W› č…íM1"ĀĮ/ôĀ€^&ŸäSVb`õRs\1p*ëô<>Hˊįafœŋ˜nR$€™hĮi@üHĩįÜåŖ~vl û(l¤}Îûî™éü.‘3hĻŖ\xúRÜĨx2Å>ŠĻ¯a!?ū1Ãnđ*Ō­eÁá,u=2Ā'íkÁIēōÄO-éŨ´÷<*6”|ĸķTŌĸ‡—_-߸ôÖ˛8žāŸwŗ&4ˤÉr$ž ø›čOĘVŅ6’J×ļÅOoWSė›ĻˑŇåi~ÜâˇyŽw×õb˛Ų“û 6nŧë{Úlŧ•'ĨbÚüą•æ˙”tũ׈+čĒ•ˇœÛˎōšØRöųî[is䜔j\íö’=^>wįÕ}åØg5 ”@6%‰V úŠLÆîĀ0–Ž5ÚR0Úc8 kŒ§ h/ômÁĨön枯ø$84­ ÆģÕäHĀî:N:?mDĒļŽZS͆fšnˇ ų•ņ1õĸģ—oĶmũ(&Š÷¯ũkåZš6ī|ŪųŧķŽ*Ž*Ž*ęu‚:Ą ôų}Gßåƒ~˙0¸ÂĖĖŦjxp@.–‹åbÃ6lÃö ÷ ÷ ÷4ķ4ķ4‹û"/ĸ:DuˆēGĪđR9)ŗíî"×ÄåËûŋߥ¸†+/˜]h†ķ€•÷DÃõa;Ē}ēĸÚ=âXąŌHB 4읅›¸ķöÂŧÍ×ļ…RŽ6ŸæWĒŨūh}ēÚלG‘Ŗ†GdŸcxxH”FüMŪôĪ€„~œÄBŠĐZģZŠĮÅ׎ĸRÉŪyÉËsėÆ č¨!÷}*?×æÚ´2jEJ%ņiɏÛΖ׏-›ų ‚ô6ڂ\WĸƒßZ›ÕŒ}özī % ļZūĶ$—h$iZĢđ¯@¤BcÃXëí§Ú‡(Ixĩj•<ļE *Øŗ~9đÚ'ŋ8ėáYq§R°*ĨvHíŊ˛kŸø•^sŒ˙âwĸ„˛S¨ËmšJŽ~nž]ūŦwq ´Î:rŦ&]ŖņbŧˆpN¨‚Ŗa%‹•EĨ˜@…ÆüPŽ÷Ü|^xíĨͯđwĄ9ûNR4„@=ŧV] ø!P‹ú’Ë9;ĸ&H\4k°ßø2c>$Žs_ęo¤ŧŦɉt#ļlՃĸnáVMÛ9'ûq”\J5ĮFDŠĩÜĮėåæŗyÛüo[[Đ=ĨĐãMãfԋš“Šŧ¸īō›Gy”GŲĮŗgîŨ9ēŗ+ەíĘVgĒ3ՙ;Āīí˙€8-3ŗ•me[ŲÁmÁmÁmš7soæŪ´>˛>˛>*ēžčúĸë•%EIšīV^ĘŗîZÅĖ.[^yŊAøi{ËíĪuíõÆcÁūcFškD‰Įĩš•_xf ZÅy0b–˜@‹1ō’ųĢ/Æ~?3ūÔwrĀÉë›iwüß_NV*ŗÄ|ŠÆuŌ`s’ÚŒ8Cˆ<ņßŨJāƒ—JŅzڏ!Â%~Õ6í-ø#}Hŧl?-Š&ŽĒų9.ÆD•Ģ ÃÎĻ/Ž 7­)B|KŲ@Ŗ!,žČ ĮīctEgđ=.^ĶFƒ@`^,ß6?†† x'y‚\O‚VDyĘŠMnwœŲ1—ĩŸ“ē×˙¯‹Ī´ôō6•ĸ,ŋũؑā—e蜎åUŧ˜Âí"ąX~kOO^}jŨCÜ3WzAC:ž#_Ģ΃ÚģÉYä‹ÅÔjg?°OˆEõ,A<'ŋ¤OÜ[‹tëS.vž)Oē8;ŧĪÎāJ{WõāÁvĄ+Ū†Ę3IŋÄũŌėÃˇj'ĸĘ{īž#cdĖ͏n~tķ#}‡žCßą7boÄ^}ž>_Ÿ¯œPN('Đũ˙^>GũK1øm´ļ)‹•ÅĘb5ÖØĩŲĩŲĩ99ȁgĢgĢgklJlJė} ]Œj]ė¤{HųsŊ]ūĄ'ĖE×c]Ũĸú8žT/xjj •įuˇ=1øÉíĶFŅcŨ§G#ËŪn|J‘m˙¨áĨæ~-I)L-D3G ž.g '†A‹ƒ% š‹ũÛ!‚cĖ18Í'hˇ/šT™¯ĶT̤ö ˇ!E_”´•M˛”͞ąų~‡á=v /ŧßÚŠ }õÎîiˆ¤Éĸ œ L% ‰ŋr€iŸPųĀ߇Õ#@čI?ĐgŋȝĄ@ĀFdp^ŽÆ~œį¨‹3T :˛ėžŪ)Úā8.—ōwˆ×‘ЎņŸ0ŠspŦ[™X%øôy+}ôáļ<čF͟ąš3ęÂpîI9zi¸Ë"úPĐ ĀŠ?ÜĶTø0Cˇ@˜Ŧô‡āO@Â*Č™ŋųÅ;¨‹]ĒG#Jy3ė&J9N~‹yœ]ŒkĢ#?đŒõ<įŪWūŊŪ Ú‰¨˛Å U—[%ˇJn•|Í/į÷ÎīßĢgÕŗęYqTGņžĀe4ū’ )šĸŠh.š‹æĒSuĒNŊš^M¯æēčēčē˜“““‘‘‘ŦŨÔnj7īũiøā”jMæe~~ö@ߍs?…ÍĶꇷVf%´ĢéT~ ĘUWÍÍCe8FQøŒFĸ(Š*]Q#°=8…lŌŠŖÕŒĩæh6Ô§•wIUëÛĮ8hõ“ÕTAƟô ˆHŌiŋ UL4īp@öˇ‡j×Č]ĄNy˛ „‹īŽs T<†ÎÚ¤ˆrxûÎL °’œÆl@ą!}BĢI)lâ*Í ūŅKā–`ˇú°v”œę-g"6$oW“7°^7ęPjã îÉ{¤b Vō‡ŋäk­Ęb8æ 5ūEFõ\θō°S\˛×ßyqĮ Ņ”ęŠe)ĘņSR]6q]Ų ÷>Eœß[erör0Čt°‰ÖøœTǤ.â ¯TWPîjĨũmŠÄœ´eč&R¸’ ā#mŦ{ % *–‰IËę_“R­Éc€šL2'™“rSrSrS\Į\Į\Į´fZ3­YžŦ#í§ũ´ŋæõ/s€|ۏũØ/Nˆâ„6RŠtžā|ÁųB°B°B°Bfș “žHú"é <ƒgPPH,ęh]Ŋ"ŽV˜Ö)ÁķÆ—fŊĢÅĒ1Ōëė=žôYņÜĀīĒŖEˆęO?S5–؈ö°ąAŒ((ˆåĸ`J6Ļą‰E|Uĸ9 }žļ Šė:›kŖ €(DünÍ͟z PEk+Å [Ä-0,;:÷YR•#îĶlˆ;ŽnŽã‹‚™Ÿ‹>hĸMtyuîo,ôĄÜƒv›9`%ØŲķɡyĀ×zūe+ë[>ĢũJßMŸ>‚.‹ŲbbÁ(•Đį†–ļŊ‰Üŋ°Ɏ¨&ú%/oxZ\q}—Œƒ)5/։’}câ*7äYĢŽĻÚvëŧ€ē8ú€,(wiH€c-›gĀ‚ @c*ŦUšÂF]@wĄ*ATÃ->gū€8îj•Ĩ9ŽĄņ¯†|ž'ŗ#‚.Q>jsÔé …:ŨąÚ.ũžŋų)>Å§ŲŽlWļKÔuE]į2į2į25IMR“ÄqGÜAOôDĪŋ>˙ˆÔDMÔÃÅp1\YĒ,U–ęô úwww›ŧ yō6x3ŧیp„˙&ãåH‰^\ŧĨĢnŠ­føüY_]¸ĒĖö}ŸqBék\ņ뀠õt†MôGÕæĪ˙ˆš!AĪ™õØÂ|ë& *Đq•ƒę>Qt5C}Œƒæ4ËFŲß9@ŠBĨ;˛‡Äqs$ėđŧA­ØäĢÖGä´ēx&Aҧ'œ‡g( €g)Bt¨„RO‡§Ųƒũ hN›Í™l`-G毐ØÎ&í5ÂHáĪÔ8„âqü^PÃĀ&—úĨëėPpâ4)`ĢōÔŽ]Ô\ŊäōËÃæ_ ™¤TtŋŽ Š´Ę@ˇ‡ā˜ör¸‡†%´Ģƒ'ŗŌƒb-ĪM#‡ŌĮ]…Côąžt•v„-™šÃ`uøš`Å ĄŋUšte6ä ¤_CšĢňaGĘIė• yVŒ~0ω/%û‘œĘŽĘ5i]Ųũ^qũšĐ˞˞ËūŽūŽūŽ‘‹#G.֚hM´&Ęjeĩ˛šjSmĒ Ú_ üŨ<ĀŲ2,Ã2e”2JĨíÔvj;ûœûœûmmm3ĻeL˘fíˇö[û˙Ķđ蓎uëÖŪā;vûØņ^ō)íÃđO(Š&Q5ōv÷ĖB Ō‘*KBÁ0ã„L8ø Áčˆú×ę)RÅ) āîÜm! *ā$9Ä!36úI€%€‚8},=ÎĻ,î{˜4ģĨ˙4€x Ãۈ"ˆQH_Š\´ËøÛ‚PÎz€‰R÷ŧg l$Éfx  būF ká"vĶ(ąKG=ôa:Åã1ü†yųOŠGwŦ ĮõĨa.äDG9]]AÄģ×'lÁŋŲȈt—ÅŊ¨ļūÖÔ ,ÕS›¸ FôŨūš(B˜˛'˜ÆÃ`˜øâîw1áƒ1ƒŒĶ¤ĸ´LBņ‚^dP’QIö°>5›úŽ5DHt)ômíīÃeņũõˇ= mGÛŅYÁŦ`VĐų•ķ+įWŽ,G–#KŊ¤^R/‰ bƒØĪpūã7ūáŅʗëC}¨2Dĸ Ņ/ë—õËnÅ­¸™*SejƙŒ3gōš¸îčžâ´Šę0°ÄËÖJuGô΁ĸö GŦ<ŖÔsîË×L‡€„ ‚†SPéŖ*‡PIžĀÆok”/" QŸz@ĶŪQ} Ô@M,>”%]´ļSaPCëŪ@ŨSūW 6"ģVFÎ'¤q’ũ)„$DQ¤2‘ø‰æãŠLōũ F ,¸[>†6ŅĶ ':ZũHTÃFn”ĮŖÔJ{Ö=šŪ¨ƒ9ˆ ØxĪ›PūĄ÷Du(í×ŌÜ;¸‹2ÜU’v)MÛūŨ€!úÎȡ1žR•­PÁpąi_õÕ ]Î xÅЂŲBŽ@Ė%ä\ø]šÉFY6PQž^4*ƒ ãtÁ—•°yZ‰pŊ¸V)+=ŧC™įŖ––KkߝįUųû„læO™?eūÄ/ō‹üĸëq×ãŽĮĩL-SËT>W>W>ĮėĀŽĀôOŲHŒÄHQDE´ĄÚPmhū­#ŦlXŲ°˛y‘y‘y‘ž=/z^|Āšķ–ŗJä‘ã+Æu\Ąļo:ˆģ(_¤jjer@AURŅØ>Āô2ÜC,H8ß3‚ČâvVŠAē#pŲ4Š;°I'ŒWØD9ÎäÅ< .Ą!’ŽĐPėō΁āÄĶná‡FŖđ nņ3Büũʙ÷´ ė‡ÄKÆd¨(Ė&8‰´BaũJÖōÜ/@‚’…cČ|1Y*ĒųÜ3ā¤Žb&Īäãö†ģL‘Öl>Áü#âĐ?e/„´nxAÅŖr€0tƒØE9˜Y`  ` pŨgH‡ĐÛ\@´°‘ Ēņ9ĒŦVmÃCŽĢŠÆĢMÔĨN7íĒ\w}sé÷¯–7?o~Ū|_}_}_ũ°Ŋa{Ãöę…õÂzá|ä: §á4%Q%˙šŽĢ˙äiĐ åCŒÅ5qM\ĶkéĩôZö {…ŊÂŧn^7¯g”Ī(ŸQŪ‘îHw¤;]N—ķ>b Gdôˆâáá“*|Öu÷ÕcoŊ~ÆųĐŨOŊ}]Da ՆD3î@Đsg?đ]2‘ ›ĒŌ§ú u8Ôāfc99č k hŗ=€ Æõ? P^ã¯!­úš@Õ];…IŽ8qRv„—›ņG D91wIš~kõØB[Ģ6)ô”éFqrŧ&ōôÔ0"ĩuØÄQ,ūÌĩˇqŠv‰+J7QĪŪ/_åæædß×XĢ/ ˙7Î9Ũīģ}šĘ&fØä¤bĸÁlŲ߆‘f•Ę KõGĩ„ ‡1˜`ūPõOŽ€D,/G4M m”ˇđIXacæ:gû“H91ŧØ'õ˛ôĪ[zk ‚0úéß2÷ 87“˛üĮī(§Ņ–[h§¸  ‰UX Û~ŲÛ JS]ežļĻÃF€{åŖĄūl0a‘¨É…émŲԘbīEs_9å\Tw“rŋ>ÚŌ5ŧĐĶ÷>āÄ?ĘeJ_’ž$}‰z]ŊŽ^wé.ŨĨkYZ––Ĩ| | |@™”I™âŽ˙™ûĪ_É˙Ŋ:5ذhD[‹ŦEÖ"cœ1ÎįwûŨ~ˇ§§§KuŠ.ĩ°ģ°ģ°ģ@Ļæ^›ŽVŧ?oûŸ-ãũŪ#æŦÁáaŽšÖ Įõ$O§;ü<ÁšČB&ø>5‘ûsnŽãsrYå<)lˆ‰ŧąhƒË%I  0Ē“f?kæ Opn‰Z2âĖ‹lžėšUŌ­?iå&ŽÁCÄcĸc„•ŦŅįqS aaqđī|’ōT†J™‡Í—ĖFĄqá“+˜á kŧōtŌśę§˙âĩŋ˜7ėZMcåžãcfŠņƅ;Š(BsÄČģn…’P¸ŦTČižiNbŋ•Ф‚‘‡Rx°VZžIQˆA4ĸšb\¨ļ]ZĀ›ĢŒNØâ.Y=vHīˆj)FĶcŠ-t_ ŋĀ/đ wbîÄ܉ U U Uz>ęų¨įķ-zŊ‹ŪE Å@ä˙ķŸ6õ_0Šų•Ögqg•ÉĘde˛ÖQë¨utžužužĩ{ŲŊė^š›r7ånJī™Ū3Ŋgâ€Ä‰0ķ1ŋ …aø•jGH™Ô4•o…~Ėũ,oŅ™õß­BՔ+ô›Š‹ë‰œ•Ų‹;8 q:(Ņ¤ËŸƒŲlۗķž'•Øų2‡”‘îŠ` Ž@ōOúŽd¨čÅ_3›ũŊMŠFƟaŖ–ŒED]°ČËzõ4ōå’$œpĢ{w8O>gŪ 6r](v)U§ŲS4ú@ųo˜I&Ę÷˛^°üøōŦĩcBĢ ÅÔú؁ĸBtüŗö:#k§¯<ņķ.qĐܜš˜Ú(%õÕ|=ī’ĐÜH%͚åĪa‹Ûú‘ŠųÎVäÍĘ7ĸpö7‹ …Ŗ0ĘČë4E|Š5Ņ3^ģíîXe÷?G%O5IĮP4šęãI<‰'3 gÎ,H$’#‰H䗸iŨĩîZwĄ Mh˙ĒŠ˙¯s€{ŖC¨uP”%AŸ OĐ'H‡tH‡Œ•ą26įLɜ3ęvuģē=vOėžØ=ųôĨ-ĖĄąâĶȧ˧wh ĩ¯36oņŠ‹_ Sx| šķ]m–ōļpđ1û56p+`CAJBŧōĢdN7X\ĮzļU3PJw=ƒ´[ĩÜ(P:ĄŦZ„4$§ĩ-؆ŊōŒšBÃyBCķ ˇÖ’Ęņq3iāZNņZr*—ĩë°ĩąŽœÍ•å%é1gûbDHÉÔː‚4€mxqãK§‚$Tãŗr‘Ŋ åüĶŌŽŊ֓YĪ\˛õåvâÛÕvŠIú“Ž˙S_ęŌÃucåæŪ¨wcĐēAâņá;x Įa# 6€"hBNŽo!­o%`ÁÍA"Ü|Ēũ¤˜ ?P_:‚Pa!tTÄrŊŖä Ģ>Õķ­”ĮÃ.¸ÛÕøj°7˛RŲÜGÂđ5ą÷ŊĪėÁžė¤ė¤ė¤ÜøÜøÜøO„'Âãläläl¤ŠšĒŠb˛˜,&ŖĘŨŋú™ø×6WP4ąVŦkÕl5[Ív|įøÎņ;ĮãÎ K K KÍh’Ņ$ŖINīœŪ9Ŋ‘…,ÜOž;ƒ>ë"חYØj^øŲjƒŸęä{QÔs 'Ø'R"DcĖ#GAņB :ŲĒoíįü¨DSŲä+vCHK–ƒđ  ã Ŋ÷™€š<Ų>ÅA+%pŒKŧŲ|Ôž‚Ûŧ×tŖ‚ō˜ū $īÅĖļāE6ˆĻKŋęÎ`ƒžāBĘŖ‚ĩFĪĒs3…HōĮä˜}á×ë´z>š Üđļ?á *%î?žĻ#éžŅžŅžŅŲ+ŗWf¯ kÖ.Ŧ+ŪīŠ×ę õ…J´­DS ÅP ~ŸaũĪį@ūw€?˛ūčūöP{¨=ÔÜoî7÷Ķ‚iÁ4ooo˙l˙l˙ėā‰FˇŽnŨ:š¯…k8ˇĮŨė™yDÎ/é8lœ¸šÍ͎ÂĘOJ ēh/ĸ.ršų=ōŒ9ˇƒlʖĄį$Ķ A¤AĐŗ”Dēv!ō"•Ûú›ŽÜŖŦ8NšyÚۄCVÉĀ| ¨;xˆYžėĶíå…ÛköeqDú̧3F—” 6üģU^Eíį&6M"Ķ 1–öđ#\Ä/4UÔUO‰’ގâžĻ4UéËnbwūÁ‡tÔÆžo6NȓYß\KKâŦր:%‡7ž)}gŪûvņįÅk‡NDŽ*ŋQ˙ŧܘī:šČ­zÃÅĮQkPMŅ÷“~Œf5ņŒē<Ũôú‡fŧˆmįÆÎ{AÛ¸|Q!RU›ŗŽ›w“%Ņ‘vŧņ›f™Ü<Ķ8‘Ā…3¤ã9ņ#"ėJŽAü#vˆWí=ü‹RŨoš^¨ŠËW",jd)Ī\+ФÖ܎ĸ¨pŸĖp ×pÍŗŪŗŪŗ>#5#5#5,#,#,#,",",Âîw„Ģ5Ôj|ˆ#š  šü;&éŋĶ,X°ō (ėēv]ģŽšÖ\kŽ lll÷UņUņUņ+~ů$lKؖ°-æbĖ؋č„NRj1į\¸z.G=˛iA1q9g˙ņkaēãK‘ĸĨÉ™Wé˜Ų>ëIö" WaA-Āš€Ģü 9ES}¤Ŗnä6°E| W”@;rË æ6Œ;žy°ø)~ųÚXyė´ß—ĘLé×=r?ŧ´Sģ™ą+t\ÔsDâ€4œ%]´ĩ!\ŅQ}8¨x´\€OaíŨw`Ø0‘‰Ķb¤ž<ŧ}Ą?yn$ _Ļ[" ‡øŠÜØ–…¯.yšzų˛I)YâĖÃßĖāŊCoļ hÚ'2ė—é;E =ŽoŒ>aK÷ĄS•yq_UÚŠLŽēX2]\Õ̇էOŨS 5°ßI˙âā÷˛[úöũéˆĖ\ļž7fWؐúcâļZ=¸ĶĨų‹%Ÿ§ÉJ5|Hâ¤Ŗ…|"t wåFc€âÔļĀ=TØČĸpTB ŽĖûœ_˜…,vEųŒÎĒ,īĒš*Ú¨Ûqh%ũ‹˜Ë)q˜+°+r*æTĖŠ˜9?s~æü°Ia“Â&…Õ ĢVĶ‘âHq¤¨]Ô.jA‚Ą7zŖ÷ŋ/ŦâßŲļ 5˙„§$*‰J"Šĸ(Šō9>Įį djæaæĨå¤å¤åČšrޜ7>n|Üø| ÎŊéŗĸS‹—‰û˛^Øķqž§>]ž$ˇØ•wW¯w?Īg|单Nj´¸†û•Å*ö°6œ`€âg˜•Iˇ‡„ÔŨާ„`@ ¯„eÛū^°ĐK6č/øØVsyüdnõ8ĐÕ93ę˜Ę@CƒSŽ×R ņ B ]p~Á†˛I{2|MÛīģi(2A|ÚĒė§ĮÕXg ZĨ\Ōk‚Q–špsãųÜĨ@Ä+%—Ōcɍš†ÉÔŗUŋ-ÄC}Įn6†&~u8yJQôļËšSr°˜ ™Žŧa׸ņÔåž˛ųõø­†f›,CŲTæŌatæ–ØĄÍĖÍ:YX™ <ĒŖ¤:KŖ’ßi¸‚׆w+1 A_÷ËÂEŖÔWčHŦūĄælđŗ$ųč!.ƒ) ‚|íHØ'ÁJThģ=OĻøXОX3JŸė˜SĢZVīhĨŸķrôƒĻ~ÔAŦ~Yũ˛úeOĪžž==ėJؕ°+îsîsîsŽĘŽĘŽĘj ĩ…ÚBd‰,‘…įđÜoßūgí÷ZrÃ6Ûl˲†ŦaŦ1ÖkB-B-B-|ž_„÷UīĢŪWŖCŅĄčPâ+‰¯$žBš”Kš•øäF;ÉWírÖÆŊŪžĮŸ™ĩG/Ššö„ûSũ{šĨ†‰!vi:Éķå*8ƒEB_„JÍÕ$ƒĸōØ ī••ä°+˛aŦĪ{Œŗø€zˇĖ!‡Ú×DŨB#›Ã÷9)ëeęų‹ “(–š‚’yZŌ”/ÕÚ,]Õĸ&ÁĻO”šnâøÆÆ}8ɋȡ*Ží"Ëy8æōä‡öM†r=âš’4°aEYã\ąīörÜ—#œ¤Ō€}Ūí‘KlS‚(äz$ę[D+ojŊÁVŠg.†^¸ŗ•–•lŲũÎÛOúĮōk×ęũėD€j¨Íų_ąK‹2‰Õž';.Šæyē¸ęënœ§´qÔ ûŲ/ą Íõ8aķVë9K)ĐP}šõ,¯œĄœõ0ēņ`Ãį+v5üûJßšļ9eúCá.>W_yĀ÷JáNÉd2šášášáa;Ãv†í kÖ2ŦĨãŦãŦãŦÚ[í­öI"I$a"&b"ūKLũ¯ų3ųY‚üä…8/΋ķz Ŋ…Ū"Ÿoā ŧAĮé8÷|éųŌķĨ9ĘeŽJš˜41iĸúĒúĒúę}ķ(L<¤Ü?[ ­ ‹m]fsî°ã%æßōŒžųËæ—ÜŠZ¨ŦŖ–zIô#ŅÉŽKqX„ōđōgÖB(Vļ?™jw °åņĪc>'Hĸ—īKQi¨‹Ŗ<+T;Ŗ–čˇēÆ!ē!:) ûĖúÆKÔ¯Ąéū°ö°Å)5āX  ÄŸŒƒ 5¨$’ė~Ą|Æ\xšŗ’‹Ņ儞u?•žŸļ<ĀurW\ö œ6+}Ȃēö{Ģۛs@úí ä28đ=Ō1<ļu֓Ë؊‘@^Ų‹ŸŠ 뭒94Z‰AōØtL|ČąYōđ\ǘ4ŗÉĶxĘe$͆ܟV Ē&…§Y—šTŽŋAGš&'cR.tÛ$dųß´6꧔o“Ã[”ŽRų`˙“ú+ŅJɡAøü§āÖFkcZ´:iuÃÃÃ"†E ‹æÚîÚîÚ^pāyD}D}¤ ÄųŪšGáŋĀūĢv€ûū~øķë7ėÁö`{°ŲĮėcö ™!3d–––äuÎëœ×YÉSō”ŧÂ7 ß(|ÃÕĪÕĪÕī3XcíĮB'|[¯xˇ4ķ~sēęw‹ÕįüŽūėę­WĨSú0%†_§5Pä{ԃžŖJh¨L×oķBëí`<‰Ü„GMĖ`ƒžqnŽ_/ŌSŋīu\^?ķõœ)ŧĪ*į‘ĸąX[‚=ä\É&žĨÃBŋô|ВãF6—^ĸpQdˇđ–^ØŖœ ŋĩļ0+7$#‚„č‹ŋņlū‘ų•žŖ r‹īä8Îã c ŧĪįicp\†•yŗĪjyûLą9[yƒYÆĢ@Asš_˛SÅČ"ÎVéxÆĄEŋÄ^=ˇęs–%­<^ĘOđnCŒ1ĻÚ4>°Ô8Íu­™î3ÅG‡÷)ūąaaõKä4û€&( ˆR&“ƒÉwøßaŲQv”Ã'„OŸāúÉõ“ë'Ŋĸ^Q¯¨^P/¨„Gx„įŋæĀķ˙o¸¯ūn¸q—qYyXyXy8_¤•öĐÚ#6‰Mb“xH<$ō ¯đŠk;¯íŧļ3!”JåŸ)‘)ņ>?ž¤|į¨ŽRhá\´°vtŪ3ōÖ§å-ģē~Ë0íŠ=<+Ė5FW•ÚŊĨūrC[ęŽîA3'+xŸÜÄbQ ņ7æx¨č†XĮÃ5 Ŧčî|…SĀöwš]!ŧh %øšo"`EÉļĒūi]Š!ŒáÔ]ŋą[PŅŋÁ֝]Û˛xĩ÷âĨŌpšŽFĮа˙īfŦKāe"y20Ē52÷aRĩ} O˛EJ4–đ$ǃdų ­ĀōČ•đTš0á´_dÍ |‚]š‘×÷‰ũŠęĪŖ}Ņo^l]ö‡Ĩŋa>k 3r Ûv™/+‹bk:ƕ+Ũ|]ôÎTåámę:÷ˆØ?dvâÎáĪĪĪ”ĖäĖäĖd-V‹Õb#?ü4ōSįyįyįyũœ~N?§ôPz(=ÄEqQ\ü˙5õ˙=y€Ô ļaļ)ã•ņĘxm›ļMÛæ˜ä˜ä˜änčnčnõnÔģQī:û8û8ûÜyüÎãwŋŲęfĢ›­ĖOĖOĖOūЧûšûÆæÄUÖöđĮ†ĩ“…Ŧ"­oæ–ˇKG\ö^´’œõ4*ãœ,—j=Âō8ôĻŲŽęBąp H:$ō ˆÚ|VŽ3˜—rëĶ;ÎK  p1´ŠMĒa—ô;Z#(”†r‡ÉEä‡L×rJëēØQē\ˇ4îîms6Snģ1vM ŠâtĶ  ^*¨Ŗš[*ũģ§Фá%™Í"Ī-ŲŨŧģĄÚąyĩ |[ąjrßO7i{d—’' ` ?Eąp Dq˜J]¸XhŦ'ÕLĖ!Ãv´ôŗŪãæTŨÎMĩKE\BF‘@kŦ¯Qķ1ˇcnU™Ö}ŖÚÅŨꏧžšĶÜiîŧuä֑[G2ædĖɘãōš|._¤?ŌéwwqwqwqŧæxÍņšzCŊĄŪGÄq$_—˙_í˙Įčlæaā<Âē`]°.˜šfŽ™ŠŇâƒÅ‚Å‚Åŧ“ŧ“ŧ“čcú˜>Nh”Đ(ĄQäČȑ‘#ŠÕĸ?ôÉûeFčĪÛ×ûø_ŧ‘ļo¨)Ō+;ĒĻ›ŋfVĐ>ąVx’ĩpqGWöˆܙœ žH­ø WāŪjģÅS…×4j€ŽÚwYŲæÚņöÂAei ėC9‚#MpĀüČJP ŋY¯3q Œ[ŧW,Jy¸cú¸R‹rÂųŽķœ&ËYÕ(\™ÁŌį$TÄ#”ĀĀíöĻ(†’Û•7‰Cƌô9°°ŽŗČA•´Bú˛ÂûŲGߊmiwøŽŨěEžo–dĪ>ģ¸ _‘3deģ>oĨËfžBšËĸĒc­GŖ:TŪ¨M’Ķü+ܓ‹öŦĶØņRÔEŋÆdĒų›ŦíŊŨ:Á'øD^ĢŧVy­2ēftÍčŠa†aáÃLJwppĐ[ę-õ–Z­‡ÖŖŗKą‹2(ƒ2˙&Ũ'ȡũ؏ũųĄR9JŽ’ŖĖ­æVsĢ2BF(ôbčÅЋūõūõūõ> ģv'ėNÂā„Á ƒQÎ(gđ›)x¯ÍE;>`&ú22V^ŧ}ķčÕ`…ë;væzžōg_×gĀc|Ģ—ÖŪR­§¨´¨ˆ†ō%ą'ĸQņ)â@rËz#äÍķį>‹ Vä+ĸ"jroôo Mį,Ã*Â ōXˇW‹Z…ö×ĪŖ*É-ko•Ųg2žėÉkŒ÷<^č4Z”†@oęDēž5ņG*ąa?¸Œ÷īY÷@ڐ&OXŸC o§AČÎĄ¯āĸ’ôtØŧ †ō^tūJ[’+)G]ĪĀWęũĮb y9¸ļ˜Ņ&÷έ<ãYĖt.¤SQĮËŧîŧ\liŖ^ŽéEŽ×I֎9.ģĶņ$–SÉ?ŊÛ¸ÛĄJĄJĄJéééééégĪžqmsmsmsnÃm8ĸŅŽhm€6@ ÎVgĢŗ…*TĄb=Öc}~8äŋĪtûīį÷„MķÕü¤Kē¤ËšeͲf™~ĶoúķUėƒûƒûƒû}=}=}=ívģCtščrŅåâÆ5Œk¨æĒšjîßų+Č×ĖūÛÁÚŠįž āfėfņŒ.'vRáP‹ôĩtŅÜ Otœs|ĸ+ÛģÛ |ymį˛Ģ }šÖĐPNĮpā Ē`øĖU ā.ķuHœĮNx¸´4ņmä°Kř2Ģ#‹\pĪ÷ö>{é šiŋRŠ ÄI¸ÆSā…œ7ÁúCIKÁtEi~€ 7ĄĀ'¤Õ5ĢŸ0Oįô¡T]ævpQ3žÍ3¨„ũ*GŠW,)Â~1”;ËėÅIK›˙ _&Ō5õ&Ŋ#+ē´"ÛëÔvŽ?[vžx[{×%˙ †ÖĒiÕ´jfÍɚ“5Į“âIņ¤¨ĪĢĪĢĪģ?uęūÔyĘyĘyJWuUWĩQÚ(m”r^9¯œ§ˇč-z C1CĪÍ˙đ÷LBBb`/åĨŧ4?jdy-¯å5Į™ãĖqĄ‡C‡t t tôˇõˇõˇD'Ņ)fHː˜!ŅÉŅÉŅÉjCĩĄÚđ/ũŝ҉ĪÛƒ{Å"ŊŅvÅč–Ž8(|Ĩāīf"ˆBČFQę‰sęĪąaXĒU‹~ŒgãáŽ|‰ŽrŽ|.´†œwēÚ íė!ö š‘û[)r4nŲuØRŲŪĒL\B‰ĩʄk—ĩË8_+ü^͇ûHjŽŌAĢ—Dœ¤ņ)•Ņēh]ôŧīyßķ~Î؜ŗ9gy,åąîîîÎ—/;_v<ęxÔņ¨fkļf+” ĘQR”% r8ŋSdų?øgí:Žãz>ËģŦ,+ËĘvČŲ!sž9ߜoö2{™ŊB-C-C-ķ‚oß ž%^¯ˆWĸ=ŅžhOôžč=Ņ{´gĩgĩg˙Ũ5œÆ Ģu`ąõŗŅ'˜h${¯ 0âĶÖ,oUÉ ^]ÅŌŌâŽqÄ͈RtŸ—Ö…ļŠ¯•üēíˇbf˜iŧõēgy[ŌŠˆüžĸ‘T—†â!4… É3ąÛøIū€÷ąEŊÔŽe$—°Ô1Aį\œįGŦEš9|Eø5Ķ›Ä$Ņ&wšÂ‹ÔÃQĶS4}QėܲqzfôņÔöę䈨Âß CŨá‰b¨ˆЧ—p —Ėíævsģį‚į‚į‚ĮöØ[6“Íd3×j×j×j×T×T×Tũ)ũ)ũ)­ĄÖPk˜KW>Q>Q>ĄQ4ŠFáUŧŠW˙:3Ī˙9Ā?bųā›đ~āk|¯ÉĢōĒŧjī°wØ;ōŖCæsŒ9&ôzčõĐëÁ“Á“Á“ÁÛÁÛÁÛ´šVĶęˆĒU#ĒF>}Ô9Ã9Ã9ƒPjđŋ‰A9[nąŽČÅæ ßiûB°UN}kĸ˙ÃĖËö™`‹ŧĨ2ÚyĻĐQ;õöá]Ė5‚íoäëĶ`WžÂ=Ŧ3xOĻXW¯Å5F\VSh}§–Ļ<õ[WcZât$Øĸ´s|BågÛČĶJkĮøđGÔŅîˇâ^RĘ8ÆöŊtwxMŠOĒī N8˙ŅNđ^ŪË{ƒ‹ƒ‹ƒ‹='='='ķVä­Č[s8‡sŽŽŽÎdg˛3YoĒ7՛jŠZĒ–ĒēT—ęRö)û”}bŊX/Öc/öb/Ęĸ,Ęâ”ũOs€{-šČÅxŒĮx^ÅĢx•Ŋ×Ūkīĩ÷Ųûė}ÖJkĨĩŌŧj^5¯…ÂFáĐÂĐÂĐ ´ƒļŨĶîi÷t.p.p.ˆŦY'˛Nøđ!áCô­úV}ë}`•Å)ÅĪÚØÄCåIėÁ Ū\pÍ­‡ŲÔ-čKQíđ&öŖ*üÄí?nũŅũgŒgŒgŧėe/į=“÷LŪ3!qˆ•%B‰pÆ;ãņŽG8Ņë‹õÅÚíŒvFŊŖŪQīˆcâ˜8–¯DYdĄ-ÚĸíĪķũ˙vøũiF`¯æÕŧÚžl_ļ/Û~ÛoûíŌviģt~žŲČe)YJ–˛‡ŲÃėaör{šŊÜrY.Ëe~i~i~iĨX)VŠŅÜhn4M M M1Ĩ)M™_¯ĒĩĶÚií\1ŽWŒ{“{“{“ķ¤ķ¤ķ¤Ö^k¯ĩWÖ)ë”u˙•Å[÷ŲXŒÅXģŊŨŪnŸīØÁĮƒL L L 4 4 44 ™…ĖB´ŽÖŅ:­ŒVF+ãååĨmĐ6h´-Úm‹ÚLmĻ6S7Ģ›ÕÍJ¤ŠD*=•žJOšKsi.ųÉO~ôC?ô+H_ū/˛˙p¯Ĩ#阌ɘĖÁüŋÁoØévē.ûĘž˛¯}Ö>kŸĩXŦVq̏UÜēf]ŗŽ™-͖fKsŗšŲÜl&›Éf˛}Ä>bÁ-ÜÂ-ůøŋÖJkĨĩrŒqŒqŒŅgč3ôZU­ĒVU̝Õ×ę+u”:JQJ”ĨčYz–žĨtJ§tšGķhŪ} ŋāüÂy äxŽįxžÁ3xFÁ į¤}Ō>iž7Ī›įÍæ ķ†1ؘlLί¨5o›ˇÍÛv;ģŨ]Đ]Ôîjwĩģ*UŠJŊ­ŪVoĢ­ŅÖhkÔ5FÉ?ģĢcÔ1ęÅR,ÅʧģWÄq%ŸS?Ÿŧā učĐ˙ˇN˙íp¯@°ō“đ2[fËlžČyĸŒ—ņ2^~%ŋ’_ŲŨėnv7Ûmģmˇ}Ū>oŸˇÛĮíãÖ k…ĩ"ãf5˛YŦŽVGĢcūŨCŽ“ëä:ŪĮûx 0°[ąUTÕD5ĒJUŠ*u§îԝnŅ-ēUPĨ“N:/æÅŧ˜Ëq9.Įëx¯ã Îā ί&ëÃ}¸O~H1?Č+‰Abō¨ō¨ō¨ēEŨĸnQO̧ÕĶę|už:_éĒtUē*•”JJ%%V‰Ub•sĘ9åœ2]™ŽL DŅ@l[ÅVęJ]Š+ePeāuŧŽ×ķŠnūįžé˙Īūņũa9–c9âA<.¸ā’[åVš•Įņ8'ʆ˛ĄT¤"•|Ápš_î—ûí-ö{KūŋË ō‚ŧ ŊŌ+Ŋ’$IĘ'Č ōĢü*ŋʛy3oæpįpîÉ=š'æb.ædFĮaÆŅzZOëķ]‚zQ/ę%æˆ9bŽ8)NŠ“ųsRDˆ!’E˛HEEQQ4ú”ĒJUĨĒ(!Jˆ"M¤‰4ú~ ō×x*G娝ĸSt oâMŧ‰†hˆ†˙;Nķ˙į˙ ËéoÅVlÅTLÅTîÅŊ¸—å˛\6Ŧōïŧˇņ6^Ãkx\"—Č%ŧ‚Wđ ŪĀxŸâS|Š}ėc'q'q nÁ-x á1ĸÍ7p7 psĩQĩņ,žÅŗ´€ĐÚMģi7…(D!J J šT“jR[jKmE7ŅMt+XŋÛPjS ‡Oņ_pjߍŨ؝š/8ÁWFeTūŸĄ˙¯ŠüŸũšŲlŗÍˇøßâŸųgū™‡đÂÄÄ$ߖoˡí;ĮÎąûØ}ė>Ök‡ĩ#ŋpĀ\f.3—™5͚fMãˆqÄ8’_Äai@‡ ¨€ ”B)”‚R(…R¨ŠĒ¨Š*¨‚*Ŧ8%P%P…Pč_LæŋŪūļ,ÂŖÜ#ũD%tEXtdate:create2018-10-06T17:06:46+00:00˙4‡Ä%tEXtdate:modify2018-10-06T17:06:46+00:00Ži?xGtEXtsvg:base-urifile:///home/andrew/projects/aiohttp/docs/aiohttp-icon.svg!hËīIENDŽB`‚aiohttp-session-2.12.1/docs/aiohttp-icon.ico000066400000000000000000000062761467501771200207750ustar00rootroot00000000000000 ¨ ( @˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ũũũųųųôôôņņņđđđđđđņņņõõõųųųũũũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙úúúņņņîííđīîķōņôôķôôôôôôôôķôōņņđīīîîķōōúúú˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ûûûđđđīîíõôôëķøÖėųŽŨøĄÕ÷‰Î÷‚Ë÷™Ķ÷ĻŲøËįųîôųõôôđīīōōōûûû˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūūūöööîííõõôāīųĒŲ÷vÃösÅøIĩųG°úXģûGļûS¸úAŗų\ŧø†É÷ĩŨøáīųöõõđīī÷÷÷˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūūūķķķđīīîõųžÖ÷Uš÷GļúEļû^ŋüJ´ûNĩû_ŊülÂüJ˛ûG˛û[ŊûVŧû_ŋûUš÷žÖ÷íõųōņđõõõūūū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūūūķķķōņđäņú†Éö>˛ų:ŗûE¸ûBļûT¸üD¯ûUšûgÁũoÃũfŧúšŌųÂåûš×úUžû9ŗû8ŽųyÁöáīúôķōõõõ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙öööņņđãņúxÄöPˇúIļû@´ûAĩûL¸ûXšûA¯ûU¸üoÄūlÂũŽÛúūūū˙˙˙ûũūĶû>´û@°û@Žúoŋöãņúôķō÷÷÷˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙úúúđīīņöúŒË÷MļúH¸ûCąû8Žû4¯ûTēûaĀûHˇûVšûkÂūoÃũŅëü˙˙˙˙˙˙˙˙˙ĩáũBŗûN¸ûH¸ûNļú•Ī÷ņ÷úōōņûûû˙˙˙˙˙˙˙˙˙ūūūņņņôö÷ŦØ÷vÂųZ¸úH´ûAŦû>Ŧû?ąû[ēûfĀüLļûSļû]ģü\ēü˛Ũû˙˙˙˙˙˙ū˙˙ĄŲüOļûRļûLˇûRļûpÁúŠ×÷õ÷øôôô˙˙˙˙˙˙˙˙˙ųųųķōōĪéų‡ĘõŪđüĮæü[¸úNŗûSļûUļüIŽûoŋúNĩú7ŽûCŗûVģübŋû§ÚüÕíūŠÚũuÄũfŧüSŗûJ˛û;°ûYŧüQļ÷Čæųöôôúúú˙˙˙˙˙˙ķķķķöøČö­Ũú˙˙˙ũū˙ÆûKąûaŧũhŋūÆúãņüĘčü_ŋû_ŋüTģû5­ûH¯ûaēûX¸ükĀū[¸ũ]¸ümÁüSšû^ŋüB´ú{Æöôöøõõõ˙˙˙üüüķōōŲėųJŗ÷kÃûŲīūËęūgŋü_ģü~Čū‚ĘūĮæü˙˙˙ûũ˙€Ęü=ąû<ąû<ŽûC¯ûSļü]ēũoÂūgžũN´ûR¸ûfĀüoÅüVŧü\ē÷ŲėųöõôũũũøøøöõôēŨøD­ú0ĢûV¸ûgŋü2ŦûJ˛ûfŧũoÁū‡ĘüØîũÅæũOąû2Ēû9ŽûKŗû?ąûYēüYļülĀūaēũO´ûE´ûVļûGąû>ąû^ēú¸Ũ÷ø÷öúúúõõôõö÷ŽČöY´û>ŦûN˛ûhŊüB¯ûLąûQŗûXˇüN˛ûVļûeŧüHŦû>ĢûG°ûO˛ûHąûcŧüO˛ûXˇüQŗûM˛ûHąû_ˇüDŦûAŽûK°ûŠČö÷øø÷÷÷ôķķđõųtŋöjžüdģũH­ü^ļüI°û6Ŧû7­û>°û4Ŧû5ĒûQ˛ûiŊüZˇüPąü>§ûIŽû\ģü7­û>°û6­û3ĒûJ¯ûgŧüUĩûYļũ=§ûjšöņöųöööôķķéōų`ĩöbēü€Čū[ļũdšüRŗû@Žû<¯ûDŗû7­û8§û>§ûgŧüvÃūcēūGŦûOąûcģûHŗúKĩúCąú8ŠûWŗüT˛ûaēũkžūLŽüf¸öėôúööõôôķčōų_ˇ÷\ˇũ‚ČūsÁūeŊüI˛ûD¯û8Ģû6Ŧû?ŽûCŽûAŽûU´üƒČūsÁūS´üNŗúnŋ÷ƒÆô‚Æķ}ÄôpžöhŊúJ°ûfģũlŋū\ˇũdē÷ęķú÷ööõôôėôųkŧöU˛ünžūŒĘûuÂúD°û=¨û5Ļû3ĒûL°ûK˛ûH˛ûRąûhŧũ}Åũa¸úlŊöĄĐōËâķËāôÍâôÂßķ‰Éõ[ļų]ļühŧūU˛ükŧöíõú÷÷ööööķ÷ųvŋöDŦûOąûŧāûĮåüHŽûBŠûBŠûDŦûI­ûI­ûFŽûF­ûK°üRŗûtĀøĻŌōĪāô§Ē퀁ōķËŌ÷ŧŨôvŋöM°ûNąüDŦûvŋöô÷úøøøøøøųøø‡Éö.§ú>ŽûvÅüzÆüHŦûLŽûW´ü\ĩũ>ĨûC¨ûD­û0¨û9Ŧû<­úcēõÃßķ˛ĩôbbō``ō_^ō‚ƒķŅá÷‰ÉôB¯ú=Žû.§ú‡ÉöúųųúúúüüüųøöēŪø;ĒøNĩûRļü:¨ûZ˛üIŦûa¸ũjŧūP¯üJ­ûM°û;Ģû>ŽûD°úpŊõĮāô•˜ô__ōbbōaaōlmōÍ×øŅôMŗų@¯û:Ēø¸Ũøûųøũũũūūū÷÷öįņúW˛öO´üB­û9¨û=Ŧû]ˇüešũjģū[´üG¯ûE¯û=¨û2§û7ĒûoŧöŧŪô¯´öbbō``ō_^ō‚ôÖäøĸŌõ]¸úM´ürž÷éķúųųø˙˙˙˙˙˙úúųųųųĻĶ÷HŽúDŦûI­û4§ûZ´ûržũd¸ũS¯ûJŽû?Ģû9ĸû2¤û=ĒûdˇøžĐķÕä÷§ŠöôŽõĘŌøžßõrģ÷B¨û;ŠúĨĶ÷ûúúûûû˙˙˙˙˙˙ūūūø÷÷įņúgļöB§ûC§ûAŠû=¨ûQ˛ü`¸ü@Ēû=¨ûA§ûD¨ûFŠûEŠûQ­û„Äø¨ÔôĮâöĐâøÎã÷Áāõ‰ÅöTŽúD¨ûgļöåņúúųøūūū˙˙˙˙˙˙˙˙˙úúúúųųĮâųNŠø<ĸûIĢû3Ļû3§ûP´ûOŗü5¤ûG§úT¯ú_´ũdļũ[°üYąû]ĩørŋöŒÉõÄögēøgļûO­û^˛øĮâųûúúüûû˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙øøøųúú¨Ķ÷JĒøK­ûAĒû9Šû<ĢûEŽûxŋųËåûÆäü‹ÉũxĀūN­üJŦûIŦû?ĢûG¯úB­ú:ĨûHĒûbĩųŽÖ÷ųúûúúú˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūūūųøøöųûžĐöH­ø;Ļû0ĸû,Ŗû?¨ûĒ×û˙˙˙ū˙˙ŸŌũbĩūOŽüBŦû?Šû5ĸû,Ŗû8§ûDĢûFŦøĨĶ÷øúûúųųūūū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ũũũųųø÷ųûąŲ÷SĒöAĨúM¯üeˇü‰Įûßđū×ėūˆĮũ_ŗüLĒûEĒû:Ŗû7Ÿû1ĸûC¨ú`˛ö˛Ú÷÷úûúúųūūū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūūūųųųûúúŪíúŸÎöb˛ø?ĸú>ĸûNŽûAĒûCĢûY´üO°ü>ĻûA¤ûEĻúX­øŠÃöÛėúüûûúúúūūū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ûûûûúųøúûØęú˜É÷pˇöXŽ÷7¤ø=¨ø?¨øHĢøjĩ÷}ŊöŖĐ÷Ûëúųúûüûúüüü˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūūūûûûûúúüûûôøûäđúĪįųĮãųĮãųÍæųâīúöųûüûûüûúûûûūūū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūūūüüüûûúûúúüûųũûúũûúũûúüûúûûûüüüūūū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ā˙˙€˙ūø?đāāĀ€€€€€Āāāđøū˙˙˙Ā˙aiohttp-session-2.12.1/docs/conf.py000066400000000000000000000255641467501771200172030ustar00rootroot00000000000000#!/usr/bin/env python3 # # aiohttp_session documentation build configuration file, created by # sphinx-quickstart on Wed Apr 1 21:54:09 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import codecs import os import re import sys _docs_path = os.path.dirname(__file__) _version_path = os.path.abspath( os.path.join(_docs_path, "..", "aiohttp_session", "__init__.py") ) with codecs.open(_version_path, "r", "latin1") as fp: try: _version_info = re.search( r'^__version__ = "' r"(?P\d+)" r"\.(?P\d+)" r"\.(?P\d+)" r'(?P.*)?"$', fp.read(), re.M, ).groupdict() except IndexError: raise RuntimeError("Unable to determine version.") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "alabaster", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "aiohttp_session" copyright = "2015-2018 Andrew Svetlov and aio-libs team" author = "Andrew Svetlov and aio-libs team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = "{major}.{minor}".format(**_version_info) # The full version, including alpha/beta/rc tags. release = "{major}.{minor}.{patch}-{tag}".format(**_version_info) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { "logo": "aiohttp-icon-128x128.png", "description": "user session support for aiohttp.web", "github_user": "aio-libs", "github_repo": "aiohttp-session", "github_button": True, "github_type": "star", "github_banner": True, "travis_button": True, "codecov_button": True, "pre_bg": "#FFF6E5", "note_bg": "#E5ECD1", "note_border": "#BFCF8C", "body_text": "#482C0A", "sidebar_text": "#49443E", "sidebar_header": "#4B4032", } # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = "aiohttp-icon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { "**": [ "about.html", "navigation.html", "searchbox.html", ] } # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = "aiohttp_sessiondoc" # -- Options for LaTeX output --------------------------------------------- # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', # Latex figure (float) alignment # 'figure_align': 'htbp', latex_elements = {} # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, "aiohttp_session.tex", "aiohttp\\_session Documentation", "Andrew Svetlov", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, "aiohttp_session", "aiohttp_session Documentation", [author], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "aiohttp_session", "aiohttp_session Documentation", author, "aiohttp_session", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "https://docs.python.org/3": None, "https://aiohttp.readthedocs.io/en/stable": None, "https://redis.readthedocs.io/en/latest": None, # 'https://github.com/aio-libs/aiomcache': None, "http://cryptography.io/en/latest": None, "https://pynacl.readthedocs.io/en/latest": None, } aiohttp-session-2.12.1/docs/glossary.rst000066400000000000000000000023551467501771200202720ustar00rootroot00000000000000.. _aiohttp-session-glossary: ========== Glossary ========== .. if you add new entries, keep the alphabetical sorting! .. glossary:: aioredis :term:`asyncio` compatible module in the :term:`redis` library. https://redis-py.readthedocs.io/en/stable/examples/asyncio_examples.html aiomcache :term:`asyncio` compatible Memcached client library https://github.com/aio-libs/aiomcache asyncio The library for writing single-threaded concurrent code using coroutines, multiplexing I/O access over sockets and other resources, running network clients and servers, and other related primitives. Reference implementation of :pep:`3156` https://docs.python.org/3/library/asyncio.html cryptography The libary used for encrypting secure cookied session https://cryptography.io pynacl Yet another libary used for encrypting secure cookied session https://pynacl.readthedocs.io session A namespace that is valid for some period of continual activity that can be used to represent a user's interaction with a web application. sqlalchemy The Python SQL Toolkit and Object Relational Mapper. http://www.sqlalchemy.org/ aiohttp-session-2.12.1/docs/index.rst000066400000000000000000000075271467501771200175440ustar00rootroot00000000000000.. aiohttp_session documentation master file, created by sphinx-quickstart on Wed Apr 1 21:54:09 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. aiohttp_session =============== .. currentmodule:: aiohttp_session .. highlight:: python The library provides sessions for :ref:`aiohttp.web`. The current version is |version| Usage ----- The library allows to store user-specific data into session object. The session object has dict-like interface (operations like ``session[key] = value`` or ``value = session[key]`` etc. are supported). Before processing session in web-handler you have to register *session middleware* in :class:`aiohttp.web.Application`. A trivial usage example: .. code-block:: python import time from aiohttp import web from aiohttp_session import get_session, setup from aiohttp_session.cookie_storage import EncryptedCookieStorage async def handler(request): session = await get_session(request) session['last_visit'] = time.time() return web.Response(text='OK') def init(): app = web.Application() setup(app, EncryptedCookieStorage(b'Thirty two length bytes key.')) app.router.add_route('GET', '/', handler) return app web.run_app(init()) All storages uses HTTP Cookie named ``AIOHTTP_COOKIE_SESSION`` for storing data. Available session storages are: * :class:`SimpleCookieStorage` -- keeps session data as plain JSON string in cookie body. Use the storage only for testing purposes, it's very non-secure. * :class:`~aiohttp_session.cookie_storage.EncryptedCookieStorage` -- stores session data into cookies like :class:`SimpleCookieStorage` does but encodes the data via :term:`cryptography` Fernet cipher. For key generation use :meth:`cryptography.fernet.Fernet.generate_key` method. Requires :term:`cryptography` library: .. code-block:: bash $ pip3 install aiohttp_session[secure] * :class:`~aiohttp_session.redis_storage.RedisStorage` -- stores JSON-ed data into *redis*, keeping into cookie only redis key (random UUID). Inside redis the key will be saved as COOKIENAME_VALUEOFTHECOOKIE. For example if inside the browser the cookie is saved with name ``'AIOHTTP_SESSION'`` (default option) and value ``e33b57c7ec6e425eb626610f811ab6ae`` (a random UUID) they key inside redis will be ``AIOHTTP_SESSION_e33b57c7ec6e425eb626610f811ab6ae``. Requires :term:`redis` library: .. code-block:: bash $ pip install aiohttp_session[aioredis] * :class:`~aiohttp_session.memcached_storage.MemcachedStorage` -- the same as Redis storage but uses Memcached database. Requires :term:`aiomcache` library: .. code-block:: bash $ pip install aiohttp_session[aiomcache] Installation -------------------- .. code-block:: bash $ pip3 install aiohttp_session Source code ----------- The project is hosted on GitHub_ .. _GitHub: https://github.com/aio-libs/aiohttp_session Please feel free to file an issue on `bug tracker `_ if you have found a bug or have some suggestion for library improvement. The library uses `Travis `_ for Continuous Integration. Dependencies ------------ - :term:`cryptography` for :class:`~aiohttp_session.cookie_storage.EncryptedCookieStorage` - :term:`redis` for :class:`~aiohttp_session.redis_storage.RedisStorage` Third party extensions ---------------------- * `aiohttp_session_mongo `_ License ------- ``aiohttp_session`` is offered under the Apache 2 license. Contents: .. toctree:: :maxdepth: 2 reference glossary Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` aiohttp-session-2.12.1/docs/make.bat000066400000000000000000000161361467501771200173040ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\aiohttp_session.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\aiohttp_session.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end aiohttp-session-2.12.1/docs/reference.rst000066400000000000000000000334451467501771200203710ustar00rootroot00000000000000.. _aiohttp-session-reference: =========== Reference =========== .. module:: aiohttp_session .. currentmodule:: aiohttp_session .. highlight:: python Public functions ================ .. function:: get_session(request) A :ref:`coroutine` for getting session instance from request object. See example below in :ref:`Session` section for :func:`get_session` usage. .. function:: new_session(request) A :ref:`coroutine` for getting a new session regardless of whether a cookie exists. .. warning:: Always use :func:`new_session` instead of :func:`get_session` in your login views to guard against Session Fixation attacks! Example usage:: from aiohttp_session import new_session async def handler(request): session = await new_session(request) session.new == True # This will always be True .. function:: session_middleware(storage) Session middleware factory. Create session middleware to pass into :class:`aiohttp.web.Application` constructor. *storage* is a session storage instance (object used to store session data into cookies, Redis, database etc., class is derived from :class:`AbstractStorage`). .. seealso:: :ref:`aiohttp-session-storage` .. note:: :func:`setup` is new-fashion way for library setup. .. function:: setup(app, storage) Setup session support for given *app*. The function is shortcut for:: app.middlewares.append(session_middleware(storage)) *app* is :class:`aiohttp.web.Application` instance. *storage* is a session storage instance (object used to store session data into cookies, Redis, database etc., class is derived from :class:`AbstractStorage`). .. seealso:: :ref:`aiohttp-session-storage` .. _aiohttp-session-session: Session ======= .. class:: Session Client's session, a namespace that is valid for some period of continual activity that can be used to represent a user's interaction with a web application. .. warning:: Never create :class:`Session` instances by hands, retieve those by :func:`get_session` call. The :class:`Session` is a :class:`MutableMapping`, thus it supports all dictionary methods, along with some extra attributes and methods:: from aiohttp_session import get_session async def handler(request): session = await get_session(request) session['key1'] = 'value 1' assert 'key2' in session assert session['key2'] == 'value 2' # ... .. attribute:: created Creation UNIX TIMESTAMP, the value returned by :func:`time.time` for very first access to the session object. .. attribute:: identity Client's identity. It may be cookie name or database key. Read-only property. For change use :func:`Session.set_new_identity`. .. attribute:: new A boolean. If new is ``True``, this session is new. Otherwise, it has been constituted from data that was already serialized. .. method:: changed() Call this when you mutate a mutable value in the session namespace. See the note below for details on when, and why you should call this. .. note:: Keys and values of session data must be JSON serializable when using one of the included storage backends. This means, typically, that they are instances of basic types of objects, such as strings, lists, dictionaries, tuples, integers, etc. If you place an object in a session data key or value that is not JSON serializable, an error will be raised when the session is serialized. If you place a mutable value (for example, a list or a dictionary) in a session object, and you subsequently mutate that value, you must call the changed() method of the session object. In this case, the session has no way to know that is was modified. However, when you modify a session object directly, such as setting a value (i.e., ``__setitem__``), or removing a key (e.g., ``del`` or ``pop``), the session will automatically know that it needs to re-serialize its data, thus calling :meth:`changed` is unnecessary. There is no harm in calling :meth:`changed` in either case, so when in doubt, call it after you've changed sessioning data. .. method:: invalidate() Call this when you want to invalidate the session (dump all data, and -- perhaps -- set a clearing cookie). .. method:: set_new_identity(identity) Call this when you want to change the :py:attr:`identity`. .. warning:: Never change :py:attr:`identity` of a session which is not new. .. _aiohttp-session-storage: Session storages ================ :mod:`aiohttp_session` uses storages to save/load persistend session data. Abstract Storage ---------------- All storages should be derived from :class:`AbstractStorage` and implement both :meth:`~AbstractStorage.load_session` and :meth:`~AbstractStorage.save_session` methods. .. class:: AbstractStorage(cookie_name="AIOHTTP_SESSION", *, \ domain=None, max_age=None, path='/', \ secure=None, httponly=True, \ encoder=json.dumps, decoder=json.loads) Base class for session storage implementations. It uses HTTP cookie for storing at least the key for session data, but some implementations may save all session info into cookies. *cookie_name* -- name of cookie used for saving session data. *domain* -- cookie's domain, :class:`str` or ``None``. *max_age* -- cookie's max age, :class:`int` or ``None``. *path* -- cookie's path, :class:`str` or ``None``. *secure* -- cookie's secure flag, :class:`bool` or ``None`` (the same as ``False``). *httponly* -- cookie's http-only flag, :class:`bool` or ``None`` (the same as ``False``). *encoder* -- session serializer. A callable with the following signature: `def encode(param: Any) -> str: ...`. Default is :func:`json.dumps`. *decoder* -- session deserializer. A callable with the following signature: `def decode(param: str) -> Any: ...`. Default is :func:`json.loads`. .. versionadded:: 2.3 Added *encoder* and *decoder* parameters. .. attribute:: max_age Maximum age for session data, :class:`int` seconds or ``None`` for "session cookie" which last until you close your browser. .. attribute:: cookie_name Name of cookie used for saving session data. .. attribute:: cookie_params :class:`dict` of cookie params: *domain*, *max_age*, *path*, *secure* and *httponly*. .. attribute:: encoder The JSON serializer that will be used to dump session cookie data. .. versionadded:: 2.3 .. attribute:: decoder The JSON deserializer that will be used to load session cookie data. .. versionadded:: 2.3 .. method:: new_session() A :ref:`coroutine` for getting a new session regardless of whether a cookie exists. Return :class:`Session` instance. .. method:: load_session(request) An *abstract* :ref:`coroutine`, called by internal machinery for retrieving :class:`Session` object for given *request* (:class:`aiohttp.web.Request` instance). Return :class:`Session` instance. .. method:: save_session(request, response, session) An *abstract* :ref:`coroutine`, called by internal machinery for storing *session* (:class:`Session`) instance for given *request* (:class:`aiohttp.web.Request`) using *response* (:class:`aiohttp.web.StreamResponse` or descendants). .. method:: load_cookie(request) A helper for loading cookie (:class:`http.cookies.SimpleCookie` instance) from *request* (:class:`aiohttp.web.Request`). .. method:: save_cookie(response, cookie_data, *, max_age=None) A helper for saving *cookie_data* (:class:`str`) into *response* (:class:`aiohttp.web.StreamResponse` or descendants). *max_age* is cookie lifetime given from session. Storage default is used if the value is ``None``. Simple Storage -------------- For testing purposes there is :class:`SimpleCookieStorage`. It stores session data as unencrypted and unsigned JSON data in browser cookies, so it's totally insecure. .. warning:: Never use this storage on production!!! It's highly insecure!!! To use the storage you should push it into :func:`session_middleware`:: aiohttp_session.setup(app, aiohttp_session.SimpleCookieStorage()) .. class:: SimpleCookieStorage(*, \ cookie_name="AIOHTTP_SESSION", \ domain=None, max_age=None, path='/', \ secure=None, httponly=True, \ encoder=json.dumps, decoder=json.loads) Create unencrypted cookie storage. The class is inherited from :class:`AbstractStorage`. Parameters are the same as for :class:`AbstractStorage` constructor. .. module:: aiohttp_session.cookie_storage .. currentmodule:: aiohttp_session.cookie_storage Cookie Storage -------------- The storage that saves session data in HTTP cookies as :class:`~cryptography.fernet.Fernet` encrypted data. To use the storage you should push it into :func:`~aiohttp_session.session_middleware`:: app = aiohttp.web.Application(middlewares=[ aiohttp_session.cookie_storage.EncryptedCookieStorage( b'Thirty two length bytes key.')]) .. class:: EncryptedCookieStorage(secret_key, *, \ cookie_name="AIOHTTP_SESSION", \ domain=None, max_age=None, path='/', \ secure=None, httponly=True, \ encoder=json.dumps, decoder=json.loads) Create encryted cookies storage. The class is inherited from :class:`~aiohttp_session.AbstractStorage`. *secret_key* is :class:`bytes` secret key with length of 32, used for encoding or base-64 encoded :class:`str` one. Other parameters are the same as for :class:`~aiohttp_session.AbstractStorage` constructor. .. note:: For key generation use :meth:`cryptography.fernet.Fernet.generate_key` method. .. module:: aiohttp_session.cookie_storage .. currentmodule:: aiohttp_session.cookie_storage NaCl Storage -------------- The storage that saves session data in HTTP cookies as :class:`~nacl.secret.SecretBox` encrypted data. To use the storage you should push it into :func:`~aiohttp_session.session_middleware`:: app = aiohttp.web.Application(middlewares=[ aiohttp_session.nacl_storage.NaClCookieStorage( b'Thirty two length bytes key.']) .. class:: NaClCookieStorage(secret_key, *, \ cookie_name="AIOHTTP_SESSION", \ domain=None, max_age=None, path='/', \ secure=None, httponly=True, \ encoder=json.dumps, decoder=json.loads) Create encryted cookies storage. The class is inherited from :class:`~aiohttp_session.AbstractStorage`. *secret_key* is :class:`bytes` secret key with length of 32, used for encoding. Other parameters are the same as for :class:`~aiohttp_session.AbstractStorage` constructor. .. module:: aiohttp_session.redis_storage .. currentmodule:: aiohttp_session.redis_storage Redis Storage ------------- The storage that stores session data in Redis database and keeps only Redis keys (UUIDs actually) in HTTP cookies. It operates with Redis database via :class:`redis.asyncio.Redis`. To use the storage you need setup it first:: redis = await aioredis.from_url("redis://127.0.0.1:6379") storage = aiohttp_session.redis_storage.RedisStorage(redis) aiohttp_session.setup(app, storage) .. class:: RedisStorage(redis_pool, *, \ cookie_name="AIOHTTP_SESSION", \ domain=None, max_age=None, path='/', \ secure=None, httponly=True, \ key_factory=lambda: uuid.uuid4().hex, \ encoder=json.dumps, decoder=json.loads) Create Redis storage for user session data. The class is inherited from :class:`~aiohttp_session.AbstractStorage`. *redis_pool* is a :class:`~redis.asyncio.Redis` which should be created by :func:`~redis.asyncio.from_url` call, e.g.:: redis = await aioredis.from_url("redis://localhost:6379") storage = aiohttp_session.redis_storage.RedisStorage(redis) Other parameters are the same as for :class:`~aiohttp_session.AbstractStorage` constructor. Memcached Storage ----------------- The storage that stores session data in Memcached and keeps only keys (UUIDs actually) in HTTP cookies. It operates with Memcached database via :class:`aiomcache.Client`. To use the storage you need setup it first:: mc = aiomcache.Client('localhost', 11211) storage = aiohttp_session.memcached_storage.Client(mc) aiohttp_session.setup(app, storage) .. versionadded:: 1.2 .. class:: MemcachedStorage(memcached_conn, *, \ cookie_name="AIOHTTP_SESSION", \ domain=None, max_age=None, path='/', \ secure=None, httponly=True, \ key_factory=lambda: uuid.uuid4().hex, \ encoder=json.dumps, decoder=json.loads) Create Memcached storage for user session data. The class is inherited from :class:`~aiohttp_session.AbstractStorage`. *memcached_conn* is a :class:`~aiomcache.Client` instance:: mc = await aiomcache.Client('localhost', 6379) storage = aiohttp_session.memcached_storage.MemcachedStorage(mc) Other parameters are the same as for :class:`~aiohttp_session.AbstractStorage` constructor. aiohttp-session-2.12.1/examples/000077500000000000000000000000001467501771200165565ustar00rootroot00000000000000aiohttp-session-2.12.1/examples/postgres_storage.py000066400000000000000000000063031467501771200225240ustar00rootroot00000000000000import json import uuid from typing import Any, Callable, Optional import psycopg2.extras from aiohttp import web from aiopg import Pool from aiohttp_session import AbstractStorage, Session class PgStorage(AbstractStorage): """PG storage""" def __init__( # type: ignore[no-any-unimported] self, pg_pool: Pool, *, cookie_name: str = "AIOHTTP_SESSION", domain: Optional[str] = None, max_age: Optional[int] = None, path: str = "/", secure: Optional[bool] = None, httponly: bool = True, key_factory: Callable[[], str] = lambda: uuid.uuid4().hex, encoder: Callable[[object], str] = psycopg2.extras.Json, decoder: Callable[[str], Any] = json.loads, ): super().__init__( cookie_name=cookie_name, domain=domain, max_age=max_age, path=path, secure=secure, httponly=httponly, encoder=encoder, decoder=decoder, ) self._pg = pg_pool self._key_factory = key_factory async def load_session(self, request: web.Request) -> Session: cookie = self.load_cookie(request) if cookie is None: return Session(None, data={}, new=True, max_age=self.max_age) else: async with self._pg.acquire() as conn: key = uuid.UUID(cookie) async with conn.cursor( cursor_factory=psycopg2.extras.DictCursor ) as cur: await cur.execute( "SELECT session, extract(epoch from created) " + "FROM web.sessions WHERE uuid = %s", (key,), ) data = await cur.fetchone() if not data: return Session(None, data={}, new=True, max_age=self.max_age) return Session(key, data=data, new=False, max_age=self.max_age) async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: key = session.identity if key is None: key = self._key_factory() self.save_cookie(response, key, max_age=session.max_age) else: if session.empty: self.save_cookie(response, "", max_age=session.max_age) else: key = str(key) self.save_cookie(response, key, max_age=session.max_age) data = self._get_session_data(session) if not data: return data_encoded = self._encoder(data["session"]) expire = data["created"] + (session.max_age or 0) async with self._pg.acquire() as conn: async with conn.cursor() as cur: await cur.execute( "INSERT INTO web.sessions (uuid,session,created,expire)" + " VALUES (%s, %s, to_timestamp(%s),to_timestamp(%s))" + " ON CONFLICT (uuid)" + " DO UPDATE" + " SET (session,expire)=(EXCLUDED.session, EXCLUDED.expire)", [key, data_encoded, data["created"], expire], ) aiohttp-session-2.12.1/pytest.ini000066400000000000000000000006141467501771200167720ustar00rootroot00000000000000[pytest] addopts = # show 10 slowest invocations: --durations=10 # a bit of verbosity doesn't hurt: -v # report all the things == -rxXs: -ra # show values of the local vars in errors: --showlocals # coverage reports --cov=aiohttp_session/ --cov=tests/ --cov-report term asyncio_mode = auto filterwarnings = error testpaths = tests/ xfail_strict = true aiohttp-session-2.12.1/requirements-dev.txt000066400000000000000000000003331467501771200207770ustar00rootroot00000000000000-r requirements.txt flake8==6.1.0 flake8-bandit==4.1.1 flake8-bugbear==24.8.19 flake8-import-order==0.18.2 flake8-requirements==2.0.1 mypy==1.7.1 pep257==0.7.0 pre-commit==3.8.0 sphinx==7.2.6 typing-extensions==4.12.2 aiohttp-session-2.12.1/requirements.txt000066400000000000000000000003401467501771200202210ustar00rootroot00000000000000-e . aiohttp==3.10.6 aiomcache==0.8.1 cryptography==43.0.1 docker==7.1.0 multidict==6.0.4 pynacl==1.5.0 pytest==7.4.3 pytest-aiohttp==1.0.5 pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-sugar==0.9.7 redis==5.0.1 yarl==1.12.1 aiohttp-session-2.12.1/setup.py000066400000000000000000000034531467501771200164570ustar00rootroot00000000000000import os import re from setuptools import setup with open( os.path.join( os.path.abspath(os.path.dirname(__file__)), "aiohttp_session", "__init__.py" ), encoding="latin1", ) as fp: try: version = re.findall(r'^__version__ = "([^"]+)"$', fp.read(), re.M)[0] except IndexError: raise RuntimeError("Unable to determine version.") def read(f): return open(os.path.join(os.path.dirname(__file__), f)).read().strip() install_requires = ["aiohttp>=3.10"] extras_require = { "aioredis": ["redis>=4.3.1"], "aiomcache": ["aiomcache>=0.5.2"], "pycrypto": ["cryptography"], "secure": ["cryptography"], "pynacl": ["pynacl"], } setup( name="aiohttp-session", version=version, description=("sessions for aiohttp.web"), long_description="\n\n".join((read("README.rst"), read("CHANGES.txt"))), long_description_content_type="text/x-rst", classifiers=[ "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Framework :: AsyncIO", "Framework :: aiohttp", ], author="Andrew Svetlov", author_email="andrew.svetlov@gmail.com", url="https://github.com/aio-libs/aiohttp_session/", license="Apache 2", packages=["aiohttp_session"], install_requires=install_requires, include_package_data=True, extras_require=extras_require, ) aiohttp-session-2.12.1/tests/000077500000000000000000000000001467501771200161025ustar00rootroot00000000000000aiohttp-session-2.12.1/tests/__init__.py000066400000000000000000000000001467501771200202010ustar00rootroot00000000000000aiohttp-session-2.12.1/tests/conftest.py000066400000000000000000000115431467501771200203050ustar00rootroot00000000000000from __future__ import annotations import asyncio import gc import socket import sys import time import uuid from typing import AsyncIterator, Iterator, TypedDict import aiomcache import pytest from docker import DockerClient, from_env as docker_from_env, models as docker_models from redis import asyncio as aioredis class _ContainerInfo(TypedDict): host: str port: int container: docker_models.containers.Container class _MemcachedParams(TypedDict): host: str port: int def unused_port() -> int: # pragma: no cover """Only used for people testing on OS X.""" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("", 0)) s.listen(1) port: int = s.getsockname()[1] s.close() return port @pytest.fixture(scope="session") def session_id() -> str: """Unique session identifier, random string.""" return str(uuid.uuid4()) @pytest.fixture(scope="session") def docker() -> DockerClient: # type: ignore[misc] # No docker types. client = docker_from_env(version="auto") yield client client.close() @pytest.fixture(scope="session") async def redis_server( # type: ignore[misc] # No docker types. docker: DockerClient, session_id: str, ) -> Iterator[_ContainerInfo]: image = "redis:{}".format("latest") if sys.platform.startswith("darwin"): # pragma: no cover port = unused_port() else: port = None container = docker.containers.run( image=image, detach=True, name="redis-test-server-{}-{}".format("latest", session_id), ports={ "6379/tcp": port, }, environment={ "http.host": "0.0.0.0", "transport.host": "127.0.0.1", }, ) if sys.platform.startswith("darwin"): # pragma: no cover host = "0.0.0.0" else: inspection = docker.api.inspect_container(container.id) host = inspection["NetworkSettings"]["IPAddress"] port = 6379 delay = 0.1 for _i in range(20): # pragma: no cover try: conn = aioredis.from_url(f"redis://{host}:{port}") # type: ignore[no-untyped-call] await conn.set("foo", "bar") break except aioredis.ConnectionError: time.sleep(delay) delay *= 2 finally: await conn.aclose() else: # pragma: no cover pytest.fail("Cannot start redis server") yield {"host": host, "port": port, "container": container} container.kill(signal=9) container.remove(force=True) @pytest.fixture def redis_url(redis_server: _ContainerInfo) -> str: # type: ignore[misc] return "redis://{}:{}".format(redis_server["host"], redis_server["port"]) @pytest.fixture async def redis( redis_url: str, ) -> AsyncIterator[aioredis.Redis]: async def start(pool: aioredis.ConnectionPool) -> aioredis.Redis: return aioredis.Redis(connection_pool=pool) pool = aioredis.ConnectionPool.from_url(redis_url) redis = await start(pool) yield redis await redis.aclose() await pool.disconnect() @pytest.fixture(scope="session") async def memcached_server( # type: ignore[misc] # No docker types. docker: DockerClient, session_id: str, ) -> AsyncIterator[_ContainerInfo]: image = "memcached:{}".format("latest") if sys.platform.startswith("darwin"): # pragma: no cover port = unused_port() else: port = None container = docker.containers.run( image=image, detach=True, name="memcached-test-server-{}-{}".format("latest", session_id), ports={ "11211/tcp": port, }, environment={ "http.host": "0.0.0.0", "transport.host": "127.0.0.1", }, ) if sys.platform.startswith("darwin"): # pragma: no cover host = "0.0.0.0" else: inspection = docker.api.inspect_container(container.id) host = inspection["NetworkSettings"]["IPAddress"] port = 11211 delay = 0.1 for _i in range(20): # pragma: no cover try: conn = aiomcache.Client(host, port) await conn.set(b"foo", b"bar") break except ConnectionRefusedError: time.sleep(delay) delay *= 2 finally: await conn.close() else: # pragma: no cover pytest.fail("Cannot start memcached server") yield {"host": host, "port": port, "container": container} container.kill(signal=9) container.remove(force=True) @pytest.fixture def memcached_params( # type: ignore[misc] memcached_server: _ContainerInfo, ) -> _MemcachedParams: return dict(host=memcached_server["host"], port=memcached_server["port"]) @pytest.fixture async def memcached(memcached_params: _MemcachedParams) -> AsyncIterator[aiomcache.Client]: conn = aiomcache.Client(**memcached_params) yield conn await conn.close() aiohttp-session-2.12.1/tests/test_abstract_storage.py000066400000000000000000000063621467501771200230510ustar00rootroot00000000000000import json import time from functools import partial from typing import Any, Dict, Optional from unittest import mock from aiohttp import web from aiohttp.test_utils import TestClient from aiohttp.typedefs import Handler from aiohttp_session import SimpleCookieStorage, get_session, setup as setup_middleware from .typedefs import AiohttpClient def make_cookie(client: TestClient, data: Dict[str, Any]) -> None: session_data = {"session": data, "created": int(time.time())} value = json.dumps(session_data) client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": value}) def create_app(handler: Handler) -> web.Application: app = web.Application() setup_middleware(app, SimpleCookieStorage(max_age=10)) app.router.add_route("GET", "/", handler) return app async def test_max_age_also_returns_expires(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.Response: session = await get_session(request) session["c"] = 3 return web.Response(body=b"OK") with mock.patch("time.time") as m_clock: m_clock.return_value = 0.0 client = await aiohttp_client(create_app(handler)) make_cookie(client, {"a": 1, "b": 2}) async with client.get("/") as resp: assert resp.status == 200 assert "expires=Thu, 01-Jan-1970 00:00:10 GMT" in resp.headers["SET-COOKIE"] async def test_max_age_session_reset(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request, n: Optional[str] = None) -> web.Response: session = await get_session(request) if n: session[n] = True return web.json_response(session._mapping) app = create_app(handler) app.router.add_route("GET", "/a", partial(handler, n="a")) app.router.add_route("GET", "/b", partial(handler, n="b")) app.router.add_route("GET", "/c", partial(handler, n="c")) client = await aiohttp_client(app) with mock.patch("time.time") as m_clock: m_clock.return_value = 0.0 # Initialise the session (with a 10 second max_age). async with client.get("/a") as resp: c = resp.cookies["AIOHTTP_SESSION"] assert "00:00:10" in c["expires"] assert {"a"} == json.loads(c.value)["session"].keys() m_clock.return_value = 8.0 # Here we update the session, which should reset expiry time to 18 seconds past. async with client.get("/b") as resp: c = resp.cookies["AIOHTTP_SESSION"] assert "00:00:18" in c["expires"] assert {"a", "b"} == json.loads(c.value)["session"].keys() m_clock.return_value = 15.0 # Because the session has been updated, it should not have expired yet. async with client.get("/") as resp: sess = await resp.json() assert {"a", "b"} == sess.keys() async with client.get("/c") as resp: c = resp.cookies["AIOHTTP_SESSION"] assert "00:00:25" in c["expires"] assert {"a", "b", "c"} == json.loads(c.value)["session"].keys() m_clock.return_value = 30.0 # Here the session should have expired. async with client.get("/") as resp: sess = await resp.json() assert sess == {} aiohttp-session-2.12.1/tests/test_cookie_storage.py000066400000000000000000000074041467501771200225150ustar00rootroot00000000000000import json import time from typing import Any, Dict, MutableMapping, cast from aiohttp import web from aiohttp.test_utils import TestClient from aiohttp.typedefs import Handler from aiohttp_session import ( Session, SimpleCookieStorage, get_session, session_middleware, ) from .typedefs import AiohttpClient def make_cookie(client: TestClient, data: Dict[str, Any]) -> None: session_data = {"session": data, "created": int(time.time())} value = json.dumps(session_data) client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": value}) def create_app(handler: Handler) -> web.Application: middleware = session_middleware(SimpleCookieStorage()) app = web.Application(middlewares=[middleware]) app.router.add_route("GET", "/", handler) return app async def test_create_new_session(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert session.new assert not session._changed assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler)) resp = await client.get("/") assert resp.status == 200 async def test_load_existing_session(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert not session.new assert not session._changed assert session.created is not None assert cast(MutableMapping[str, Any], {"a": 1, "b": 2}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler)) make_cookie(client, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 async def test_change_session(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["c"] = 3 return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler)) make_cookie(client, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 morsel = resp.cookies["AIOHTTP_SESSION"] cookie_data = json.loads(morsel.value) assert "session" in cookie_data assert "a" in cookie_data["session"] assert "b" in cookie_data["session"] assert "c" in cookie_data["session"] assert "created" in cookie_data assert cookie_data["session"]["a"] == 1 assert cookie_data["session"]["b"] == 2 assert cookie_data["session"]["c"] == 3 assert morsel["httponly"] assert "/" == morsel["path"] async def test_clear_cookie_on_session_invalidation( aiohttp_client: AiohttpClient, ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.invalidate() return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler)) make_cookie(client, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 assert ( 'Set-Cookie: AIOHTTP_SESSION="{}"; ' "domain=127.0.0.1; httponly; Path=/".upper() ) == resp.cookies["AIOHTTP_SESSION"].output().upper() async def test_dont_save_not_requested_session(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.StreamResponse: return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler)) make_cookie(client, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 assert "AIOHTTP_SESSION" not in resp.cookies aiohttp-session-2.12.1/tests/test_encrypted_cookie_storage.py000066400000000000000000000165531467501771200245770ustar00rootroot00000000000000import asyncio import base64 import json import time from typing import Any, Dict, MutableMapping, Tuple, Union, cast import pytest from aiohttp import web from aiohttp.test_utils import TestClient from aiohttp.typedefs import Handler from cryptography.fernet import Fernet from aiohttp_session import ( Session, get_session, new_session, session_middleware, ) from aiohttp_session.cookie_storage import EncryptedCookieStorage from .typedefs import AiohttpClient MAX_AGE = 1 def make_cookie(client: TestClient, fernet: Fernet, data: Dict[str, Any]) -> None: session_data = {"session": data, "created": int(time.time())} cookie_data = json.dumps(session_data).encode("utf-8") encrypted_data = fernet.encrypt(cookie_data).decode("utf-8") client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": encrypted_data}) def create_app( handler: Handler, key: Union[str, bytes, bytearray, Fernet] ) -> web.Application: middleware = session_middleware(EncryptedCookieStorage(key)) app = web.Application(middlewares=[middleware]) app.router.add_route("GET", "/", handler) return app def decrypt(fernet: Fernet, cookie_value: str) -> Dict[str, Any]: assert type(cookie_value) == str cookie_value = fernet.decrypt(cookie_value.encode("utf-8")).decode("utf-8") return cast(Dict[str, Any], json.loads(cookie_value)) @pytest.fixture def fernet_and_key() -> Tuple[Fernet, bytes]: key = Fernet.generate_key() fernet = Fernet(key) return fernet, base64.urlsafe_b64decode(key) @pytest.fixture def fernet(fernet_and_key: Tuple[Fernet, bytes]) -> Fernet: return fernet_and_key[0] @pytest.fixture def key(fernet_and_key: Tuple[Fernet, bytes]) -> bytes: return fernet_and_key[1] def test_invalid_key() -> None: with pytest.raises(ValueError): EncryptedCookieStorage(b"123") # short key def test_str_key() -> None: k = Fernet.generate_key().decode("utf-8") assert EncryptedCookieStorage(k) async def test_create_new_session_broken_by_format( aiohttp_client: AiohttpClient, fernet: Fernet, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert session.new assert not session._changed assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") new_fernet = Fernet(Fernet.generate_key()) client = await aiohttp_client(create_app(handler, key)) make_cookie(client, new_fernet, {"a": 1, "b": 12}) resp = await client.get("/") assert resp.status == 200 async def test_load_existing_session( aiohttp_client: AiohttpClient, fernet: Fernet, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert not session.new assert not session._changed assert cast(MutableMapping[str, Any], {"a": 1, "b": 12}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, key)) make_cookie(client, fernet, {"a": 1, "b": 12}) resp = await client.get("/") assert resp.status == 200 async def test_load_existing_session_with_fernet( aiohttp_client: AiohttpClient, fernet: Fernet ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert not session.new assert not session._changed assert {"a": 1, "b": 12} == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, fernet)) make_cookie(client, fernet, {"a": 1, "b": 12}) resp = await client.get("/") assert resp.status == 200 async def test_change_session( aiohttp_client: AiohttpClient, fernet: Fernet, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["c"] = 3 return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, key)) make_cookie(client, fernet, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 morsel = resp.cookies["AIOHTTP_SESSION"] cookie_data = decrypt(fernet, morsel.value) assert "session" in cookie_data assert "a" in cookie_data["session"] assert "b" in cookie_data["session"] assert "c" in cookie_data["session"] assert "created" in cookie_data assert cookie_data["session"]["a"] == 1 assert cookie_data["session"]["b"] == 2 assert cookie_data["session"]["c"] == 3 assert morsel["httponly"] assert "/" == morsel["path"] async def test_clear_cookie_on_session_invalidation( aiohttp_client: AiohttpClient, fernet: Fernet, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.invalidate() return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, key)) make_cookie(client, fernet, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 morsel = resp.cookies["AIOHTTP_SESSION"] assert "" == morsel.value assert not morsel["httponly"] assert morsel["path"] == "/" async def test_encrypted_cookie_session_fixation( aiohttp_client: AiohttpClient, fernet: Fernet, key: bytes ) -> None: async def login(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["k"] = "v" return web.Response() async def logout(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.invalidate() return web.Response() app = create_app(login, key) app.router.add_route("DELETE", "/", logout) client = await aiohttp_client(app) resp = await client.get("/") assert "AIOHTTP_SESSION" in resp.cookies evil_cookie = resp.cookies["AIOHTTP_SESSION"].value resp = await client.delete("/") assert resp.cookies["AIOHTTP_SESSION"].value == "" client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": evil_cookie}) resp = await client.get("/") assert resp.cookies["AIOHTTP_SESSION"].value != evil_cookie async def test_fernet_ttl( aiohttp_client: AiohttpClient, fernet: Fernet, key: bytes ) -> None: async def login(request: web.Request) -> web.StreamResponse: session = await new_session(request) session["created"] = int(time.time()) return web.Response() async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) created = session["created"] if not session.new else "" return web.Response(text=str(created)) middleware = session_middleware(EncryptedCookieStorage(key, max_age=MAX_AGE)) app = web.Application(middlewares=[middleware]) app.router.add_route("POST", "/", login) app.router.add_route("GET", "/", handler) client = await aiohttp_client(app) resp = await client.post("/") assert "AIOHTTP_SESSION" in resp.cookies cookie = resp.cookies["AIOHTTP_SESSION"].value await asyncio.sleep(MAX_AGE + 1) client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": cookie}) resp = await client.get("/") assert await resp.text() == "" aiohttp-session-2.12.1/tests/test_get_session.py000066400000000000000000000045711467501771200220440ustar00rootroot00000000000000import pytest from aiohttp import web from aiohttp.test_utils import make_mocked_request from aiohttp_session import ( SESSION_KEY, STORAGE_KEY, AbstractStorage, Session, get_session, new_session, ) async def test_get_stored_session() -> None: req = make_mocked_request("GET", "/") session = Session("identity", data=None, new=False) req[SESSION_KEY] = session ret = await get_session(req) assert session is ret async def test_session_is_not_stored() -> None: req = make_mocked_request("GET", "/") with pytest.raises(RuntimeError): await get_session(req) async def test_storage_returns_not_session_on_load_session() -> None: req = make_mocked_request("GET", "/") class Storage: async def load_session(self, request: web.Request) -> None: return None req[STORAGE_KEY] = Storage() with pytest.raises(RuntimeError): await get_session(req) async def test_get_new_session() -> None: req = make_mocked_request("GET", "/") session = Session("identity", data=None, new=False) class Storage(AbstractStorage): async def load_session(self, request: web.Request) -> Session: # type: ignore[empty-body] """Dummy""" async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: """Dummy""" req[SESSION_KEY] = session req[STORAGE_KEY] = Storage() ret = await new_session(req) assert ret is not session async def test_get_new_session_no_storage() -> None: req = make_mocked_request("GET", "/") session = Session("identity", data=None, new=False) req[SESSION_KEY] = session with pytest.raises(RuntimeError): await new_session(req) async def test_get_new_session_bad_return() -> None: req = make_mocked_request("GET", "/") class Storage(AbstractStorage): async def new_session(self) -> Session: return "" # type: ignore[return-value] async def load_session(self, request: web.Request) -> Session: # type: ignore[empty-body] """Dummy""" async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: """Dummy""" req[STORAGE_KEY] = Storage() with pytest.raises(RuntimeError): await new_session(req) aiohttp-session-2.12.1/tests/test_http_exception.py000066400000000000000000000022551467501771200225540ustar00rootroot00000000000000from typing import Tuple from aiohttp import web from aiohttp.typedefs import Handler from aiohttp_session import ( SimpleCookieStorage, get_session, session_middleware, ) from .typedefs import AiohttpClient def create_app(*handlers: Tuple[str, Handler]) -> web.Application: middleware = session_middleware(SimpleCookieStorage()) app = web.Application(middlewares=[middleware]) for url, handler in handlers: app.router.add_route("GET", url, handler) return app async def test_exceptions(aiohttp_client: AiohttpClient) -> None: async def save(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["message"] = "works" raise web.HTTPFound("/show") async def show(request: web.Request) -> web.StreamResponse: session = await get_session(request) message = session.get("message") return web.Response(text=str(message)) client = await aiohttp_client(create_app(("/save", save), ("/show", show))) resp = await client.get("/save") assert resp.status == 200 assert str(resp.url)[-5:] == "/show" text = await resp.text() assert text == "works" aiohttp-session-2.12.1/tests/test_memcached_storage.py000066400000000000000000000251211467501771200231460ustar00rootroot00000000000000import asyncio import json import time import uuid from typing import Any, Callable, Dict, MutableMapping, Optional, cast import aiomcache from aiohttp import web from aiohttp.test_utils import TestClient from aiohttp.typedefs import Handler from aiohttp_session import Session, get_session, session_middleware from aiohttp_session.memcached_storage import MemcachedStorage from .typedefs import AiohttpClient def create_app( handler: Handler, memcached: aiomcache.Client, max_age: Optional[int] = None, key_factory: Callable[[], str] = lambda: uuid.uuid4().hex, ) -> web.Application: middleware = session_middleware( MemcachedStorage(memcached, max_age=max_age, key_factory=key_factory) ) app = web.Application(middlewares=[middleware]) app.router.add_route("GET", "/", handler) return app async def make_cookie( client: TestClient, memcached: aiomcache.Client, data: Dict[str, Any] ) -> None: session_data = {"session": data, "created": int(time.time())} value = json.dumps(session_data) key = uuid.uuid4().hex storage_key = ("AIOHTTP_SESSION_" + key).encode("utf-8") await memcached.set(storage_key, bytes(value, "utf-8")) client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": key}) async def make_cookie_with_bad_value( client: TestClient, memcached: aiomcache.Client ) -> None: key = uuid.uuid4().hex storage_key = ("AIOHTTP_SESSION_" + key).encode("utf-8") await memcached.set(storage_key, b"") client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": key}) async def load_cookie( client: TestClient, memcached: aiomcache.Client ) -> Dict[str, Any]: cookies = client.session.cookie_jar.filter_cookies(client.make_url("/")) key = cookies["AIOHTTP_SESSION"] storage_key = ("AIOHTTP_SESSION_" + key.value).encode("utf-8") encoded = await memcached.get(storage_key) assert encoded is not None s = encoded.decode("utf-8") return cast(Dict[str, Any], json.loads(s)) async def test_create_new_session( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert session.new assert not session._changed assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, memcached)) resp = await client.get("/") assert resp.status == 200 async def test_load_existing_session( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert not session.new assert not session._changed assert cast(MutableMapping[str, Any], {"a": 1, "b": 12}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, memcached)) await make_cookie(client, memcached, {"a": 1, "b": 12}) resp = await client.get("/") assert resp.status == 200 async def test_load_bad_session( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert not session.new assert not session._changed assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, memcached)) await make_cookie_with_bad_value(client, memcached) resp = await client.get("/") assert resp.status == 200 async def test_change_session( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["c"] = 3 return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, memcached)) await make_cookie(client, memcached, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 value = await load_cookie(client, memcached) assert "session" in value assert "a" in value["session"] assert "b" in value["session"] assert "c" in value["session"] assert "created" in value assert value["session"]["a"] == 1 assert value["session"]["b"] == 2 assert value["session"]["c"] == 3 morsel = resp.cookies["AIOHTTP_SESSION"] assert morsel["httponly"] assert "/" == morsel["path"] async def test_clear_cookie_on_session_invalidation( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.invalidate() return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, memcached)) await make_cookie(client, memcached, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 value = await load_cookie(client, memcached) assert {} == value morsel = resp.cookies["AIOHTTP_SESSION"] assert morsel["path"] == "/" assert morsel["expires"] == "Thu, 01 Jan 1970 00:00:00 GMT" assert morsel["max-age"] == "0" async def test_create_cookie_in_handler( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["a"] = 1 session["b"] = 2 return web.Response(body=b"OK", headers={"HOST": "example.com"}) client = await aiohttp_client(create_app(handler, memcached)) resp = await client.get("/") assert resp.status == 200 value = await load_cookie(client, memcached) assert "session" in value assert "a" in value["session"] assert "b" in value["session"] assert "created" in value assert value["session"]["a"] == 1 assert value["session"]["b"] == 2 morsel = resp.cookies["AIOHTTP_SESSION"] assert morsel["httponly"] assert morsel["path"] == "/" storage_key = ("AIOHTTP_SESSION_" + morsel.value).encode("utf-8") exists = await memcached.get(storage_key) assert exists async def test_create_new_session_if_key_doesnt_exists_in_memcached( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert session.new return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, memcached)) client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": "invalid_key"}) resp = await client.get("/") assert resp.status == 200 async def test_create_storage_with_custom_key_factory( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["key"] = "value" assert session.new return web.Response(body=b"OK") def key_factory() -> str: return "test-key" client = await aiohttp_client(create_app(handler, memcached, 8, key_factory)) resp = await client.get("/") assert resp.status == 200 assert resp.cookies["AIOHTTP_SESSION"].value == "test-key" value = await load_cookie(client, memcached) assert "key" in value["session"] assert value["session"]["key"] == "value" async def test_memcached_session_fixation( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def login(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["k"] = "v" return web.Response() async def logout(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.invalidate() return web.Response() app = create_app(login, memcached) app.router.add_route("DELETE", "/", logout) client = await aiohttp_client(app) resp = await client.get("/") assert "AIOHTTP_SESSION" in resp.cookies evil_cookie = resp.cookies["AIOHTTP_SESSION"].value resp = await client.delete("/") assert resp.cookies["AIOHTTP_SESSION"].value == "" client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": evil_cookie}) resp = await client.get("/") assert resp.cookies["AIOHTTP_SESSION"].value != evil_cookie async def test_load_session_dont_load_expired_session( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) exp_param = request.rel_url.query.get("exp", None) if exp_param is None: session["a"] = 1 session["b"] = 2 else: assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, memcached, 2)) resp = await client.get("/") assert resp.status == 200 await asyncio.sleep(5) resp = await client.get("/?exp=yes") assert resp.status == 200 async def test_memcached_max_age_over_30_days( aiohttp_client: AiohttpClient, memcached: aiomcache.Client ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["stored"] = "TEST_VALUE" session.max_age = 30 * 24 * 60 * 60 + 1 assert session.new return web.Response(body=b"OK") async def get_value(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert not session.new response = session["stored"] return web.Response(body=response.encode("utf-8")) app = create_app(handler, memcached) app.router.add_route("GET", "/get_value", get_value) client = await aiohttp_client(app) resp = await client.get("/") assert resp.status == 200 assert "AIOHTTP_SESSION" in resp.cookies storage_key = ("AIOHTTP_SESSION_" + resp.cookies["AIOHTTP_SESSION"].value).encode( "utf-8" ) storage_value = await memcached.get(storage_key) assert storage_value is not None storage_dict = json.loads(storage_value.decode("utf-8")) assert storage_dict["session"]["stored"] == "TEST_VALUE" resp = await client.get("/get_value") assert resp.status == 200 resp_content = await resp.text() assert resp_content == "TEST_VALUE" aiohttp-session-2.12.1/tests/test_nacl_storage.py000066400000000000000000000206741467501771200221650ustar00rootroot00000000000000import asyncio import json import time from typing import Any, Dict, MutableMapping, Optional, cast import nacl.secret import nacl.utils import pytest from aiohttp import web from aiohttp.test_utils import TestClient from aiohttp.typedefs import Handler from nacl.encoding import Base64Encoder from aiohttp_session import ( Session, get_session, new_session, session_middleware, ) from aiohttp_session.nacl_storage import NaClCookieStorage from .typedefs import AiohttpClient def test_invalid_key() -> None: with pytest.raises(ValueError): NaClCookieStorage(b"123") # short key def make_cookie( client: TestClient, secretbox: nacl.secret.SecretBox, data: Dict[str, Any] ) -> None: session_data = {"session": data, "created": int(time.time())} cookie_data = json.dumps(session_data).encode("utf-8") nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE) encr = secretbox.encrypt(cookie_data, nonce, encoder=Base64Encoder).decode("utf-8") client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": encr}) def create_app( handler: Handler, key: bytes, max_age: Optional[int] = None ) -> web.Application: middleware = session_middleware(NaClCookieStorage(key, max_age=max_age)) app = web.Application(middlewares=[middleware]) app.router.add_route("GET", "/", handler) return app def decrypt(secretbox: nacl.secret.SecretBox, cookie_value: str) -> Any: assert type(cookie_value) == str return json.loads( secretbox.decrypt(cookie_value.encode("utf-8"), encoder=Base64Encoder).decode( "utf-8" ) ) @pytest.fixture def secretbox(key: bytes) -> nacl.secret.SecretBox: return nacl.secret.SecretBox(key) @pytest.fixture def key() -> bytes: return nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE) async def test_create_new_session( aiohttp_client: AiohttpClient, secretbox: nacl.secret.SecretBox, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert session.new assert not session._changed assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, key)) resp = await client.get("/") assert resp.status == 200 async def test_load_existing_session( aiohttp_client: AiohttpClient, secretbox: nacl.secret.SecretBox, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert not session.new assert not session._changed assert cast(MutableMapping[str, Any], {"a": 1, "b": 12}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, key)) make_cookie(client, secretbox, {"a": 1, "b": 12}) resp = await client.get("/") assert resp.status == 200 async def test_change_session( aiohttp_client: AiohttpClient, secretbox: nacl.secret.SecretBox, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["c"] = 3 return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, key)) make_cookie(client, secretbox, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 morsel = resp.cookies["AIOHTTP_SESSION"] cookie_data = decrypt(secretbox, morsel.value) assert "session" in cookie_data assert "a" in cookie_data["session"] assert "b" in cookie_data["session"] assert "c" in cookie_data["session"] assert "created" in cookie_data assert cookie_data["session"]["a"] == 1 assert cookie_data["session"]["b"] == 2 assert cookie_data["session"]["c"] == 3 assert morsel["httponly"] assert "/" == morsel["path"] async def test_del_cookie_on_session_invalidation( aiohttp_client: AiohttpClient, secretbox: nacl.secret.SecretBox, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.invalidate() return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, key)) make_cookie(client, secretbox, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 morsel = resp.cookies["AIOHTTP_SESSION"] assert "" == morsel.value assert not morsel["httponly"] assert morsel["path"] == "/" async def test_nacl_session_fixation( aiohttp_client: AiohttpClient, secretbox: nacl.secret.SecretBox, key: bytes ) -> None: async def login(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["k"] = "v" return web.Response() async def logout(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.invalidate() return web.Response() app = create_app(login, key) app.router.add_route("DELETE", "/", logout) client = await aiohttp_client(app) resp = await client.get("/") assert "AIOHTTP_SESSION" in resp.cookies evil_cookie = resp.cookies["AIOHTTP_SESSION"].value resp = await client.delete("/") assert resp.cookies["AIOHTTP_SESSION"].value == "" client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": evil_cookie}) resp = await client.get("/") assert resp.cookies["AIOHTTP_SESSION"].value != evil_cookie async def test_load_session_dont_load_expired_session( aiohttp_client: AiohttpClient, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) exp_param = request.rel_url.query.get("exp", None) if exp_param is None: session["a"] = 1 session["b"] = 2 else: assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, key, 2)) resp = await client.get("/") assert resp.status == 200 await asyncio.sleep(5) resp = await client.get("/?exp=yes") assert resp.status == 200 async def test_load_corrupted_session( aiohttp_client: AiohttpClient, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert session.new assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, key)) client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": "bad key"}) resp = await client.get("/") assert resp.status == 200 async def test_load_session_different_key( aiohttp_client: AiohttpClient, key: bytes ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert session.new assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, key)) # create another box with another key key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE) secretbox = nacl.secret.SecretBox(key) make_cookie(client, secretbox, {"a": 1, "b": 12}) resp = await client.get("/") assert resp.status == 200 async def test_load_expired_session(aiohttp_client: AiohttpClient, key: bytes) -> None: MAX_AGE = 2 async def login(request: web.Request) -> web.StreamResponse: session = await new_session(request) session["created"] = int(time.time()) return web.Response() async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) created = session.get("created", "") if not session.new else "" return web.Response(text=str(created)) app = create_app(handler, key, max_age=MAX_AGE) app.router.add_route("POST", "/", login) client = await aiohttp_client(app) resp = await client.post("/") assert "AIOHTTP_SESSION" in resp.cookies cookie = resp.cookies["AIOHTTP_SESSION"].value await asyncio.sleep(MAX_AGE + 1) client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": cookie}) resp = await client.get("/") assert await resp.text() == "" aiohttp-session-2.12.1/tests/test_path_domain.py000066400000000000000000000111571467501771200220030ustar00rootroot00000000000000import json import time from http import cookies from typing import Any, Optional from aiohttp import web from aiohttp.test_utils import TestClient from aiohttp.typedefs import Handler from aiohttp_session import SimpleCookieStorage, get_session, session_middleware from .typedefs import AiohttpClient def make_cookie( client: TestClient, data: Any, path: Optional[str] = None, domain: Optional[str] = None, ) -> None: session_data = {"session": data, "created": int(time.time())} C = cookies.SimpleCookie() value = json.dumps(session_data) C["AIOHTTP_SESSION"] = value C["AIOHTTP_SESSION"]["path"] = path C["AIOHTTP_SESSION"]["domain"] = domain client.session.cookie_jar.update_cookies(C) def create_app( handler: Handler, path: Optional[str] = None, domain: Optional[str] = None ) -> web.Application: storage = SimpleCookieStorage(max_age=10, path="/anotherpath", domain="127.0.0.1") middleware = session_middleware(storage) app = web.Application(middlewares=[middleware]) app.router.add_route("GET", "/", handler) app.router.add_route("GET", "/anotherpath", handler) return app async def test_with_same_path_domain(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["c"] = 3 return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler)) make_cookie(client, {"a": 1, "b": 2}, path="/anotherpath", domain="127.0.0.1") resp = await client.get("/anotherpath") assert resp.status == 200 morsel = resp.cookies["AIOHTTP_SESSION"] cookie_data = json.loads(morsel.value) assert "session" in cookie_data assert "a" in cookie_data["session"] assert "b" in cookie_data["session"] assert "c" in cookie_data["session"] assert "created" in cookie_data assert cookie_data["session"]["a"] == 1 assert cookie_data["session"]["b"] == 2 assert cookie_data["session"]["c"] == 3 assert morsel["httponly"] assert "/anotherpath" == morsel["path"] assert "127.0.0.1" == morsel["domain"] async def test_with_different_path(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["c"] = 3 return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler)) make_cookie(client, {"a": 1, "b": 2}, path="/NotTheSame", domain="127.0.0.1") resp = await client.get("/anotherpath") assert resp.status == 200 morsel = resp.cookies["AIOHTTP_SESSION"] cookie_data = json.loads(morsel.value) assert "session" in cookie_data assert "a" not in cookie_data["session"] assert "b" not in cookie_data["session"] assert "c" in cookie_data["session"] assert "created" in cookie_data assert cookie_data["session"]["c"] == 3 assert morsel["httponly"] assert "/anotherpath" == morsel["path"] assert "127.0.0.1" == morsel["domain"] async def test_with_different_domain(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["c"] = 3 return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler)) make_cookie(client, {"a": 1, "b": 2}, path="/anotherpath", domain="localhost") resp = await client.get("/anotherpath") assert resp.status == 200 morsel = resp.cookies["AIOHTTP_SESSION"] cookie_data = json.loads(morsel.value) assert "session" in cookie_data assert "a" not in cookie_data["session"] assert "b" not in cookie_data["session"] assert "c" in cookie_data["session"] assert "created" in cookie_data assert cookie_data["session"]["c"] == 3 assert morsel["httponly"] assert "/anotherpath" == morsel["path"] assert "127.0.0.1" == morsel["domain"] async def test_invalidate_with_same_path_domain(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.invalidate() return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler)) make_cookie(client, {"a": 1, "b": 2}, path="/anotherpath", domain="127.0.0.1") resp = await client.get("/anotherpath") assert resp.status == 200 morsel = resp.cookies["AIOHTTP_SESSION"] cookie_data = json.loads(morsel.value) assert {} == cookie_data assert morsel["httponly"] assert "/anotherpath" == morsel["path"] assert "127.0.0.1" == morsel["domain"] aiohttp-session-2.12.1/tests/test_redis_storage.py000066400000000000000000000265571467501771200223640ustar00rootroot00000000000000from __future__ import annotations import asyncio import json import time import uuid from typing import Any, Callable, Dict, MutableMapping, Optional, cast import pytest from aiohttp import web from aiohttp.test_utils import TestClient from aiohttp.typedefs import Handler from pytest_mock import MockFixture from redis import asyncio as aioredis from aiohttp_session import Session, get_session, session_middleware from aiohttp_session.redis_storage import RedisStorage from .typedefs import AiohttpClient def create_app( handler: Handler, redis: aioredis.Redis, max_age: Optional[int] = None, key_factory: Callable[[], str] = lambda: uuid.uuid4().hex, ) -> web.Application: middleware = session_middleware( RedisStorage(redis, max_age=max_age, key_factory=key_factory) ) app = web.Application(middlewares=[middleware]) app.router.add_route("GET", "/", handler) return app async def make_cookie( client: TestClient, redis: aioredis.Redis, data: Dict[Any, Any] ) -> None: session_data = {"session": data, "created": int(time.time())} value = json.dumps(session_data) key = uuid.uuid4().hex await redis.set("AIOHTTP_SESSION_" + key, value) client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": key}) async def make_cookie_with_bad_value(client: TestClient, redis: aioredis.Redis) -> None: key = uuid.uuid4().hex await redis.set("AIOHTTP_SESSION_" + key, "") client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": key}) async def load_cookie(client: TestClient, redis: aioredis.Redis) -> Any: cookies = client.session.cookie_jar.filter_cookies(client.make_url("/")) key = cookies["AIOHTTP_SESSION"] value_bytes = await redis.get("AIOHTTP_SESSION_" + key.value) return None if value_bytes is None else json.loads(value_bytes) async def test_create_new_session( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert session.new assert not session._changed assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, redis)) resp = await client.get("/") assert resp.status == 200 async def test_load_existing_session( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert not session.new assert not session._changed assert cast(MutableMapping[str, Any], {"a": 1, "b": 12}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, redis)) await make_cookie(client, redis, {"a": 1, "b": 12}) resp = await client.get("/") assert resp.status == 200 async def test_load_bad_session( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert isinstance(session, Session) assert not session.new assert not session._changed assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, redis)) await make_cookie_with_bad_value(client, redis) resp = await client.get("/") assert resp.status == 200 async def test_change_session( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["c"] = 3 return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, redis)) await make_cookie(client, redis, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 value = await load_cookie(client, redis) assert "session" in value assert "a" in value["session"] assert "b" in value["session"] assert "c" in value["session"] assert "created" in value assert value["session"]["a"] == 1 assert value["session"]["b"] == 2 assert value["session"]["c"] == 3 morsel = resp.cookies["AIOHTTP_SESSION"] assert morsel["httponly"] assert "/" == morsel["path"] async def test_clear_cookie_on_session_invalidation( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.invalidate() return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, redis)) await make_cookie(client, redis, {"a": 1, "b": 2}) resp = await client.get("/") assert resp.status == 200 value = await load_cookie(client, redis) assert {} == value morsel = resp.cookies["AIOHTTP_SESSION"] assert morsel["path"] == "/" assert morsel["expires"] == "Thu, 01 Jan 1970 00:00:00 GMT" assert morsel["max-age"] == "0" async def test_create_cookie_in_handler( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["a"] = 1 session["b"] = 2 return web.Response(body=b"OK", headers={"HOST": "example.com"}) client = await aiohttp_client(create_app(handler, redis)) resp = await client.get("/") assert resp.status == 200 value = await load_cookie(client, redis) assert "session" in value assert "a" in value["session"] assert "b" in value["session"] assert "created" in value assert value["session"]["a"] == 1 assert value["session"]["b"] == 2 morsel = resp.cookies["AIOHTTP_SESSION"] assert morsel["httponly"] assert morsel["path"] == "/" exists = await redis.exists("AIOHTTP_SESSION_" + morsel.value) assert exists async def test_set_ttl_on_session_saving( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["a"] = 1 return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, redis, max_age=10)) resp = await client.get("/") assert resp.status == 200 key = resp.cookies["AIOHTTP_SESSION"].value ttl = await redis.ttl("AIOHTTP_SESSION_" + key) assert ttl > 9 assert ttl <= 10 async def test_set_ttl_manually_set( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.max_age = 10 session["a"] = 1 return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, redis)) resp = await client.get("/") assert resp.status == 200 key = resp.cookies["AIOHTTP_SESSION"].value ttl = await redis.ttl("AIOHTTP_SESSION_" + key) assert ttl > 9 assert ttl <= 10 async def test_create_new_session_if_key_doesnt_exists_in_redis( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) assert session.new return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, redis)) client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": "invalid_key"}) resp = await client.get("/") assert resp.status == 200 async def test_create_storage_with_custom_key_factory( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["key"] = "value" assert session.new return web.Response(body=b"OK") def key_factory() -> str: return "test-key" client = await aiohttp_client(create_app(handler, redis, 8, key_factory)) resp = await client.get("/") assert resp.status == 200 assert resp.cookies["AIOHTTP_SESSION"].value == "test-key" value = await load_cookie(client, redis) assert "key" in value["session"] assert value["session"]["key"] == "value" async def test_redis_session_fixation( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def login(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["k"] = "v" return web.Response() async def logout(request: web.Request) -> web.StreamResponse: session = await get_session(request) session.invalidate() return web.Response() app = create_app(login, redis) app.router.add_route("DELETE", "/", logout) client = await aiohttp_client(app) resp = await client.get("/") assert "AIOHTTP_SESSION" in resp.cookies evil_cookie = resp.cookies["AIOHTTP_SESSION"].value resp = await client.delete("/") assert resp.cookies["AIOHTTP_SESSION"].value == "" client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": evil_cookie}) resp = await client.get("/") assert resp.cookies["AIOHTTP_SESSION"].value != evil_cookie async def test_redis_from_create_pool(redis_url: str) -> None: async def handler(request: web.Request) -> web.Response: # type: ignore[empty-body] """Dummy handler""" redis = aioredis.from_url(redis_url) # type: ignore[no-untyped-call] create_app(handler=handler, redis=redis) await redis.aclose() async def test_not_redis_provided_to_storage() -> None: async def handler(request: web.Request) -> web.Response: # type: ignore[empty-body] """Dummy handler""" with pytest.raises(TypeError): create_app(handler=handler, redis=None) # type: ignore[arg-type] async def test_no_aioredis_installed(mocker: MockFixture) -> None: async def handler(request: web.Request) -> web.Response: # type: ignore[empty-body] """Dummy handler""" mocker.patch("aiohttp_session.redis_storage.aioredis", None) with pytest.raises(RuntimeError): create_app(handler=handler, redis=None) # type: ignore[arg-type] async def test_old_aioredis_version(mocker: MockFixture) -> None: async def handler(request: web.Request) -> web.Response: # type: ignore[empty-body] """Dummy handler""" mocker.patch("aiohttp_session.redis_storage.REDIS_VERSION", (0, 3, "dev0")) with pytest.raises(RuntimeError): create_app(handler=handler, redis=None) # type: ignore[arg-type] async def test_load_session_dont_load_expired_session( aiohttp_client: AiohttpClient, redis: aioredis.Redis ) -> None: async def handler(request: web.Request) -> web.StreamResponse: session = await get_session(request) exp_param = request.rel_url.query.get("exp", None) if exp_param is None: session["a"] = 1 session["b"] = 2 else: assert cast(MutableMapping[str, Any], {}) == session return web.Response(body=b"OK") client = await aiohttp_client(create_app(handler, redis, 2)) resp = await client.get("/") assert resp.status == 200 await asyncio.sleep(5) resp = await client.get("/?exp=yes") assert resp.status == 200 aiohttp-session-2.12.1/tests/test_response_types.py000066400000000000000000000034621467501771200226020ustar00rootroot00000000000000from typing import Tuple import pytest from aiohttp import web from aiohttp.test_utils import make_mocked_request from aiohttp.typedefs import Handler from aiohttp_session import ( SESSION_KEY, SimpleCookieStorage, get_session, session_middleware, ) from .typedefs import AiohttpClient def create_app(*handlers: Tuple[str, Handler]) -> web.Application: middleware = session_middleware(SimpleCookieStorage()) app = web.Application(middlewares=[middleware]) for url, handler in handlers: app.router.add_route("GET", url, handler) return app async def test_stream_response(aiohttp_client: AiohttpClient) -> None: async def stream_response(request: web.Request) -> web.StreamResponse: session = await get_session(request) session["will_not"] = "show up" return web.StreamResponse() client = await aiohttp_client(create_app(("/stream", stream_response))) resp = await client.get("/stream") assert resp.status == 200 assert SESSION_KEY.upper() not in resp.cookies async def test_bad_response_type(aiohttp_client: AiohttpClient) -> None: async def bad_response(request: web.Request): # type: ignore[no-untyped-def] return "" middleware = session_middleware(SimpleCookieStorage()) req = make_mocked_request("GET", "/") with pytest.raises(RuntimeError): await middleware(req, bad_response) async def test_prepared_response_type(aiohttp_client: AiohttpClient) -> None: async def prepared_response(request: web.Request) -> web.StreamResponse: resp = web.Response() await resp.prepare(request) return resp middleware = session_middleware(SimpleCookieStorage()) req = make_mocked_request("GET", "/") with pytest.raises(RuntimeError): await middleware(req, prepared_response) aiohttp-session-2.12.1/tests/test_session_dict.py000066400000000000000000000110241467501771200221770ustar00rootroot00000000000000import time from typing import Any, MutableMapping, cast import pytest from aiohttp_session import Session, SessionData def test_create() -> None: s = Session("test_identity", data=None, new=True) assert s == cast(MutableMapping[str, Any], {}) assert s.new assert "test_identity" == s.identity assert not s._changed assert s.created is not None def test_create2() -> None: s = Session("test_identity", data={"session": {"some": "data"}}, new=False) assert s == cast(MutableMapping[str, Any], {"some": "data"}) assert not s.new assert "test_identity" == s.identity assert not s._changed assert s.created is not None def test_create3() -> None: s = Session(identity=1, data=None, new=True) assert s == cast(MutableMapping[str, Any], {}) assert s.new assert s.identity == 1 assert not s._changed assert s.created is not None def test_set_new_identity_ok() -> None: s = Session(identity=1, data=None, new=True) assert s.new assert s.identity == 1 s.set_new_identity(2) assert s.new assert s.identity == 2 def test_set_new_identity_for_not_new_session() -> None: s = Session(identity=1, data=None, new=False) with pytest.raises(RuntimeError): s.set_new_identity(2) def test__repr__() -> None: s = Session("test_identity", data=None, new=True) assert str(s) == "".format( s.created ) s["foo"] = "bar" assert str( s ) == "".format( s.created ) def test__repr__2() -> None: created = int(time.time()) - 1000 session_data: SessionData = {"session": {"key": 123}, "created": created} s = Session("test_identity", data=session_data, new=False) assert str( s ) == "".format( created ) s.invalidate() assert str(s) == "".format( created ) def test_invalidate() -> None: s = Session("test_identity", data={"session": {"foo": "bar"}}, new=False) assert s == cast(MutableMapping[str, Any], {"foo": "bar"}) assert not s._changed s.invalidate() assert s == cast(MutableMapping[str, Any], {}) assert s._changed # Mypy bug: https://github.com/python/mypy/issues/11853 assert s.created is not None # type: ignore[unreachable] def test_invalidate2() -> None: s = Session("test_identity", data={"session": {"foo": "bar"}}, new=False) assert s == cast(MutableMapping[str, Any], {"foo": "bar"}) assert not s._changed s.invalidate() assert s == cast(MutableMapping[str, Any], {}) assert s._changed # Mypy bug: https://github.com/python/mypy/issues/11853 assert s.created is not None # type: ignore[unreachable] def test_operations() -> None: s = Session("test_identity", data=None, new=False) assert s == cast(MutableMapping[str, Any], {}) assert len(s) == 0 assert list(s) == [] assert "foo" not in s assert "key" not in s s = Session("test_identity", data={"session": {"foo": "bar"}}, new=False) assert len(s) == 1 assert s == cast(MutableMapping[str, Any], {"foo": "bar"}) assert list(s) == ["foo"] assert "foo" in s assert "key" not in s s["key"] = "value" assert len(s) == 2 assert s == cast(MutableMapping[str, Any], {"foo": "bar", "key": "value"}) assert sorted(s) == ["foo", "key"] assert "foo" in s assert "key" in s del s["key"] assert len(s) == 1 assert s == cast(MutableMapping[str, Any], {"foo": "bar"}) assert list(s) == ["foo"] assert "foo" in s assert "key" not in s s.pop("foo") assert len(s) == 0 assert s == cast(MutableMapping[str, Any], {}) assert list(s) == [] assert "foo" not in s assert "key" not in s def test_change() -> None: created = int(time.time()) s = Session( "test_identity", new=False, data={"session": {"a": {"key": "value"}}, "created": created}, ) assert not s._changed s["a"]["key2"] = "val2" assert not s._changed assert cast(MutableMapping[str, Any], {"a": {"key": "value", "key2": "val2"}}) == s assert s.created == created s.changed() assert s._changed # Mypy bug: https://github.com/python/mypy/issues/11853 assert s.created == created # type: ignore[unreachable] assert cast(MutableMapping[str, Any], {"a": {"key": "value", "key2": "val2"}}) == s aiohttp-session-2.12.1/tests/test_session_middleware.py000066400000000000000000000004351467501771200233750ustar00rootroot00000000000000import pytest from aiohttp_session import session_middleware async def test_session_middleware_bad_storage() -> None: with pytest.raises(RuntimeError): # Ignoring typing since parameter type is wrong on purpose session_middleware(None) # type: ignore[arg-type] aiohttp-session-2.12.1/tests/typedefs.py000066400000000000000000000002561467501771200203020ustar00rootroot00000000000000from typing import Awaitable, Callable from aiohttp import web from aiohttp.test_utils import TestClient AiohttpClient = Callable[[web.Application], Awaitable[TestClient]]