pax_global_header00006660000000000000000000000064141760663220014521gustar00rootroot0000000000000052 comment=af0560812d3dc2043565de1108ac41b65caac7d0 aiohttp-session-2.11.0/000077500000000000000000000000001417606632200147335ustar00rootroot00000000000000aiohttp-session-2.11.0/.codecov.yml000066400000000000000000000000711417606632200171540ustar00rootroot00000000000000codecov: branch: master notify: wait_for_ci: yes aiohttp-session-2.11.0/.coveragerc000066400000000000000000000001461417606632200170550ustar00rootroot00000000000000[run] branch = True source = aiohttp_session, tests omit = site-packages [html] directory = coverage aiohttp-session-2.11.0/.flake8000066400000000000000000000014441417606632200161110ustar00rootroot00000000000000[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.11.0/.github/000077500000000000000000000000001417606632200162735ustar00rootroot00000000000000aiohttp-session-2.11.0/.github/dependabot.yml000066400000000000000000000005371417606632200211300ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily open-pull-requests-limit: 10 ignore: - dependency-name: docker versions: - 4.4.1 - 4.4.2 - 4.4.3 - dependency-name: sphinx versions: - 3.4.3 - 3.5.0 - 3.5.1 - dependency-name: cryptography versions: - 3.4.1 aiohttp-session-2.11.0/.github/workflows/000077500000000000000000000000001417606632200203305ustar00rootroot00000000000000aiohttp-session-2.11.0/.github/workflows/auto-merge.yml000066400000000000000000000011401417606632200231140ustar00rootroot00000000000000name: 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.1.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.11.0/.github/workflows/ci.yaml000066400000000000000000000054171417606632200216160ustar00rootroot00000000000000name: CI on: [pull_request, push] jobs: lint: name: Linter runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Checkout uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v2 with: python-version: 3.9 - name: Cache PyPI uses: actions/cache@v2 with: key: pip-lint-${{ hashFiles('requirements-dev.txt') }} path: ~/.cache/pip restore-keys: | pip-lint- - name: Install dependencies uses: py-actions/py-dependency-install@v2 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 twine wheel python setup.py sdist bdist_wheel - name: Run twine checker run: twine check dist/* test: name: Tests runs-on: ubuntu-latest strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | pip install --upgrade pip build twine pip install -r requirements-dev.txt - name: Run tests run: | make cov python -m build twine check dist/* - name: Upload coverage uses: codecov/codecov-action@v1 with: file: ./coverage.xml flags: unit 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: [test] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2.4.0 - 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.2.3 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.11.0/.gitignore000066400000000000000000000013371417606632200167270ustar00rootroot00000000000000# 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.11.0/.isort.cfg000066400000000000000000000001641417606632200166330ustar00rootroot00000000000000[settings] line_length=88 include_trailing_comma=True multi_line_output=3 force_grid_wrap=0 combine_as_imports=True aiohttp-session-2.11.0/.mypy.ini000066400000000000000000000015611417606632200165130ustar00rootroot00000000000000[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 implicit_reexport = False no_implicit_optional = True show_error_codes = 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-aioredis.*] 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.11.0/.pre-commit-config.yaml000066400000000000000000000041521417606632200212160ustar00rootroot00000000000000repos: - 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.0.1' hooks: - id: check-merge-conflict - repo: https://github.com/asottile/yesqa rev: v1.3.0 hooks: - id: yesqa - repo: https://github.com/PyCQA/isort rev: '5.9.3' hooks: - id: isort - repo: https://github.com/psf/black rev: '21.10b0' hooks: - id: black language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks rev: 'v4.0.1' 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: 'v2.29.0' hooks: - id: pyupgrade args: ['--py36-plus'] - repo: https://github.com/PyCQA/flake8 rev: '4.0.1' hooks: - id: flake8 exclude: "^docs/" - repo: git://github.com/Lucas-C/pre-commit-hooks-markup rev: v1.0.1 hooks: - id: rst-linter files: >- ^[^/]+[.]rst$ exclude: >- ^CHANGES\.rst$ aiohttp-session-2.11.0/.pyup.yml000066400000000000000000000001221417606632200165240ustar00rootroot00000000000000# Label PRs with `deps-update` label label_prs: deps-update schedule: every week aiohttp-session-2.11.0/CHANGES.txt000066400000000000000000000066601417606632200165540ustar00rootroot00000000000000.. towncrier release notes start 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.11.0/LICENSE000066400000000000000000000011271417606632200157410ustar00rootroot00000000000000 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.11.0/MANIFEST.in000066400000000000000000000002341417606632200164700ustar00rootroot00000000000000include 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.11.0/Makefile000066400000000000000000000017041417606632200163750ustar00rootroot00000000000000# 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.11.0/README.rst000066400000000000000000000066171417606632200164340ustar00rootroot00000000000000aiohttp_session =============== .. image:: https://travis-ci.com/aio-libs/aiohttp-session.svg?branch=master :target: https://travis-ci.com/aio-libs/aiohttp-session .. 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 ``aioredis`` pool object, created by ``await aioredis.create_redis_pool(...)`` call. Requires ``aioredis`` library (only versions ``1.0+`` are supported):: $ 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.11.0/aiohttp_session/000077500000000000000000000000001417606632200201465ustar00rootroot00000000000000aiohttp-session-2.11.0/aiohttp_session/__init__.py000066400000000000000000000244371417606632200222710ustar00rootroot00000000000000"""User sessions for aiohttp.web.""" __version__ = "2.11.0" import abc import json import sys import time from typing import ( Any, Awaitable, Callable, Dict, Iterator, MutableMapping, Optional, Union, cast, ) from aiohttp import web Handler = Callable[[web.Request], Awaitable[web.StreamResponse]] Middleware = Callable[[web.Request, Handler], Awaitable[web.StreamResponse]] if sys.version_info >= (3, 8): from typing import TypedDict else: from typing_extensions import TypedDict 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]: # TODO: Remove explicit type anotation when aiohttp 3.8 is out cookie: Optional[str] = request.cookies.get(self._cookie_name) return cookie 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: # Ignoring type for params until aiohttp#4238 is released 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.11.0/aiohttp_session/cookie_storage.py000066400000000000000000000051061417606632200235170ustar00rootroot00000000000000import 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.11.0/aiohttp_session/log.py000066400000000000000000000000651417606632200213020ustar00rootroot00000000000000import logging log = logging.getLogger(__package__) aiohttp-session-2.11.0/aiohttp_session/memcached_storage.py000066400000000000000000000062001417606632200241500ustar00rootroot00000000000000import 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 = await self.conn.get(stored_key) # type: ignore[call-overload] if data is None: return Session(None, data=None, new=True, max_age=self.max_age) data = data.decode("utf-8") try: data = self._decoder(data) 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 = 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.11.0/aiohttp_session/nacl_storage.py000066400000000000000000000050531417606632200231640ustar00rootroot00000000000000import 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.11.0/aiohttp_session/py.typed000066400000000000000000000000001417606632200216330ustar00rootroot00000000000000aiohttp-session-2.11.0/aiohttp_session/redis_storage.py000066400000000000000000000060501417606632200233530ustar00rootroot00000000000000import json import uuid from distutils.version import StrictVersion from typing import Any, Callable, Optional from aiohttp import web from . import AbstractStorage, Session try: import aioredis except ImportError: # pragma: no cover aioredis = None # type: ignore[assignment] 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 aioredis") # May have installed aioredis separately (without aiohttp-session[aioredis]). if StrictVersion(aioredis.__version__).version < (2, 0): raise RuntimeError("aioredis<2.0 is not supported") self._key_factory = key_factory if not isinstance(redis_pool, aioredis.Redis): raise TypeError(f"Expected aioredis.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 = await self._redis.get(self.cookie_name + "_" + key) if data is None: return Session(None, data=None, new=True, max_age=self.max_age) data = data.decode("utf-8") try: data = self._decoder(data) 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 = self._encoder(self._get_session_data(session)) await self._redis.set( self.cookie_name + "_" + key, data, ex=session.max_age, # type: ignore[arg-type] ) aiohttp-session-2.11.0/codecov.yml000066400000000000000000000001011417606632200170700ustar00rootroot00000000000000coverage: status: patch: default: target: 90 aiohttp-session-2.11.0/demo/000077500000000000000000000000001417606632200156575ustar00rootroot00000000000000aiohttp-session-2.11.0/demo/flash_messages_example.py000066400000000000000000000035121417606632200227310ustar00rootroot00000000000000import base64 from typing import Awaitable, Callable, List, NoReturn, cast from aiohttp import web from cryptography import fernet from aiohttp_session import get_session, setup from aiohttp_session.cookie_storage import EncryptedCookieStorage _Handler = Callable[[web.Request], Awaitable[web.StreamResponse]] 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")) async def flash_middleware(app: web.Application, handler: _Handler) -> _Handler: async def process(request: web.Request) -> web.StreamResponse: session = await get_session(request) request["flash_incoming"] = session.pop("flash", []) response = await handler(request) session["flash"] = request.get("flash_incoming", []) + request.get( "flash_outgoing", [] ) return response return process 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)) app.router.add_get("/", handler) app.router.add_get("/flash", flash_handler) # Install flash middleware app.middlewares.append(flash_middleware) return app web.run_app(make_app()) aiohttp-session-2.11.0/demo/login_required_example.py000066400000000000000000000052271417606632200227620ustar00rootroot00000000000000import 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]] 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"] = user return await fn(request, *args, **kwargs) return wrapped @login_required async def handler(request: web.Request) -> web.Response: user = request.app["user"] 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.11.0/demo/main.py000066400000000000000000000015111417606632200171530ustar00rootroot00000000000000import 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.11.0/demo/memcached_storage.py000066400000000000000000000014031417606632200216610ustar00rootroot00000000000000import 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.11.0/demo/redis_storage.py000066400000000000000000000017261417606632200210710ustar00rootroot00000000000000import time from typing import AsyncIterator import aioredis from aiohttp import web 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( # type: ignore[no-untyped-call] redis_address, timeout=1, ) as redis: 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.11.0/docs/000077500000000000000000000000001417606632200156635ustar00rootroot00000000000000aiohttp-session-2.11.0/docs/Makefile000066400000000000000000000164251417606632200173330ustar00rootroot00000000000000# 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.11.0/docs/_static/000077500000000000000000000000001417606632200173115ustar00rootroot00000000000000aiohttp-session-2.11.0/docs/_static/aiohttp-icon-128x128.png000066400000000000000000001533251417606632200233610ustar00rootroot00000000000000‰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µQyy²žòïñ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©¦TSª‰câ˜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=NVJ+¥•Ê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èGdËï/Ùì( ‰|MºUÙ5ŒM_Ø«E01kp Ð?ï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݇¤åáfYÕÊ ,åÿË/Šä 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,Uq_Q­š_A ®[à”y4Ì,[JíZàD“¡ŠÇ¯x¶Îÿêðiwë„¡rÒ¹S3£Ä&;~+lØDŸd’79Qíä #·{ôXÉ+ùVámØ=Å Ê‘úûÙ‹…î ¡ã' €¼%›,Í`÷1ï"öÉ\@¡Ï¥#’ltINTî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ËÞ ìåÀy4­’ÚêNýµ‘É#âO݈YYyp÷®âš~Ëïµ£POÐôësësës7»ÙÍ™f±6[›­Í¦rTŽÊeUCúoÇ{±{eYAV0ŸšOͧîžîžîžz#½‘ÞÈYÙYÙù†ÊaNð¾—üÖ5¿MíñitÂña~Í9*£˜z7Û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Æö0oYßÑÍ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,&Ṟ\…`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ɬgnè†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žˆãëÿqC"^³™{4¥‡õ);A\)Ô£Íz^òøÔα2=¦ÏÙ©XLk ù ­ª©œs7ÐÇ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íqDAî£0†ñ:’õŒ‚(h¶tE8牔 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’„‰'Òiq ‹ü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 :þå·«ª‰ž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­#$qSrM ¤¥ù?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ÊXD”Å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[¬iuò,õÁ[!5þô…¨ËÕÎp:\+×’Zk'è®:ÞöAGÁ2 ­µB·B*æ8¤òAK F®ñ]à{æ$Ï3 ‘ylW1%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Ê1b6Åþ‰«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ˆ/åÅWCK”{VO2ÔÂØ€¾Ê˜ÀiÜxuö訃üü¥º×òÊÔ9sPHÞ‹§°ñ¸Ó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‡‘±ã”í0cÐx‘¹0ÝO!p _ hY¢SÇ—T‚¿qg‹;Á¦?xJ!á—*.aõóœgÃjíý ð˜b½1T:ÊëÆ¬ƒ”‘ý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÷‡Î§¬ „gY¤ò?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ÉfusVD†@(µøÍüó• €±™Ë¦Ûý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ì| …£Í‰`4Ljߙ\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—òœÖ)ªŒ¢Â Ñ€ï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è*Ý¡²®uOljȴ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ìTRÅ=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ÛhUÉ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Îáiv’¶«@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½NE7¨½P‡/á>ÎQJƒªÛßB#T•ÇÊ“æLîiæÉ¨ƒ4-6`}’훲ÇpÓ5!&ﺋƥ@àÕÎäkØ©+ÍDO”%ìô„¤|Œx”Es¼´{û£‹2Ì> †ðd•éÍBPTv˜¡˜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 {¿åKY.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¾ ?ÏÆÒeP}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©.0qMñ¼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£È‰ñ4M$¢,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< óªŒp‹Þ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›Áh2šÒPqL°9,¯·-´ÂQSÁœ qÏš*ŠA¤ÉˆÐ¶­5¹c!ÆÏ„ßGgdžè-ü”é÷„³%£Œ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ñü8xH_ ,ç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 ij41AND©»EË¢H\Å&öÚ{åYù£<-ÇñPk%¡ Ÿn.Ü‘¥¿ÉÍÏy¯y({9rL¿¸á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—ESì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úÃúÃúÃŽ­Ž­Ž­žÓžÓžÓ³"fẼÅ`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:?mDª¶Ž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‘£†GdŸ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°*¥vHí½²kŸø•^sŒÿâw¢„²S¨Ëm¹JŽ~n¾]þ¬wq ´Î:r¬&]£ñb¼ˆpN¨‚£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ÙdzgîÝ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~kOO^}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´¶)‹•ÅÊb5ÖØµÙµÙµ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Ì18Í'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ø0C·@˜¬ô‡àO@Â*È™¿ùÅ;¨‹]ªG#Jy3ì&J9N~‹yœ]Œk«#?ðŒõ<çÞWþ½Þ Ú‰¨²Å U—[%·Jn•|Í/ç÷Îïß«gÕ³êYqTGñžÀe4þ’ )š¢©h.š‹æªSuªN½š^M¯æºèºèº˜“““‘‘‘¬ÝÔnj7ïýiøà”jMæe~~ö@ßs?…ÍÓꇷVf%´«éT~ ÊUWÍÍCe8FQøŒF¢(Š*]Q#°=8…lÒ©£ÕŒµæh6Ô§•wIUëÛÇ8hõ“ÕTAÊŸô ˆHÒi¿ UL4ïp@ö·‡j×È]¡Ny² „‹ï®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•ƒê>Qt5C}Œƒæ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û“H91¼Ø'õ²ôÏ[zk ‚0úéß2÷ 87“²üÇï(§Ñ–[h§¸  ‰UX Û~ÙÛ JS]e¾¶¦ÃF€{壡þl0a‘¨É…émÙÔ˜bïEs_9å\Tw“r¿>ÚÒ5¼ÐÓ÷>àÄ?ÊeJ_’¾$}‰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 aaqðï|’ò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 Uz>êù¨çó-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¶U3PJw=ƒ´[µÜ(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íç&6M"Ó 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š&'cR.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 ¼Ïçicp\†•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Ööðdžµ“…¬"­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ÄÓˆRtŸ—Ö…¶©¯•üºí·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Æ;ãñŽG8Ñë‹õÅÚíŒ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.11.0/docs/aiohttp-icon.ico000066400000000000000000000062761417606632200207700ustar00rootroot00000000000000 ¨ ( @ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýýýùùùôôôñññððððððñññõõõùùùýýýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúúúñññîííðïîóòñôôóôôôôôôôôóôòññðïïîîóòòúúúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûûûðððïîíõôôëóøÖìù®Ýø¡Õ÷‰Î÷‚Ë÷™Ó÷¦ÙøËçùîôùõôôðïïòòòûûûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþöööîííõõôàïùªÙ÷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.11.0/docs/conf.py000066400000000000000000000255671417606632200172010ustar00rootroot00000000000000#!/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://aioredis.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.11.0/docs/glossary.rst000066400000000000000000000022651417606632200202650ustar00rootroot00000000000000.. _aiohttp-session-glossary: ========== Glossary ========== .. if you add new entries, keep the alphabetical sorting! .. glossary:: aioredis :term:`asyncio` compatible Redis client library https://aioredis.readthedocs.io/ 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.11.0/docs/index.rst000066400000000000000000000075351417606632200175360ustar00rootroot00000000000000.. 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:`aioredis` 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:`aioredis` 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.11.0/docs/make.bat000066400000000000000000000161361417606632200172770ustar00rootroot00000000000000@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.11.0/docs/reference.rst000066400000000000000000000334341417606632200203620ustar00rootroot00000000000000.. _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:`aioredis.RedisPool`. To use the storage you need setup it first:: redis = await aioredis.create_pool(('localhost', 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:`~aioredis.RedisPool` which should be created by :func:`~aioredis.create_pool` call, e.g.:: redis = await aioredis.create_pool(('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.11.0/examples/000077500000000000000000000000001417606632200165515ustar00rootroot00000000000000aiohttp-session-2.11.0/examples/postgres_storage.py000066400000000000000000000063031417606632200225170ustar00rootroot00000000000000import 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.11.0/pytest.ini000066400000000000000000000002531417606632200167640ustar00rootroot00000000000000[pytest] filterwarnings= error # This is internal to the docker library. ignore:the imp module is deprecated:DeprecationWarning ignore::DeprecationWarning aiohttp-session-2.11.0/requirements-dev.txt000066400000000000000000000007031417606632200207730ustar00rootroot00000000000000-e . aiohttp==3.8.1 aiomcache==0.7.0 aioredis==2.0.0 attrs==21.4.0 chardet==4.0.0 cryptography==36.0.1 docker==5.0.3 flake8==4.0.1 flake8-bandit==2.1.2 flake8-bugbear==22.1.11 flake8-import-order==0.18.1 flake8-requirements==1.5.2 multidict==6.0.2 mypy==0.930 pep257==0.7.0 pre-commit==2.17.0 pynacl==1.5.0 pytest==6.2.5 pytest-aiohttp==1.0.3 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-sugar==0.9.4 sphinx==4.3.2 typing-extensions==4.0.1 yarl==1.7.2 aiohttp-session-2.11.0/setup.py000066400000000000000000000034271417606632200164530ustar00rootroot00000000000000import 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.8", 'typing_extensions>=3.7.4; python_version<"3.8"'] extras_require = { "aioredis": ["aioredis>=2.0.0"], "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.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "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"], python_requires=">=3.7", install_requires=install_requires, include_package_data=True, extras_require=extras_require, ) aiohttp-session-2.11.0/tests/000077500000000000000000000000001417606632200160755ustar00rootroot00000000000000aiohttp-session-2.11.0/tests/__init__.py000066400000000000000000000000001417606632200201740ustar00rootroot00000000000000aiohttp-session-2.11.0/tests/conftest.py000066400000000000000000000133321417606632200202760ustar00rootroot00000000000000import asyncio import gc import socket import sys import time import uuid from typing import Iterator import aiomcache import aioredis import pytest from docker import DockerClient, from_env as docker_from_env, models as docker_models if sys.version_info >= (3, 8): from typing import TypedDict else: from typing_extensions import TypedDict # TODO: Remove once fixed: https://github.com/aio-libs/aioredis-py/issues/1115 aioredis.Redis.__del__ = lambda *args: None # type: ignore class _ContainerInfo(TypedDict): host: str port: int container: docker_models.containers.Container class _MemcachedParams(TypedDict): host: str port: int def unused_port() -> int: 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 event_loop() -> Iterator[asyncio.AbstractEventLoop]: loop = asyncio.new_event_loop() asyncio.set_event_loop(None) yield loop if not loop.is_closed(): loop.call_soon(loop.stop) loop.run_forever() loop.close() gc.collect() asyncio.set_event_loop(None) @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") def redis_server( # type: ignore[misc] # No docker types. docker: DockerClient, session_id: str, event_loop: asyncio.AbstractEventLoop, ) -> Iterator[_ContainerInfo]: image = "redis:{}".format("latest") asyncio.set_event_loop(event_loop) if sys.platform.startswith("darwin"): 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"): 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): try: conn = aioredis.from_url(f"redis://{host}:{port}") # type: ignore[no-untyped-call] # noqa event_loop.run_until_complete(conn.set("foo", "bar")) break except ConnectionError: time.sleep(delay) delay *= 2 finally: event_loop.run_until_complete(conn.close()) # TODO: Remove once fixed: github.com/aio-libs/aioredis-py/issues/1103 event_loop.run_until_complete(conn.connection_pool.disconnect()) else: 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 def redis( event_loop: asyncio.AbstractEventLoop, redis_url: str, ) -> Iterator[aioredis.Redis]: async def start(pool: aioredis.ConnectionPool) -> aioredis.Redis: return aioredis.Redis(connection_pool=pool) asyncio.set_event_loop(event_loop) pool = aioredis.ConnectionPool.from_url(redis_url) redis = event_loop.run_until_complete(start(pool)) yield redis event_loop.run_until_complete(redis.close()) # type: ignore[no-untyped-call] event_loop.run_until_complete(pool.disconnect()) @pytest.fixture(scope="session") def memcached_server( # type: ignore[misc] # No docker types. docker: DockerClient, session_id: str, event_loop: asyncio.AbstractEventLoop, ) -> Iterator[_ContainerInfo]: image = "memcached:{}".format("latest") if sys.platform.startswith("darwin"): 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"): 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): try: conn = aiomcache.Client(host, port) event_loop.run_until_complete(conn.set(b"foo", b"bar")) break except ConnectionRefusedError: time.sleep(delay) delay *= 2 finally: event_loop.run_until_complete(conn.close()) else: 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 def memcached( event_loop: asyncio.AbstractEventLoop, memcached_params: _MemcachedParams ) -> Iterator[aiomcache.Client]: conn = aiomcache.Client(**memcached_params) yield conn event_loop.run_until_complete(conn.close()) aiohttp-session-2.11.0/tests/test_abstract_storage.py000066400000000000000000000063531417606632200230440ustar00rootroot00000000000000import 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_session import ( Handler, 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.11.0/tests/test_cookie_storage.py000066400000000000000000000073541417606632200225140ustar00rootroot00000000000000import json import time from typing import Any, Dict, MutableMapping, cast from aiohttp import web from aiohttp.test_utils import TestClient from aiohttp_session import ( Handler, 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.11.0/tests/test_encrypted_cookie_storage.py000066400000000000000000000165771417606632200246000ustar00rootroot00000000000000import 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 cryptography.fernet import Fernet from aiohttp_session import ( Handler, 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 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 # type: ignore[comparison-overlap] 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 None text = "" if created is not None and (time.time() - created) > MAX_AGE: text += "WARNING!" return web.Response(text=text) 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("/") body = await resp.text() assert body == "" aiohttp-session-2.11.0/tests/test_get_session.py000066400000000000000000000045711417606632200220370ustar00rootroot00000000000000import 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( # type: ignore[no-untyped-def] self, request: web.Request, ): pass async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: pass 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): # type: ignore[no-untyped-def] return "" async def load_session(self, request: web.Request) -> Session: return Session(None, data=None, new=True) async def save_session( self, request: web.Request, response: web.StreamResponse, session: Session ) -> None: pass req[STORAGE_KEY] = Storage() with pytest.raises(RuntimeError): await new_session(req) aiohttp-session-2.11.0/tests/test_http_exception.py000066400000000000000000000022251417606632200225440ustar00rootroot00000000000000from typing import Tuple from aiohttp import web from aiohttp_session import ( Handler, 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.11.0/tests/test_memcached_storage.py000066400000000000000000000251201417606632200231400ustar00rootroot00000000000000import 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_session import Handler, 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) # type: ignore[call-overload] 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) # type: ignore[call-overload] 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) # type: ignore[call-overload] storage_value = json.loads(storage_value.decode("utf-8")) assert storage_value["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.11.0/tests/test_nacl_storage.py000066400000000000000000000210471417606632200221530ustar00rootroot00000000000000import 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 nacl.encoding import Base64Encoder from aiohttp_session import ( Handler, 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", None) if not session.new else None text = "" if created is not None and (time.time() - created) > MAX_AGE: text += "WARNING!" return web.Response(text=text) 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("/") body = await resp.text() assert body == "" aiohttp-session-2.11.0/tests/test_path_domain.py000066400000000000000000000112031417606632200217660ustar00rootroot00000000000000import json import time from http import cookies from typing import Any, Optional from aiohttp import web from aiohttp.test_utils import TestClient from aiohttp_session import ( Handler, 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[str] = 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.11.0/tests/test_redis_storage.py000066400000000000000000000263671417606632200223560ustar00rootroot00000000000000import asyncio import json import time import uuid from typing import Any, Callable, Dict, MutableMapping, Optional, cast import aioredis import pytest from aiohttp import web from aiohttp.test_utils import TestClient from pytest_mock import MockFixture from aiohttp_session import Handler, 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"] encoded = await redis.get("AIOHTTP_SESSION_" + key.value) s = encoded.decode("utf-8") value = json.loads(s) return value 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.StreamResponse: pass redis = aioredis.from_url(redis_url) # type: ignore[no-untyped-call] create_app(handler=handler, redis=redis) await redis.close() async def test_not_redis_provided_to_storage() -> None: async def handler(request: web.Request) -> web.StreamResponse: pass 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.StreamResponse: pass 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.StreamResponse: pass class Dummy: def __init__(self, *args: object, **kwargs: object) -> None: self.version = (0, 3) mocker.patch("aiohttp_session.redis_storage.StrictVersion", Dummy) 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.11.0/tests/test_response_types.py000066400000000000000000000034321417606632200225720ustar00rootroot00000000000000from typing import Tuple import pytest from aiohttp import web from aiohttp.test_utils import make_mocked_request from aiohttp_session import ( SESSION_KEY, Handler, 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.11.0/tests/test_session_dict.py000066400000000000000000000110241417606632200221720ustar00rootroot00000000000000import 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.11.0/tests/test_session_middleware.py000066400000000000000000000004351417606632200233700ustar00rootroot00000000000000import 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.11.0/tests/typedefs.py000066400000000000000000000002561417606632200202750ustar00rootroot00000000000000from typing import Awaitable, Callable from aiohttp import web from aiohttp.test_utils import TestClient AiohttpClient = Callable[[web.Application], Awaitable[TestClient]]