From e34af753a30eae9c636f82ff199c3d0857535a81 Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Sat, 2 Mar 2024 12:03:13 +0100 Subject: [PATCH 01/16] style(pyproject.toml): regroup configuration in pyproject.toml --- .coveragerc | 35 --- .pyup.yml | 6 - Makefile | 1 - pyproject.toml | 215 ++++++++++++++++++ .../docs.txt => requirements-docs.txt | 3 +- .../test.txt => requirements-tests.txt | 11 +- requirements.txt | 3 + requirements/ci.txt | 2 - requirements/default.txt | 3 - requirements/dev.txt | 4 - requirements/dist.txt | 11 - requirements/extras/eventlet.txt | 2 - requirements/extras/gevent.txt | 1 - requirements/extras/uvloop.txt | 1 - ruff.toml | 79 ------- setup.cfg | 72 ------ setup.py | 135 ----------- tox.ini | 8 - 18 files changed, 228 insertions(+), 364 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .pyup.yml create mode 100644 pyproject.toml rename requirements/docs.txt => requirements-docs.txt (52%) rename requirements/test.txt => requirements-tests.txt (68%) create mode 100644 requirements.txt delete mode 100644 requirements/ci.txt delete mode 100644 requirements/default.txt delete mode 100644 requirements/dev.txt delete mode 100644 requirements/dist.txt delete mode 100644 requirements/extras/eventlet.txt delete mode 100644 requirements/extras/gevent.txt delete mode 100644 requirements/extras/uvloop.txt delete mode 100644 ruff.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 8dab3b2e..00000000 --- a/.coveragerc +++ /dev/null @@ -1,35 +0,0 @@ -[run] -branch = 1 -cover_pylib = 0 -include=*mode/* -omit = tests.* - -[report] -omit = - */python?.?/* - */site-packages/* - */pypy/* - - # tested by functional tests - */mode/loop/* - - # not needed - */mode/types/* - */mode/utils/types/* - */mode/utils/mocks.py - - # been in celery since forever - */mode/utils/graphs/* -exclude_lines = - # Have to re-enable the standard pragma - if\ typing\.TYPE_CHECKING\: - - pragma: no cover - - if sys.platform == 'win32': - - \@abc\.abstractmethod - - \# Py3\.6 - - \@overload diff --git a/.pyup.yml b/.pyup.yml deleted file mode 100644 index 36e95039..00000000 --- a/.pyup.yml +++ /dev/null @@ -1,6 +0,0 @@ -# Label PRs with `deps-update` label -label_prs: deps-update - -pin: False - -schedule: every week diff --git a/Makefile b/Makefile index 571bdb20..b07b957b 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,6 @@ GIT ?= git TOX ?= tox NOSETESTS ?= nosetests ICONV ?= iconv -PYDOCSTYLE ?= pydocstyle MYPY ?= mypy SPHINX2RST ?= sphinx2rst diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..971e63ea --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,215 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "mode-streaming" +# description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +readme = "README.rst" +requires-python = ">=3.8" +license = "BSD" +keywords = ["asyncio", "service", "bootsteps", "graph", "coroutine"] +authors = [ + # { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, +] +classifiers = [ + "Framework :: AsyncIO", + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Unix", + "Environment :: No Input/Output (Daemon)", + "Framework :: AsyncIO", + "Intended Audience :: Developers", +] +dependencies = [ + "colorlog>=6.0.0,<7.0.0", + "croniter>=2.0.0,<3.0.0", + "mypy_extensions", +] +dynamic = ["version"] + +[project.optional-dependencies] +eventlet = [ + "faust-aioeventlet", + "dnspython", +] +gevent = [ + "aiogevent~=0.2", +] +uvloop = [ + "uvloop>=0.19.0", +] + + +[project.urls] +Homepage = "https://github.com/faust-streaming/mode" +Documentation = "https://faust-streaming.github.io/mode/" +Repository = "https://github.com/faust-streaming/mode" + +[tool.coverage.run] +branch = 1 +cover_pylib = 0 +include = "*mode/*" +omit = "t.*" + +[tool.pytest.ini_options] +minversion = "6.0" +python_classes = "test_*" +testpaths = [ + "t/unit", + "t/functional", +] + +[tool.coverage.report] +omit = """ + */python?.?/* + */site-packages/* + */pypy/* + + # tested by functional tests + */mode/loop/* + + # not needed + */mode/types/* + */mode/utils/types/* + */mode/utils/mocks.py + + # been in celery since forever + */mode/utils/graphs/* +""" +exclude_lines = """ + # Have to re-enable the standard pragma + if typing.TYPE_CHECKING: + + pragma: no cover + + if sys.platform == 'win32': + + @abc.abstractmethod + + # Py3.6 + + @overload +""" + +[mypy] +# --strict but not --implicit-optional +python_version = 3.8 +cache_fine_grained = true +check_untyped_defs = true +disallow_any_decorated = false +disallow_any_expr = false +disallow_any_generics = false +disallow_any_unimported = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +follow_imports = "normal" +ignore_missing_imports = true +implicit_reexport = false +no_implicit_optional = false +pretty = true +show_column_numbers = true +show_error_codes = true +show_error_context = true +strict_equality = true +strict_optional = true +warn_incomplete_stub = true +warn_no_return = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true + +[tool.ruff] +target-version = "py38" +line-length = 79 +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "examples", + "buck-out", + "build", + "dist", + "docs", + "node_modules", + "site-packages", + "venv", +] + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "C", # flake8-comprehensions + "D101",# pydocstyle: docstring in public class + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "S", # bandit + "W", # pycodestyle warnings + "RUF", # Ruff-specific warnings + "UP", # pyupgrade + "PT", # style check for pytest +] +ignore = [ + # line too long, handled by ruff format + "E501", + + # Disable bandit check of "assert" in code as they are used to fix + # mypy detection in some case + "S101", + + # Ignore: `@pytest.mark.parametrize`, expected `tuple` + "PT006", + + # Allow pytest.mark.asyncio without the parentheses + "PT023" +] + +[tool.ruff.lint.per-file-ignores] +"t/**" = ["D", "S"] + +[tool.ruff.lint.isort] +known-first-party = ["mode", "t"] +split-on-trailing-comma = false + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.format] +skip-magic-trailing-comma = true + +# Like Black +quote-style = "double" +indent-style = "space" +line-ending = "auto" diff --git a/requirements/docs.txt b/requirements-docs.txt similarity index 52% rename from requirements/docs.txt rename to requirements-docs.txt index 83f3411d..f67f9879 100644 --- a/requirements/docs.txt +++ b/requirements-docs.txt @@ -1,7 +1,8 @@ --r default.txt six sphinx<6.0.0 sphinx_celery>=1.4.8 sphinx-autodoc-annotation alabaster babel +sphinx-autobuild # TODO: check need +sphinx2rst>=1.0 # TODO: check need diff --git a/requirements/test.txt b/requirements-tests.txt similarity index 68% rename from requirements/test.txt rename to requirements-tests.txt index 0e0160fb..dda9034d 100644 --- a/requirements/test.txt +++ b/requirements-tests.txt @@ -1,12 +1,17 @@ --r default.txt --r dev.txt -hypothesis>=3.31 freezegun>=0.3.11 +hypothesis>=3.31 +mypy +pre-commit pytest-aiofiles>=0.2.0 pytest-asyncio==0.21.1 pytest-base-url>=1.4.1 +pytest-cov pytest-forked pytest-openfiles>=0.2.0 pytest-random-order>=0.5.4 +pytest-sugar # TODO: check the need pytest>=5.4.0 pytz +ruff>=0.3.0 +vulture +yarl diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..39b931ef --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +-e .[eventlet] +-r requirements-tests.txt +-r requirements-docs.txt diff --git a/requirements/ci.txt b/requirements/ci.txt deleted file mode 100644 index 1b7ac904..00000000 --- a/requirements/ci.txt +++ /dev/null @@ -1,2 +0,0 @@ --r dev.txt -pytest-cov diff --git a/requirements/default.txt b/requirements/default.txt deleted file mode 100644 index 6f086a08..00000000 --- a/requirements/default.txt +++ /dev/null @@ -1,3 +0,0 @@ -colorlog>=2.9.0 -mypy_extensions -croniter>=0.3.16 diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index b2237230..00000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,4 +0,0 @@ -ruff -mypy -yarl -pre-commit diff --git a/requirements/dist.txt b/requirements/dist.txt deleted file mode 100644 index 16058aa7..00000000 --- a/requirements/dist.txt +++ /dev/null @@ -1,11 +0,0 @@ -asyncio-ipython-magic -packaging -pydocstyle -pytest-sugar -setuptools>=36.2.0 -sphinx-autobuild -sphinx2rst>=1.0 -tox>=2.3.1 -twine -vulture -wheel>=0.29.0 diff --git a/requirements/extras/eventlet.txt b/requirements/extras/eventlet.txt deleted file mode 100644 index 86b6a1d4..00000000 --- a/requirements/extras/eventlet.txt +++ /dev/null @@ -1,2 +0,0 @@ -faust-aioeventlet -dnspython diff --git a/requirements/extras/gevent.txt b/requirements/extras/gevent.txt deleted file mode 100644 index 88d88001..00000000 --- a/requirements/extras/gevent.txt +++ /dev/null @@ -1 +0,0 @@ -aiogevent~=0.2 diff --git a/requirements/extras/uvloop.txt b/requirements/extras/uvloop.txt deleted file mode 100644 index 0d4f0750..00000000 --- a/requirements/extras/uvloop.txt +++ /dev/null @@ -1 +0,0 @@ -uvloop>=0.8.1 diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index 3d60f064..00000000 --- a/ruff.toml +++ /dev/null @@ -1,79 +0,0 @@ -target-version = "py38" -line-length = 79 -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "examples", - "buck-out", - "build", - "dist", - "docs", - "node_modules", - "site-packages", - "venv", -] - -[lint] -select = [ - "B", # flake8-bugbear - "C", # flake8-comprehensions - "D101",# pydocstyle: docstring in public class - "E", # pycodestyle errors - "F", # pyflakes - "I", # isort - "S", # bandit - "W", # pycodestyle warnings - "RUF", # Ruff-specific warnings - "UP", # pyupgrade - "PT", # style check for pytest -] -ignore = [ - # line too long, handled by ruff format - "E501", - - # Disable bandit check of "assert" in code as they are used to fix - # mypy detection in some case - "S101", - - # Ignore: `@pytest.mark.parametrize`, expected `tuple` - "PT006", - - # Allow pytest.mark.asyncio without the parentheses - "PT023" -] - -[lint.per-file-ignores] -"tests/**" = ["D", "S"] - -[lint.isort] -known-first-party = ["mode", "tests"] -split-on-trailing-comma = false - -[lint.pydocstyle] -convention = "google" - -[format] -skip-magic-trailing-comma = true - -# Like Black -quote-style = "double" -indent-style = "space" -line-ending = "auto" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b9cc9259..00000000 --- a/setup.cfg +++ /dev/null @@ -1,72 +0,0 @@ -[metadata] -name = mode-streaming -version = attr: mode.__version__ -author = attr: mode.__author__ -author_email = attr: mode.__contact__ -url = attr: mode.__homepage__ -description = attr: mode.__doc__ -long_description = file: README.rst -keywords = asyncio, service, bootsteps, graph, coroutine -license = BSD 3-Clause License -license_file = LICENSE -classifiers = - Framework :: AsyncIO - Development Status :: 5 - Production/Stable - License :: OSI Approved :: BSD License - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Operating System :: POSIX - Operating System :: Microsoft :: Windows - Operating System :: MacOS :: MacOS X - Operating System :: Unix - Environment :: No Input/Output (Daemon) - Intended Audience :: Developers - -[options] -zip_safe = False -include_package_data = True -packages = find: - -[tool:pytest] -minversion=2.8 -testpaths = tests/unit tests/functional -python_classes = test_* - -[wheel] -universal = 1 - -[mypy] -# --strict but not --implicit-optional -python_version = 3.8 -cache_fine_grained = True -check_untyped_defs = True -disallow_any_decorated = False -disallow_any_expr = False -disallow_any_generics = False -disallow_any_unimported = True -disallow_incomplete_defs = True -disallow_subclassing_any = True -disallow_untyped_calls = True -disallow_untyped_decorators = True -disallow_untyped_defs = True -follow_imports = normal -ignore_missing_imports = True -implicit_reexport = False -no_implicit_optional = False -pretty = True -show_column_numbers = True -show_error_codes = True -show_error_context = True -strict_equality = True -strict_optional = True -warn_incomplete_stub = True -warn_no_return = True -warn_redundant_casts = True -warn_return_any = True -warn_unreachable = True -warn_unused_configs = True -warn_unused_ignores = True diff --git a/setup.py b/setup.py deleted file mode 100644 index 2143c9de..00000000 --- a/setup.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python - -import re - -from setuptools import find_packages, setup - -NAME = "mode-streaming" -EXTENSIONS = {"eventlet", "gevent", "uvloop"} - -from pathlib import Path # noqa - -README = Path("README.rst") - -# -*- Classifiers -*- - -classes = """ - Development Status :: 5 - Production/Stable - License :: OSI Approved :: BSD License - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Operating System :: POSIX - Operating System :: Microsoft :: Windows - Operating System :: MacOS :: MacOS X - Operating System :: Unix - Environment :: No Input/Output (Daemon) - Framework :: AsyncIO - Intended Audience :: Developers -""" -classifiers = [s.strip() for s in classes.split("\n") if s] - -# -*- Distribution Meta -*- - -re_meta = re.compile(r"__(\w+?)__\s*=\s*(.*)") -re_doc = re.compile(r'^"""(.+?)"""') - - -def add_default(m): - attr_name, attr_value = m.groups() - return ((attr_name, attr_value.strip("\"'")),) - - -def add_doc(m): - return (("doc", m.groups()[0]),) - - -pats = {re_meta: add_default, re_doc: add_doc} -here = Path(__file__).parent.absolute() -with open(here / "mode" / "__init__.py") as meta_fh: - meta = {} - for line in meta_fh: - if line.strip() == "# -eof meta-": - break - for pattern, handler in pats.items(): - m = pattern.match(line.strip()) - if m: - meta.update(handler(m)) - -# -*- Installation Requires -*- - - -def strip_comments(line): - return line.split("#", 1)[0].strip() - - -def _pip_requirement(req): - if req.startswith("-r "): - _, path = req.split() - return reqs(*path.split("/")) - return [req] - - -def _reqs(*f): - path = (Path.cwd() / "requirements").joinpath(*f) - reqs = (strip_comments(line) for line in path.open().readlines()) - return [_pip_requirement(r) for r in reqs if r] - - -def reqs(*f): - return [req for subreq in _reqs(*f) for req in subreq] - - -def extras(*p): - """Parse requirement in the requirements/extras/ directory.""" - return reqs("extras", *p) - - -def extras_require(): - """Get map of all extra requirements.""" - return {x: extras(x + ".txt") for x in EXTENSIONS} - - -# -*- Long Description -*- - - -if README.exists(): - long_description = README.read_text(encoding="utf-8") -else: - long_description = f"See http://pypi.org/project/{NAME}" - -# -*- %%% -*- - -packages = find_packages( - exclude=["t", "t.*", "docs", "docs.*", "examples", "examples.*"] -) -assert not any(package.startswith("t.") for package in packages) - - -setup( - name=NAME, - use_scm_version=True, - setup_requires=["setuptools_scm"], - description=meta["doc"], - author=meta["author"], - author_email=meta["contact"], - url=meta["homepage"], - platforms=["any"], - license="BSD", - keywords="asyncio service bootsteps graph coroutine", - packages=packages, - include_package_data=True, - # PEP-561: https://www.python.org/dev/peps/pep-0561/ - package_data={"mode": ["py.typed"]}, - zip_safe=False, - install_requires=reqs("default.txt"), - tests_require=reqs("test.txt"), - extras_require=extras_require(), - python_requires=">=3.8", - classifiers=classifiers, - long_description=long_description, - long_description_content_type="text/x-rst", -) diff --git a/tox.ini b/tox.ini index 63b6b746..1ad5241f 100644 --- a/tox.ini +++ b/tox.ini @@ -35,11 +35,3 @@ commands = [testenv:typecheck] commands = mypy -p mode - -[testenv:docstyle] -commands = - pydocstyle mode - -[testenv:bandit] -commands = - bandit -c extra/bandit/config.yml -b extra/bandit/baseline.json -r mode From 09f9f3965ce34648aac40961371d4344b606b02f Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Sat, 2 Mar 2024 12:08:52 +0100 Subject: [PATCH 02/16] style(pyproject.toml): regroup configuration in pyproject.toml --- .gitignore | 1 + MANIFEST.in | 16 ------- Makefile | 7 ++- README.rst | 4 +- docs/includes/installation.txt | 4 +- mode/__init__.py | 47 ++------------------ pyproject.toml | 78 +++++++++++++++++----------------- requirements-tests.txt | 18 ++++---- scripts/README.md | 3 +- scripts/build | 2 +- scripts/check | 2 +- scripts/clean | 8 +++- scripts/format | 11 +++++ scripts/install | 19 --------- scripts/lint | 12 ------ scripts/{tests => test} | 6 +-- tests/functional/test_mode.py | 1 - 17 files changed, 79 insertions(+), 160 deletions(-) delete mode 100644 MANIFEST.in create mode 100755 scripts/format delete mode 100755 scripts/install delete mode 100755 scripts/lint rename scripts/{tests => test} (59%) diff --git a/.gitignore b/.gitignore index 6a86bd48..003553f3 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,7 @@ Session.vim tags .pytest_cache/ .ipython/ +.ruff_cache/ ### PyCharm .idea diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 28070d7d..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,16 +0,0 @@ -include Changelog -include LICENSE -include README.rst -include MANIFEST.in -include setup.cfg -include setup.py -recursive-include docs * -recursive-include extra/* -recursive-include examples * -recursive-include tests * -recursive-include mode *.py *.html *.typed -recursive-include requirements *.txt *.rst - -recursive-exclude * __pycache__ -recursive-exclude * *.py[co] -recursive-exclude * .*.sw[a-z] diff --git a/Makefile b/Makefile index b07b957b..140782a3 100644 --- a/Makefile +++ b/Makefile @@ -51,8 +51,7 @@ clean: clean-docs clean-pyc clean-build clean-dist: clean clean-git-force release: - $(PYTHON) setup.py register sdist bdist_wheel upload --sign --identity="$(PGPIDENT)" - + $(PYTHON) register sdist bdist_wheel upload --sign --identity="$(PGPIDENT)" . PHONY: deps-default deps-default: @@ -77,7 +76,7 @@ deps-extras: . PHONY: develop develop: deps-default deps-dist deps-docs deps-test deps-extras - $(PYTHON) setup.py develop + $(PYTHON) develop . PHONY: Documentation Documentation: @@ -151,7 +150,7 @@ cov: $(PYTEST) -x --cov="$(PROJ)" --cov-report=html build: - $(PYTHON) setup.py sdist bdist_wheel + $(PYTHON) sdist bdist_wheel distcheck: lint test clean diff --git a/README.rst b/README.rst index 132fa5c6..b7ec20c5 100644 --- a/README.rst +++ b/README.rst @@ -298,8 +298,8 @@ You can install it by doing the following: $ tar xvfz mode-streaming-0.2.1.tar.gz $ cd mode-0.2.1 - $ python setup.py build - # python setup.py install + $ python build + # python install The last command must be executed as a privileged user if you are not currently using a virtualenv. diff --git a/docs/includes/installation.txt b/docs/includes/installation.txt index f53d9414..2827492a 100644 --- a/docs/includes/installation.txt +++ b/docs/includes/installation.txt @@ -22,8 +22,8 @@ You can install it by doing the following:: $ tar xvfz mode-streaming-0.1.0.tar.gz $ cd mode-streaming-0.1.0 - $ python setup.py build - # python setup.py install + $ python build + # python install The last command must be executed as a privileged user if you are not currently using a virtualenv. diff --git a/mode/__init__.py b/mode/__init__.py index 8e2a8d5c..44afd6ea 100644 --- a/mode/__init__.py +++ b/mode/__init__.py @@ -1,50 +1,18 @@ """AsyncIO Service-based programming.""" -# :copyright: (c) 2017-2020, Robinhood Markets -# (c) 2020-2022, faust-streaming Org -# All rights reserved. -# :license: BSD (3 Clause), see LICENSE for more details. -import re +__version__ = "0.0.1" + import sys import typing -from importlib.metadata import version # Lazy loading. # - See werkzeug/__init__.py for the rationale behind this. from types import ModuleType -from typing import Any, Mapping, NamedTuple, Sequence - -__version__ = version("mode-streaming") -__author__ = "Faust Streaming" -__contact__ = "vpatki@wayfair.com, williambbarnhart@gmail.com" -__homepage__ = "https://github.com/faust-streaming/mode" -__docformat__ = "restructuredtext" +from typing import Any, Mapping, Sequence # -eof meta- -class version_info_t(NamedTuple): - major: int - minor: int - micro: int - releaselevel: str - serial: str - - -# bumpversion can only search for {current_version} -# so we have to parse the version here. -_match = re.match(r"(\d+)\.(\d+).(\d+)(.+)?", __version__) -if _match is None: # pragma: no cover - raise RuntimeError("MODE VERSION HAS ILLEGAL FORMAT") -_temp = _match.groups() -VERSION = version_info = version_info_t( - int(_temp[0]), int(_temp[1]), int(_temp[2]), _temp[3] or "", "" -) -del _match -del _temp -del re - - if typing.TYPE_CHECKING: # pragma: no cover from .services import Service, task, timer from .signals import BaseSignal, Signal, SyncSignal @@ -142,10 +110,8 @@ def __dir__(self) -> Sequence[str]: "__name__", "__path__", "VERSION", - "version_info_t", "version_info", "__package__", - "__version__", "__author__", "__contact__", "__homepage__", @@ -166,13 +132,6 @@ def __dir__(self) -> Sequence[str]: "__doc__": __doc__, "__all__": tuple(object_origins), "__version__": __version__, - "__author__": __author__, - "__contact__": __contact__, - "__homepage__": __homepage__, - "__docformat__": __docformat__, "__package__": __package__, - "version_info_t": version_info_t, - "version_info": version_info, - "VERSION": VERSION, } ) diff --git a/pyproject.toml b/pyproject.toml index 971e63ea..0ce72e94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,18 @@ [build-system] -requires = ["setuptools", "setuptools-scm"] +requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" +[tool.setuptools.package-data] +"modeStreaming" = ["py.typed"] + [project] name = "mode-streaming" -# description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +description = "AsyncIO Service-based programming" readme = "README.rst" requires-python = ">=3.8" -license = "BSD" keywords = ["asyncio", "service", "bootsteps", "graph", "coroutine"] authors = [ - # { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, + { name = "Faust Streaming", email = "williambbarnhart@gmail.com" }, ] classifiers = [ "Framework :: AsyncIO", @@ -49,61 +51,57 @@ uvloop = [ "uvloop>=0.19.0", ] - [project.urls] Homepage = "https://github.com/faust-streaming/mode" Documentation = "https://faust-streaming.github.io/mode/" Repository = "https://github.com/faust-streaming/mode" -[tool.coverage.run] -branch = 1 -cover_pylib = 0 -include = "*mode/*" -omit = "t.*" - [tool.pytest.ini_options] minversion = "6.0" python_classes = "test_*" testpaths = [ - "t/unit", - "t/functional", + "tests/unit", + "tests/functional", ] -[tool.coverage.report] -omit = """ - */python?.?/* - */site-packages/* - */pypy/* +[tool.coverage.run] +parallel = true +context = '${CONTEXT}' +source = [ + "mode", + "tests/*" +] +omit = [ + "venv/*", + "tests/*", # tested by functional tests - */mode/loop/* + "mode/loop/*", # not needed - */mode/types/* - */mode/utils/types/* - */mode/utils/mocks.py + "mode/types/*", + "mode/utils/types/*", + "mode/utils/mocks.py", # been in celery since forever - */mode/utils/graphs/* -""" -exclude_lines = """ - # Have to re-enable the standard pragma - if typing.TYPE_CHECKING: - - pragma: no cover - - if sys.platform == 'win32': - - @abc.abstractmethod - - # Py3.6 + "mode/utils/graphs/*", +] +branch = true - @overload -""" +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "@abstractmethod", + "@abc.abstractmethod", + "if sys.platform == 'win32':", + "if typing.TYPE_CHECKING:", + "if TYPE_CHECKING:" +] +fail_under = 93 +show_missing = true -[mypy] +[tool.mypy] # --strict but not --implicit-optional -python_version = 3.8 cache_fine_grained = true check_untyped_defs = true disallow_any_decorated = false @@ -197,7 +195,7 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -"t/**" = ["D", "S"] +"tests/**" = ["D", "S"] [tool.ruff.lint.isort] known-first-party = ["mode", "t"] diff --git a/requirements-tests.txt b/requirements-tests.txt index dda9034d..ac161e9c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,16 +1,16 @@ freezegun>=0.3.11 hypothesis>=3.31 -mypy -pre-commit +mypy>=1.8.0 +pre-commit>=3.6.2 pytest-aiofiles>=0.2.0 pytest-asyncio==0.21.1 -pytest-base-url>=1.4.1 -pytest-cov -pytest-forked -pytest-openfiles>=0.2.0 -pytest-random-order>=0.5.4 -pytest-sugar # TODO: check the need -pytest>=5.4.0 +pytest-base-url>=2.1.0 +pytest-cov>=4.1.0 +pytest-forked>=1.6.0 +pytest-openfiles>=0.5.0 +pytest-random-order>=1.1.1 +pytest-sugar>=1.0.0 # TODO: check the need +pytest>=8.0.2 pytz ruff>=0.3.0 vulture diff --git a/scripts/README.md b/scripts/README.md index e52246c9..2ba18d56 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,8 +1,7 @@ # Development Scripts -* `scripts/install` - Install dependencies in a virtual environment. * `scripts/test` - Run the test suite. -* `scripts/lint` - Run the automated code linting/formatting tools. +* `scripts/format` - Run the automated code linting/formatting tools. * `scripts/check` - Run the code linting, checking that it passes. * `scripts/build` - Build source and wheel packages. * `scripts/publish` - Publish the latest version to PyPI. diff --git a/scripts/build b/scripts/build index 7d327b5c..5d5e71a1 100755 --- a/scripts/build +++ b/scripts/build @@ -8,6 +8,6 @@ fi set -x -${PREFIX}python setup.py sdist bdist_wheel +${PREFIX}python -m build ${PREFIX}twine check dist/* # ${PREFIX}mkdocs build diff --git a/scripts/check b/scripts/check index f95577c6..67409901 100755 --- a/scripts/check +++ b/scripts/check @@ -4,7 +4,7 @@ export PREFIX="" if [ -d 'venv' ] ; then export PREFIX="venv/bin/" fi -export SOURCE_FILES="mode tests setup.py" +export SOURCE_FILES="mode tests" set -x diff --git a/scripts/clean b/scripts/clean index 46a9b508..7f058f7f 100755 --- a/scripts/clean +++ b/scripts/clean @@ -9,9 +9,13 @@ fi if [ -d 'htmlcov' ] ; then rm -r htmlcov fi -if [ -d 'faust.egg-info' ] ; then - rm -r mode.egg-info +if [ -d 'mode_streaming.egg-info' ] ; then + rm -r mode_streaming.egg-info fi +# delete python cache +find . -iname '*.pyc' -delete +find . -iname '__pycache__' -delete + rm -rf .coverage.* rm -rf *.logs diff --git a/scripts/format b/scripts/format new file mode 100755 index 00000000..f305fc43 --- /dev/null +++ b/scripts/format @@ -0,0 +1,11 @@ +#!/bin/sh -e + +export PREFIX="" +if [ -d 'venv' ] ; then + export PREFIX="venv/bin/" +fi + +set -x + +${PREFIX}ruff format mode tests +${PREFIX}ruff check mode tests --fix diff --git a/scripts/install b/scripts/install deleted file mode 100755 index bb56162d..00000000 --- a/scripts/install +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -e - -# Use the Python executable provided from the `-p` option, or a default. -[ "$1" = "-p" ] && PYTHON=$2 || PYTHON="python3" - -REQUIREMENTS="requirements/test.txt" -VENV="venv" - -set -x - -if [ -z "$GITHUB_ACTIONS" ]; then - "$PYTHON" -m venv "$VENV" - PIP="$VENV/bin/pip" -else - PIP="pip" -fi - -"$PIP" install -r "$REQUIREMENTS" -"$PIP" install -e . diff --git a/scripts/lint b/scripts/lint deleted file mode 100755 index 3c03a237..00000000 --- a/scripts/lint +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -e - -export PREFIX="" -if [ -d 'venv' ] ; then - export PREFIX="venv/bin/" -fi -export SOURCE_FILES="mode tests setup.py" - -set -x - -${PREFIX}ruff check $SOURCE_FILES --fix -${PREFIX}ruff format $SOURCE_FILES diff --git a/scripts/tests b/scripts/test similarity index 59% rename from scripts/tests rename to scripts/test index b4d76185..e2fdcb29 100755 --- a/scripts/tests +++ b/scripts/test @@ -11,8 +11,4 @@ if [ -z $GITHUB_ACTIONS ]; then scripts/check fi -${PREFIX}pytest tests/unit tests/functional - -if [ -z $GITHUB_ACTIONS ]; then - scripts/coverage -fi +${PREFIX}pytest tests/unit tests/functional --cov=mode diff --git a/tests/functional/test_mode.py b/tests/functional/test_mode.py index 4943e5ae..b306f946 100644 --- a/tests/functional/test_mode.py +++ b/tests/functional/test_mode.py @@ -3,4 +3,3 @@ def test_dir(): assert dir(mode) - assert "__version__" in dir(mode) From b91e8013711c45c20fe553d6450d0d75abfdc052 Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Wed, 6 Mar 2024 23:30:32 +0100 Subject: [PATCH 03/16] docs(mkdocs): setup mkdocs and switch docstring style --- Makefile | 1 - docs/Makefile | 233 --------------- docs/_static/.keep | 0 docs/_templates/.keep | 0 docs/changelog.rst | 1 - docs/conf.py | 59 ---- docs/copyright.rst | 28 -- docs/faq.rst | 7 - docs/glossary.rst | 12 - docs/images/favicon.ico | Bin 1150 -> 0 bytes docs/images/logo.png | Bin 105363 -> 0 bytes docs/images/tutorial_graph.png | Bin 21770 -> 0 bytes docs/includes/code-of-conduct.txt | 43 --- docs/includes/faq.txt | 130 -------- docs/includes/installation.txt | 42 --- docs/{includes/introduction.txt => index.md} | 82 +++-- docs/index.rst | 32 -- docs/introduction.rst | 11 - docs/make.bat | 272 ----------------- docs/reference/index.rst | 75 ----- docs/reference/mode.debug.rst | 11 - docs/reference/mode.exceptions.rst | 11 - docs/reference/mode.locals.rst | 11 - docs/reference/mode.loop.eventlet.rst | 12 - docs/reference/mode.loop.gevent.rst | 12 - docs/reference/mode.loop.rst | 11 - docs/reference/mode.loop.uvloop.rst | 12 - docs/reference/mode.proxy.rst | 11 - docs/reference/mode.rst | 11 - docs/reference/mode.services.rst | 11 - docs/reference/mode.signals.rst | 11 - docs/reference/mode.supervisors.rst | 11 - docs/reference/mode.threads.rst | 11 - docs/reference/mode.timers.rst | 11 - docs/reference/mode.types.rst | 11 - docs/reference/mode.types.services.rst | 11 - docs/reference/mode.types.signals.rst | 11 - docs/reference/mode.types.supervisors.rst | 11 - docs/reference/mode.utils.aiter.rst | 11 - docs/reference/mode.utils.collections.rst | 11 - docs/reference/mode.utils.compat.rst | 11 - docs/reference/mode.utils.contexts.rst | 11 - docs/reference/mode.utils.cron.rst | 11 - docs/reference/mode.utils.futures.rst | 11 - docs/reference/mode.utils.graphs.rst | 11 - docs/reference/mode.utils.imports.rst | 11 - docs/reference/mode.utils.locals.rst | 11 - docs/reference/mode.utils.locks.rst | 11 - docs/reference/mode.utils.logging.rst | 11 - docs/reference/mode.utils.loops.rst | 11 - docs/reference/mode.utils.mocks.rst | 11 - docs/reference/mode.utils.objects.rst | 11 - docs/reference/mode.utils.queues.rst | 11 - docs/reference/mode.utils.text.rst | 11 - docs/reference/mode.utils.times.rst | 11 - docs/reference/mode.utils.tracebacks.rst | 11 - docs/reference/mode.utils.trees.rst | 11 - docs/reference/mode.utils.types.graphs.rst | 11 - docs/reference/mode.utils.types.trees.rst | 11 - docs/reference/mode.worker.rst | 11 - docs/references/mode.debug.md | 3 + docs/references/mode.exceptions.md | 3 + docs/references/mode.locals.md | 3 + docs/references/mode.loop.eventlet.md | 8 + docs/references/mode.loop.gevent.md | 8 + docs/references/mode.loop.md | 3 + docs/references/mode.loop.uvloop.md | 8 + docs/references/mode.md | 3 + docs/references/mode.proxy.md | 3 + docs/references/mode.services.md | 3 + docs/references/mode.signals.md | 3 + docs/references/mode.supervisors.md | 3 + docs/references/mode.threads.md | 3 + docs/references/mode.timers.md | 3 + docs/references/mode.types.md | 3 + docs/references/mode.types.services.md | 3 + docs/references/mode.types.signals.md | 3 + docs/references/mode.types.supervisors.md | 3 + docs/references/mode.utils.aiter.md | 3 + docs/references/mode.utils.collections.md | 3 + docs/references/mode.utils.compat.md | 3 + docs/references/mode.utils.contexts.md | 3 + docs/references/mode.utils.cron.md | 3 + docs/references/mode.utils.futures.md | 3 + docs/references/mode.utils.graphs.md | 3 + docs/references/mode.utils.imports.md | 3 + docs/references/mode.utils.locals.md | 3 + docs/references/mode.utils.locks.md | 3 + docs/references/mode.utils.logging.md | 3 + docs/references/mode.utils.loops.md | 3 + docs/references/mode.utils.mocks.md | 3 + docs/references/mode.utils.objects.md | 3 + docs/references/mode.utils.queues.md | 3 + docs/references/mode.utils.text.md | 3 + docs/references/mode.utils.times.md | 3 + docs/references/mode.utils.tracebacks.md | 3 + docs/references/mode.utils.trees.md | 3 + docs/references/mode.utils.types.graphs.md | 3 + docs/references/mode.utils.types.trees.md | 3 + docs/references/mode.worker.md | 3 + docs/templates/readme.txt | 36 --- docs/userguide/index.rst | 13 - docs/userguide/services.rst | 249 ---------------- mkdocs.yml | 98 ++++++ mode/__init__.py | 4 - mode/locals.py | 75 +++-- mode/loop/__init__.py | 66 +++-- mode/proxy.py | 13 +- mode/services.py | 296 ++++++++++--------- mode/signals.py | 20 +- mode/utils/aiter.py | 21 +- mode/utils/collections.py | 8 +- mode/utils/futures.py | 16 +- mode/utils/imports.py | 111 ++++--- mode/utils/locals.py | 6 +- mode/utils/logging.py | 123 ++++---- mode/utils/mocks.py | 29 +- mode/utils/objects.py | 137 +++++---- mode/utils/queues.py | 34 ++- mode/utils/text.py | 27 +- mode/utils/times.py | 41 +-- pyproject.toml | 2 +- 122 files changed, 844 insertions(+), 2187 deletions(-) delete mode 100644 docs/Makefile delete mode 100644 docs/_static/.keep delete mode 100644 docs/_templates/.keep delete mode 100644 docs/changelog.rst delete mode 100644 docs/conf.py delete mode 100644 docs/copyright.rst delete mode 100644 docs/faq.rst delete mode 100644 docs/glossary.rst delete mode 100644 docs/images/favicon.ico delete mode 100644 docs/images/logo.png delete mode 100644 docs/images/tutorial_graph.png delete mode 100644 docs/includes/code-of-conduct.txt delete mode 100644 docs/includes/faq.txt delete mode 100644 docs/includes/installation.txt rename docs/{includes/introduction.txt => index.md} (86%) delete mode 100644 docs/index.rst delete mode 100644 docs/introduction.rst delete mode 100644 docs/make.bat delete mode 100644 docs/reference/index.rst delete mode 100644 docs/reference/mode.debug.rst delete mode 100644 docs/reference/mode.exceptions.rst delete mode 100644 docs/reference/mode.locals.rst delete mode 100644 docs/reference/mode.loop.eventlet.rst delete mode 100644 docs/reference/mode.loop.gevent.rst delete mode 100644 docs/reference/mode.loop.rst delete mode 100644 docs/reference/mode.loop.uvloop.rst delete mode 100644 docs/reference/mode.proxy.rst delete mode 100644 docs/reference/mode.rst delete mode 100644 docs/reference/mode.services.rst delete mode 100644 docs/reference/mode.signals.rst delete mode 100644 docs/reference/mode.supervisors.rst delete mode 100644 docs/reference/mode.threads.rst delete mode 100644 docs/reference/mode.timers.rst delete mode 100644 docs/reference/mode.types.rst delete mode 100644 docs/reference/mode.types.services.rst delete mode 100644 docs/reference/mode.types.signals.rst delete mode 100644 docs/reference/mode.types.supervisors.rst delete mode 100644 docs/reference/mode.utils.aiter.rst delete mode 100644 docs/reference/mode.utils.collections.rst delete mode 100644 docs/reference/mode.utils.compat.rst delete mode 100644 docs/reference/mode.utils.contexts.rst delete mode 100644 docs/reference/mode.utils.cron.rst delete mode 100644 docs/reference/mode.utils.futures.rst delete mode 100644 docs/reference/mode.utils.graphs.rst delete mode 100644 docs/reference/mode.utils.imports.rst delete mode 100644 docs/reference/mode.utils.locals.rst delete mode 100644 docs/reference/mode.utils.locks.rst delete mode 100644 docs/reference/mode.utils.logging.rst delete mode 100644 docs/reference/mode.utils.loops.rst delete mode 100644 docs/reference/mode.utils.mocks.rst delete mode 100644 docs/reference/mode.utils.objects.rst delete mode 100644 docs/reference/mode.utils.queues.rst delete mode 100644 docs/reference/mode.utils.text.rst delete mode 100644 docs/reference/mode.utils.times.rst delete mode 100644 docs/reference/mode.utils.tracebacks.rst delete mode 100644 docs/reference/mode.utils.trees.rst delete mode 100644 docs/reference/mode.utils.types.graphs.rst delete mode 100644 docs/reference/mode.utils.types.trees.rst delete mode 100644 docs/reference/mode.worker.rst create mode 100644 docs/references/mode.debug.md create mode 100644 docs/references/mode.exceptions.md create mode 100644 docs/references/mode.locals.md create mode 100644 docs/references/mode.loop.eventlet.md create mode 100644 docs/references/mode.loop.gevent.md create mode 100644 docs/references/mode.loop.md create mode 100644 docs/references/mode.loop.uvloop.md create mode 100644 docs/references/mode.md create mode 100644 docs/references/mode.proxy.md create mode 100644 docs/references/mode.services.md create mode 100644 docs/references/mode.signals.md create mode 100644 docs/references/mode.supervisors.md create mode 100644 docs/references/mode.threads.md create mode 100644 docs/references/mode.timers.md create mode 100644 docs/references/mode.types.md create mode 100644 docs/references/mode.types.services.md create mode 100644 docs/references/mode.types.signals.md create mode 100644 docs/references/mode.types.supervisors.md create mode 100644 docs/references/mode.utils.aiter.md create mode 100644 docs/references/mode.utils.collections.md create mode 100644 docs/references/mode.utils.compat.md create mode 100644 docs/references/mode.utils.contexts.md create mode 100644 docs/references/mode.utils.cron.md create mode 100644 docs/references/mode.utils.futures.md create mode 100644 docs/references/mode.utils.graphs.md create mode 100644 docs/references/mode.utils.imports.md create mode 100644 docs/references/mode.utils.locals.md create mode 100644 docs/references/mode.utils.locks.md create mode 100644 docs/references/mode.utils.logging.md create mode 100644 docs/references/mode.utils.loops.md create mode 100644 docs/references/mode.utils.mocks.md create mode 100644 docs/references/mode.utils.objects.md create mode 100644 docs/references/mode.utils.queues.md create mode 100644 docs/references/mode.utils.text.md create mode 100644 docs/references/mode.utils.times.md create mode 100644 docs/references/mode.utils.tracebacks.md create mode 100644 docs/references/mode.utils.trees.md create mode 100644 docs/references/mode.utils.types.graphs.md create mode 100644 docs/references/mode.utils.types.trees.md create mode 100644 docs/references/mode.worker.md delete mode 100644 docs/templates/readme.txt delete mode 100644 docs/userguide/index.rst delete mode 100644 docs/userguide/services.rst create mode 100644 mkdocs.yml diff --git a/Makefile b/Makefile index 140782a3..15309713 100644 --- a/Makefile +++ b/Makefile @@ -80,7 +80,6 @@ develop: deps-default deps-dist deps-docs deps-test deps-extras . PHONY: Documentation Documentation: - $(PIP) install -r requirements/docs.txt (cd "$(SPHINX_DIR)"; $(MAKE) html) mv "$(SPHINX_HTMLDIR)" $(DOCUMENTATION) diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 602941ab..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,233 +0,0 @@ -# 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 -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 " epub3 to make an epub3" - @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)" - @echo " apicheck to verify that all modules are present in autodoc" - @echo " spelling to run a spell checker on the documentation" - -.PHONY: clean -clean: - rm -rf $(BUILDDIR)/* - -.PHONY: html -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -.PHONY: dirhtml -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.PHONY: singlehtml -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -.PHONY: pickle -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -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." - -.PHONY: qthelp -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/PROJ.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PROJ.qhc" - -.PHONY: applehelp -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." - -.PHONY: devhelp -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/PROJ" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PROJ" - @echo "# devhelp" - -.PHONY: epub -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: epub3 -epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 - @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." - -.PHONY: latex -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)." - -.PHONY: latexpdf -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." - -.PHONY: latexpdfja -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." - -.PHONY: text -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -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)." - -.PHONY: info -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." - -.PHONY: gettext -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -.PHONY: linkcheck -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." - -.PHONY: doctest -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -.PHONY: coverage -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -.PHONY: apicheck -apicheck: - $(SPHINXBUILD) -b apicheck $(ALLSPHINXOPTS) $(BUILDDIR)/apicheck - -.PHONY: spelling -spelling: - SPELLCHECK=1 $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling - -.PHONY: xml -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -.PHONY: pseudoxml -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_static/.keep b/docs/_static/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/_templates/.keep b/docs/_templates/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index 5b20da33..00000000 --- a/docs/changelog.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../Changelog diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 89eb765f..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,59 +0,0 @@ -import sys -from contextlib import suppress - -from sphinx_celery import conf - -sys.path.append(".") - -extensions = [] - -globals().update( - conf.build_config( - "mode", - __file__, - project="Mode", - # version_dev='2.0', - # version_stable='1.4', - canonical_url="https://faust-streaming.github.io/mode/", - webdomain="", - github_project="faust-streaming/mode", - copyright="2017-2020", - html_logo="images/logo.png", - html_favicon="images/favicon.ico", - html_prepend_sidebars=[], - include_intersphinx={"python", "sphinx"}, - extra_extensions=[ - "sphinx.ext.napoleon", - "sphinx_autodoc_annotation", - "alabaster", - ], - extra_intersphinx_mapping={}, - # django_settings='testproj.settings', - # from pathlib import Path - # path_additions=[Path.cwd().parent / 'testproj'] - apicheck_ignore_modules=[ - "mode.loop.eventlet", - "mode.loop.gevent", - "mode.loop.uvloop", - "mode.loop._gevent_loop", - "mode.utils", - "mode.utils.graphs.formatter", - "mode.utils.graphs.graph", - "mode.utils.types", - ], - ) -) - -html_theme = "alabaster" -html_sidebars = {} -templates_path = ["_templates"] - -autodoc_member_order = "bysource" - -pygments_style = "sphinx" - -# This option is deprecated and raises an error. -with suppress(NameError): - del html_use_smartypants # noqa - -extensions.remove("sphinx.ext.viewcode") diff --git a/docs/copyright.rst b/docs/copyright.rst deleted file mode 100644 index 72ba9c2d..00000000 --- a/docs/copyright.rst +++ /dev/null @@ -1,28 +0,0 @@ -Copyright -========= - -*Mode User Manual* - -by Ask Solem - -.. |copy| unicode:: U+000A9 .. COPYRIGHT SIGN - -Copyright |copy| 2016, Ask Solem - -All rights reserved. This material may be copied or distributed only -subject to the terms and conditions set forth in the `Creative Commons -Attribution-ShareAlike 4.0 International` -`_ license. - -You may share and adapt the material, even for commercial purposes, but -you must give the original author credit. -If you alter, transform, or build upon this -work, you may distribute the resulting work only under the same license or -a license compatible to this one. - -.. note:: - - While the Mode *documentation* is offered under the - Creative Commons *Attribution-ShareAlike 4.0 International* license - the Mode *software* is offered under the - `BSD License (3 Clause) `_ diff --git a/docs/faq.rst b/docs/faq.rst deleted file mode 100644 index 3d293e14..00000000 --- a/docs/faq.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. _faq: - -================================== - FAQ: Frequently Asked Questions -================================== - -.. include:: includes/faq.txt diff --git a/docs/glossary.rst b/docs/glossary.rst deleted file mode 100644 index 9bbb6005..00000000 --- a/docs/glossary.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. _glossary: - -Glossary -======== - -.. glossary:: - :sorted: - - thread safe - A function or process that is thread safe means that multiple POSIX - threads can execute it in parallel without race conditions or deadlock - situations. diff --git a/docs/images/favicon.ico b/docs/images/favicon.ico deleted file mode 100644 index cfb6545e26ff3e22c2e635619e1d1af0f9a6b3f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmb7EO-oxr6rF?^LROYQi<=mOP;r+or0!f4#C?83ATF|Ukxd|>3s-*jgBCQFf~KV< z>OzB1D#V5PsG6ujgn$8wLd8P0U>kf%PVe03L`^I;_i^UV+5C*zhTLp~{ zF=!Ivp%9{rKqac^bxK0eUVVU~TCFlFm5N+89pP{osZ@%2nx-|)_xt^@TCFG)3b!<* zulqzO6aw`Hr_+giKF<`@i_HJi2?m4Aml}4vy=jd=Ai#W!#lqQUv%%$Zu^*q$SM&2P zAMtn`CX zgw=xp(kH{%IUhszY?QIBGSy1e?>ZN{2VT&ye;UO|;sM@lbm2wPfr*42lZo~laA0h` z4ev93IQw&W_55oMt@0Zmas$MFjCa|mn926sMy7}Ay?DFXjYzH^ze-0}KIq&)KQpHn zdzjmOhL`Dfyh?Xab~NDiw@!>FA7MK41V1kJm3`9Czw=W0oO)S6vN(Dt62%dujwY~n Y_#C;DNR2Nix{v#vmz%`;|3S0szw(em#{d8T diff --git a/docs/images/logo.png b/docs/images/logo.png deleted file mode 100644 index 9e6f0173c9b81a88189d1f91dadeb15f6524312a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105363 zcmXV22Rzp8yBEopBxI8$Nk~XGA<1Z1$t)yELXwf4BuP@)RFZ^{l>KCstRz$t5<*C_ zIp6#JpU>&^p7XvQ&u`rKb$!=$M;aUIvM};6Qc+Q{9MID~jDJp2QLS;KUx)vz(lCA# z{;|&fpsqI6D*2zZ@~k-g8wPhhiwjg#Qd`LXuJL)O=1u;kx4VWD{`ueMsTP*5vQkl9 zp*oTpZzlF)ROiU*YS*-G%LoNxgybKC+(rSyu-n@I4 z`&L4NE)|<C?h(&bO1A_*m)ox__s# zc;UMFfT5w7vOo-#(BbUc?^D%MPo6pz8@X}YtiFKvjb{V`tBD@O4isiSNG@$ zgHOZGPPnE|8h_+?SX||K7Zw)O>DN$&Y6?p;hjewBuccmNa9@rgM)?nYU5w!RXdRkx zyXJXo#xUxE`k4C1CYh6&g3I)k^G)aSy+%!?nP~)K=nBlQ)6lVAX&e+`kEEjF4i7q< zy;gYbsZ*z%&z{{c&Y|hxu*F}k;A zW9!;Vbi3KFR{JyLX>op6PU$_*SlW=uj<|O-QHp zWtM{pQu`epf2NJ@$9F{Q_>^1YWBBl}r_!6++MxP@yy6h3~|7(IbpgjBag8S

2mvhimXDpTT%D_a3iK7PaeVnu2!A%Hmf}zTe%VE|oA4#$~N;WVBx8-tOJI zO%5Lp))S?^XAqu|A#&8zRD>tG|Jm!`8J#1eqgt0P?UmTE<9z=chL4?{6t@x@Dlwy! z>u=s@n@e(D=h?&9X%nc@KQJK7MqkzExAD62(j;Ya-fwPsB5-{|W0l**rIA}xz{mMmcCP*Ra0Z?1q1nJ zdjr+x{8sXZP8?)?FU_p7bM;R`r4To>rdVA1>({)@6s)!2`T@K5cV&DAq7~BGNAPEI zWpQF)SKvI;Utblfw2TaDn}GSPVundqPq#nn`}gHs#DAZuZcg53a{PEyrfy7sSAj!U zz8%erbG{uIybOq)sK3aPUIE;FG3%yXlP7mjC8YvlgFVIOZHu zt^WM^b9yRLG5-Ad(_j?vXQOPul8*=nW+VI2Y&-!K7yOvKRHtCRpMSVq_tPzsdWq0NF>|sf`zANO!2G*q8x3&LFPcu-l^YR`PxI8BF2Ul_bt4HI`P&@8h zadBGmZeR8AaP{0?kIT!RddKkxEApnEsOChEeF0lehrXYxAVZj@({fd;UMOmy$I{&S zKR@D(ZegL3+V#jVS>HgcJ*{_nI(5}xBG!)EW1@WaR&#UnZtK~Yz@;rG@@cpx)|a31 zY}{FLGbJU(&giv)_x_{lnQh{DmQ{W3tzW({g;ZZr7oy5+Lp2UcO5!UjDalMvUxPyT zUwu0Hmi>MS^%cXc5S`5G&&6k^J{>>Tru?_)`MwBLt%&gOb>x10CadXU@87@XP=BLO zZFRmSt+%PAB?8Nr>UMm5-#w=k4Nc9O&3XIm-_YxcvTJ9WxGVCxms(-_vb?GnyniTB zo@H10Uv_&3hX}2n#*<&UtbO`u|E(V%AD_ehR!sZt#JWWh6p6(}V-451%2S->?EBwD zn&;cDVdYb#!nI_j_%aqcHYuISd7PO^)1tPj%*e=Sm}5}*bH=y8V^ANJ#vny`b9Q#N zd1ZhKiqy}hMBQn*VoMh6-MTuRvYAiE#dq%HKRWmuJNM#61yqw;w{Eq5`*!2qJN-!? zWwZ?XwbY8q&4M3;+3DA!tZ4Z9D&5&-gO&f)ZrAHi7i_7t#o}WB{`Eu=*R!zLjI|{^ zJ=C&|bb^ub@vTQ583^Z?<|>`jYin=UdiU-fsxZUeOMgTeA}CHvQ?i>)B;tQ?RnF1w_4v`??zgDLkiL3uOHl8G zt|A{P;D-+%md1+5GCOK!Mj7$!|M4~scRN&O=;|u@yL3q4V@F4yS>?i}v*SrFzU2bw zlZw87#Xmhe5w$;xQ&?DdO;1lx)=WgI|33@oVoN!B`F_0j>YXKDgLfW(fugxZN{Y4G zmMw5)I+eT)JlvB`PDePp*VjHWxEE}*@+ZOk`AG(Wz@;gTws-luXZzeNz|BJ5saMXtP57y5~lMwJ!sE z+EOdlNNwuxyi|I)L;#!4(f!igOVyGh*mj$DJ2GExU%w~Rk$WBB5!Q!9 zJfn(=3LfG1?U5O&^ulwCy{oM8`n1KC{>{H0S?5LIPm`mTp&=`(a=n?=T}>%V(y;?0m2P zEKAK1-e{eo;^Op|FS+Yu1Xne7W=sv#JN#&frF{FVr5dz{hXtzuU{55L{=wnx8ExYD z8=Ehp^ukchxOe9B=j;HYKnz2k-&z`-ilj-c{R)=>u`MP(RHE^Ll75z2e zXUZNg4u8;gX_zThw$ehXsscNA?xeah{-wm>&D9NfSzVQZYJhdBUoTITXR&BuF9Fg4 zr@ndfW=V*1eQHZRi^A6{0J9Z@oG_djkyY~CH8@ieBo;{oJNOfZb z!a7I8^dHvLgfX@3lU2O%)8M**3J;b6%BxUl)#!}E3tv%rZCjdym6@7NJynY%8bvHTI2 zp`)Xiwg1eqr{)FJ^YinRl)qZ6=?uas_jeV}3CN%QytcA8ru_13e_w>4j+4_iD=9A2 z0yJ3;##)ODy+NE@ToHoH!@Af=Mb80TK79CKTi88`pBBL#;mI~MG;oLJ`!0-RRS!(H zwzty&6+b*2>8^Mkt10clgOjcPbRimkEAvC<6@Gj1Ht@#AycaEEZ99Xbfw>5CKwGZt z7@NsILD`nyJ`&pP`Y1iS)*cX^Vplf*Gm$M)dw6_&Kd#R7srj19-oS5V3qVHoxLvh| zK1og{T$KA*mkdAzpJgs*!k#HO-7Y3+ z^^z^GVnM<{Y$E{c4gP)Jhs@G0T(~e*xw=>>E-udP{jG1~+^RxqOT@-)hNzj?iyETr z>A+#_Sw{7MCsDz{)aaVksLzvqlzhuvr`oOUJ&}}ZG=eqwSnlzX8kge3>UwfKVYT+v zg)OR4cy*~$Gs(%x!y_Zr@9*s<&{snu-hSp+2a28fYww*;a&pMZG0D_PfAxx|pr9b% zT(Wa)>>S|bw@X9(21$zNyDo(D&v`P1X|_7cfsll0rmKS+5D=t1_dP@?)D{bO7+VZ= z;QZXAoup;aa9O9MghWl^xr)e{(awRMi@oG=;%(rMV@pajDrwRb>3ig3Ie8V=u)aTj z{P;aqS0Jd(Ikd?!#9UGb-BJJG!M5Js7%XeROZ{ zna7VGU(w0bKt;>7wu$ooJ8UxNS7Dd8+57wM#J49kFJImVgvBN(xE}Bd{o!$k3py*x z2)cy9UFkK^(a|6&PGyGv{wkbOR##CnBR`CQ2?FCkcp&=V(IXLHQudbQx9=JoWAEO* zN}l0hf{l)ijbN?;O)qc;x)GNAlP3~nuk*2HR#vXwKOq^n%}CXOHlQn<|1Rfq-^-V` zAHHVC4YGlX1MW2TxtNLIGnvc(Tv7I2yPpqU&vFM)30NMr0eIslEg0WeIaB`k?_VJq znGLe{m{FAF6Io_vX0TicFZy}UY4UHok;9i4XTJl6ynp`&xG=9ftY*-o;PR~7U4ut> z4$VO=uAyw?g*NY-c+k-g1SKaYSLH01k)6$8n6+{C+vQ!L&H3KrmR+v$%JV!u12F?+kbs|zN`2QmvzAWsRvnE1e1XC zVXbhh_{yPKymy?o$f>{<2>GnX<-!?7iy8@>wN71iYUI|uq+-{AW6xF z7dv*bgBB3jLR$U5yQmyBPYVmH4b|6@F6Xze-drtuU{^BRkpy>dWMENV74Im$L>&7oQ^udv_JPioLC>qZ^h5P zbjr3#w)<96y1Ke_?;Ur8xdMKD_kK+$oPS$?>+g!i-%P=#&wozOPY$WAZcdaxTl2N_ z!g&gdLTecp6_kX@yuf893k!>(vgSvJ5}^@DJJd1r?ek{F)4<9VINJTwqx5QYborCW zaWH&B>91`Pe1TgFewvfR1n7uw~i;g$Znw zHSTTb>dI{8-F+9OaBZNSGJz|8!!y3h%YWNF6;}-$W$zL7Fe18~_%MpM4^Ievc)z+63=!a9UWBkM)RYw2|ukNw%G?ay+$v1{IEID zgK4D`o5XkRih!I0Bn9n-I;d*Tk)R6jpYzy%>r*`_dnZduY^ATRtOT5NbUeIAlv*P$3>pU*j2%0Jd(MuB*i&86i#;J7?d+;I&*EcV6-f&WOGuhg%-LgyAcKc7A z+FCK!&$NU^mtoaG1zBbicRXOTy1)g}aWf$yqWDZF01i};UMpz+bMPHy?giM ziUjw1h{|v?i*DaOa=-7=%HXcRy?m*x5v>W1xbMZ;i7ZL}SiQAdbZ=;*IjPS7*jP4I z#~V^}422st+JO?za`Wv;fQMR#7!@6_%PmLk%10h0f}(6-3{$61x2%d$T@XhDWT7JH z#I0i9@#xW`>Ny?#nK7yvonv60cwS)j77;t1)wR!(R;St%p3|!3Fg7Ph1rkPZ9j*Y4 zE2!HwyxNv(lBJY~x9;<}H_;qdr`1+N=mdPIfXXPjCEpmckLpu26Pv<&USABUALvVu z5U2qNNIxn86~^S~(Y5Ku^ij3JyDH|WK^(Yo^H^1FU%nii7`OZpV2ySVf1LkCAK%=q zXfH+lY{{4CPoGQyMAM3kH`(}2(Jq%m@I*@sO345z0~lC82{nT6OUb$VrTOV8f_ffrRdC} zqM~*eCW`mq=QOs@L({@M(bgus*AHih0#6OMW!jZ|N$+r(ic4KuTe&=bu3DNI@X5s5 z`o_K8P9NeH3^=mz92=UN_Jg6QF8@8EJoE7g5K}F{DOm)(yis#=J`y7zzI=%y0KUAu z99)NwHFc&N6>TZ?)1PCN>0De~>(@(KVK5 zL+G-ezJ74a5KTg3Ez_Qf8Fmz)?HF@HH)Ot z6^Y&Po8#^K_o4a^H8nM^?oUAjOzA*A$K0gHJ7BDT!4X7y~7gPsNu5 zjUd=MuRbqeW(S+5t?a#F@$bNcELfGRv#Tq;LjIyuAhFys_G{TUZp>D<1SmpxLFYoR z+Q1rNC&!cbu9g67tFl-3`Mbuw11DIYwvT}EY^Q!wR>lXmy7+ILh9V8b=j`H=VQ|Yd zXA>1x)Y2MonXs_1x&EkKpkDnNRDS50G7fd>lAHrj_QQ`#5OUPx7D4H&0zWzry_XPh zB1S=Nph7`+S7Zjo2R8?%rAOUEvkq@_)`s9hHcC%VG!Pi@{*NC&(q6w_yzX$#%=dBf z&70DEsy0omep}5gEqPF?y(hl0d{4Z0f;A!pVq5l6Y)eKwEa=8)3fMrvK%@;$RIaL8 z`%Uc^6%(tmFHo0=XMMu`y2s76=hdde*&I+WaDy*NIRP9%>%mg}j)lDaQ=W}c&NnJ! zDAB-0;Q9UEt3yC>>3(?Z#S=BLw7hoWwKr(-7WT-rk`gZTQRmaAH5B;6FJkHCj$6}9A8IX|qjpa2-y%>Qz|4_T=zd3@Ufx8Eh2XQi&B#%&C zs0!$aJUrp;+Zn*22|mEiWd$Q40Lj}M3zh75w04!~mHk3&w!mR`_nhIxJI+5ppyMpZ z!@H&JWJmmtJm7rwkE4R z60ephN}+7P=)h@sN{|-&tAc3Um6FK@Y3t|+yMCSdx}chXjsMIhFrs7)+o&K~ac}YD zmZ5BiC%}xz`cMSDD?X6W))Orj;W_fZJAB}xQ&sCNyhE#OS?R7 z)u3HFLo0yv(Q2@bh2!2z`~G!a8=?V#N?Z&&R=z8pqqPmOTSge}q@)-cZOH44;17cB-g|D#2R%O3BsuJfPY9EK1jycY1>IHv<32F*g(&rg|Nm?$l3D^u&S z5#Oh%@Di~FdN-CI9d<07gUx%_-PcU7@}KRANL~4xg<|t@J53PPS(D-yFNhWdZ-RpU zd#$KQj*5FN4Zn*6_d49zbiTpxB*W4nHJL42Xz-gH3-3vY<}(diZDTmtFU7%VVr)zW z(*oLgR{Mb+ukjPOT;eu?7(iHsMuyHtFiv7()VFWeu3h_`Vjkx5zue9m z7A>H1KjkmwAB~qSHYH>fZsv7(Tu}2hn4)P<{+*o-q4Xsce0b94EPeWeDB33R8kGg0 zhPMZHx)e*_GaxpqM=7i}P-kLjMMu|S?G(6lNc^($e$6j?@A}W86DLmiz=R>p0y`OU zgr+Eaa`;Qe2nyCK6x2Gc^(vt-2>6tJ*h1NeT){^F9n~7#nxKS`^{VuEO?Z{i&?U|D zsd963$s0#`B?1y5>`7VMm^cc)j6-KK&ORzGDXD$o!fwMP#UQu_ATu?6?#!h$fxPxr zcCS6I3)9%wm-Ghk#;E-3+HWHX=^BnQq&U2gO`A4B5keEtK@qW%qQiru^l{6H__)j^ z-@2u#yRLF&VRUz}$dz+dnPur1)_e4kw;^*gWgvPO$> zW}CW5EV?K!mQ)zXiMo(FBqK04;L#|w&BP}82TZ~$D*S}3p|3@h_gkt4&HJgky&4hw zpStaRbN8Y9a*?d-@T6$r=RZFHuqV3hzqJe$!tFUzl$~n`| zg&lE&Seq)b-O&);P+CE(ss}xUIT+h!Pwx0iw_ZRw%UDr(?f(RH=poLouAwNs@Wzfx zJZ$cOga)erV{kCNq9Vm*`-$se6gyyEtfZ%w#f%W}bdfv&q=m}iFQs+XrI_b%wrAIt zXEPFV!fVkUYHMrd#`>px%wM_-pRm=*-0=48+s99zB8?E-<3^YIU#YvFn3%n#z~7?W zdOZ*X1azzvUgMqDZ65HY$U}k19K!jkt8B)g`I2gceJ|>XmLzB9 z)2Gb%G87Z|9E9!TMWI_mI`%yv#Ec-qj~^%R8i6*iz*O6A{TJ z;$6sPZ&U;m!L|ewNsrpMckkxCd-p!h&R#>2271U6cEIDrOCpg2*xy~_|1@sA8T7C# zFdt^C0DuJezz#>#YxWfc0O8o(6kd#t`SW=MD?iXeCT;3Lm14Xsm6;N^o@;2LkTjrt zwt06xIhNxrH99>n_2e8c;506ls<-G4Fd)zl|Cz5KbueiUA2|X|Q&qIw4loM_70br1 z6Qit8k4hB4(IoVxqCx<#5OSsuSOAeffyJ-|ur;xgG^G+?*0;C51ZV*8Vqjn}7U#gb zcBbOP4xgem&FmcUOqrfZxV6L4(GgYzieNCM zuXPO*+GDsK7St|F`b1zM0A?ccLlPm%;^)sq1cUbcvd0bO`Bl2rlc{;XuYs$}w^z?} zJu{($I|9`NO%D}|z<$6s2Vcl_T3AuIap=haq4It_eZj+Hc)OQ;_49Mndu#2dHXF5;l_|eZ-p93y_yH7CjcNfJ zEk{jy;M`pcCr_Fd(%gW7#SLLSJX;bwGLdK@qv^Y?iKUQi|$5DO#RuCo^^%Y z*I~-|L7*qq9O~i=*Y02tEUc;fTRP!FVT+^X;lic{Q)(Ec!Il&hkow&MFcB`nU zdS+W%PR?l3?7-|Ib|4%l$U2IA%{L`AHR(}1pd!Ev%kEFJ%+-NT4I&;l{Y2PW-<&s6 zyK2xw#;#_qKAXF6=|b)2za#At7kUMQX>yKAjD0QLxJBps6`1V!6kGrk8=Lho55QWW zA)_$j(E(P%EhBz~ffzJPkh6h1$K6m5(sq~H7u>Dk57QLJ6Gsngv7ooNyAyg-BYr4d zX+NSqF`=Pr(Ke7dN{6)uCPuVIXd%suzAxf8Me9`VIrFg1`3>Gp1XN9Dx%(RSr?t*X z8hCX*J5OgS&Vf>jYFq_|x1Q>*@+Da!^0ufhG6Qg74HC`y?aAN{m;H8j!jA9n{>SNH zv^f@TW~O{^+*PR!y%4z^lv}T(AT1EaK`O@N(pIw}$&qR{G##6o8UwWt;)pQQE;VSW zuZS}fs+oQ+-bfALDd%(Zurj@rb3yR9qGoTSWPH zi5E;RTad{COouLh?TF)XRg^Zkz}P6-(%gh40vOxdix9H@)t$mJH-0WK3SNI-%H`i0 zsO(3Dy@Ar{t9{<2q2|@e*3QyBQ+2 zHM_R~7Z@Kq7Dhk^_CIkz&=ElQ+&|A8!OGgWF$6g9zY8ADjA_24hGluPaS)|#Z*<|W zYh4>wclFY_oAm8UZ4u-+V5Qd(AX7K!5qt06-mx{E3k*nogu*{Woei<${^~oLOQdDI z64Wv=gS%I3rRh<;A)~;_MoIhe^JfNRSnyTk1V96{Pn_sE-8u2=R}Fp#k<-B)#*(sF zK)S%G&$W8b}x`z+56Jr|7USIX%qmA44?MpPxZ7FGG5boVPv-3-Px$#?L zsRSm0nAzOOK6CB+2!9~91^`509-t#6Xw8QGv+*sC67j!^EC%gf*YVx8;|9}%3y=;y z70#cc=P(=ON>pW}C;*&59&C#(p`Vb=Nnkn@{oCdJt^*gS#7!;oIte+=${KoF`*>pE zV>Hr2Iy&o!u&Qx33EN6Zy87n_E?<`jiXGoxkF~|c#d2$%7JnsZ1s=%jwBgkInSVk< zxX3_E3%B;_)hpBol!4PLZx1)*b37T#7?|=Q*b{X(1eq1My~zr<+ec>r%DDG=OA7c7 zr|;0Y++bhu9kv?R*N{^d(iWWWn3~$?!h6Pc(^j0$$?o{os97Y%85L2@+xds#{lHY3 zv3TpJPwTMJ5RR(0FR)#jo9Ze#CkS5}_l)kDWVj9O9%j>DUlsgID3r$5)&bvm;ru{; z)x}2t-SgT`n)~Lxdy&up#(FDP_nmV3G38^+9UeaH#Cu7(cMm)X?(otd0?&YdV(K$# zRAdjyX~C(717qQhrh= z#hXPbA4M9~0WZ_o(o+9&tYz+<-`}jH5@e5~RHIE2$lf?;{qcbT)NK-^6W>*qB=-a% z4m3*f?c0M|hW16r|FJg^=OB3rAmumMQz-bWxz;T|Yv7F|o->RGq4f`a2)VxoY3K?) zz-yj7u3gWB>6#lFu7LEQ$1<;@LG%NgM&l1)zDLmLUEdUe5-%=6*{fc#GXZ( zQfKq7UhH_`%`F4d^K;OSFTH)w2~@-ClD}y_-EgZpXPn*ZSJ4+2r?IwH<-`;T|mv)GMyX>HKcehdu()}Snb zfcqh7Nz5KoQ%b$OvJQjJ~>^~U|n*^=}J57u^fcM}zhhnKhDR3j&f z%Lj>tn=0>Dd0)|~JEiWbgRzey4@m<8!*-TDb4dYNu^VTqLAr@1Yi0(<%ZgwaoMmJ! zf6?t8P;Lmau($tx)K9i0PWB$`uR0cK9Jp@;E2wwdlBTv1K&i>UFsLdeiS< z8ov_Ge{co#*X(J<ktYk(%i$k9Ohk^Zh;2E0EbGT^bIERpH}8 z4U+9}-ul<|?!ZLO%uO^G@H{y3)bIeXS{vTIBbx*WUf|M?4ZwcH0KLtT(m0p_H4RKu z;P2H==EppM52z5F@JawvdO>IP}Qe9oTKrA26I1n%^ zsY-F?-BHy74QKfywb5))Qg`w0tB-esgA2E*Ds^?q36Lr-_|#W71vGzpVdz?(<(UATDUl2kUYAIGMk?~C&;KGpGhU>g5V2TL@xC8c>w!j#~B;T@Q2NME+nn>j-v+t?$KWlP$mQvXO`Y@%>aBm(RtCqOEYVHV~2 zsYYZR0?#Vtw>k6Ox@jR2YulEwpST~=+;6^>UzYLubrdfT@DTie056F+<93DJtsNcv z`$B0bi$SiNvhG8?0T0C5A`w#1Ic$otFD2_ylmn44honSaEtWO}S|S@FJAu$1L}zsX zglpHXNkag&&p~{P`1tO*-sDH`_Mnzfbq8yx=_V!nl!sZT`7lRdQ9x zY@L+tgO9y~zpnJxb>{p^(51CiTUU|n$S}=`G|kzn|M1pV&-OzfHb!#uBf4vYm7Z0^sq5nXSZmJ6B2TN?^zxA1h0>zKQ_WyqhbrUyK0!V1xNKy+ST{s#@+=c<7dw9s=0*|eeOKdVPu&3R0 ztnkaY&!4y3;MFjbL>5xw6Psn)0sDkPjhEyPN(ai+$$I8*Ns?UkgjnO}w{-d!Cs*Qe z>(l1LckXa1uP#lK-*e@hQ2A4jA8-_!uj=gPW+rkO&;k;*Bhx@3P`$AqhL(~b-;uZ? z;j`xsy>Z{lJ@m14+@4P!5-9RrXfhZ%QAY~(hM?Mw+qXH;HOl^dK6BLTo3os?seABI ziCQ?+Q!qcN=fc$kMm{`*@MT`?vdP88#n{T~I*A+-=M@HZFp=Ys-`_9Fju8)R?z$~8 z%E1tuH$rHoK>xc+GXBWUO_WWu>O}K+eSYt8_U!o0*p?|DN=d8jp>l{46YqE4-ebH79}h)KH(Gt`}Chb(uiL-oP2(v`A#9T z1Quo$;t8;9 z#`|`*p72y6>FZSC*Yf65dofQF#>AD8m$x2Qi_$`Je*pn2$KuzoXH2hb64FHc8GRAF zl(czYiG12Sjr+rVRSS_ULlR0T?(NN6x2OTRu3ckf(?m&)*p}i$4S9n+j*678ng)?fQGYkeZwOMyb1hpQmi$R}Oi;XPFAhdkQx_Hb1~0_jWC7 z#BON#n@Sy$lVKnr<^;H~b?a6Fr#f=JL3#ob1E&YHn)+~XSG8jyDQTm}D5f5kf^!CU zfIs&Aeo2EzlBjS_2k;cR?DJ&F~__ z0(3$ABq$T95p`rG+{&hO;81Hy#PYy--f>T-w?-nDBznK1#j4nhDCkHZ%21@9N@ z?|s_W_qNhjM+LETGLfWlqjFlX2BYdodc{5MTi(li|3)!bwDP|$2hZz8Xyj0J?F;6L zq~3HNE~%)fXy@~{9S6)y$IHc38Q@vwh#vHAUETWAH+&&V`jmgm3E!1?@zICH?7-O^ zn`Op>cO-+pjEBRlt`4D_S{Ti982yxMmNfmxN13ScNS_dfM}|Q#*npbWpPiMWX=cV1 z@HbOX7={~EMW{aHj?bOj1rVCu0N*L?Y!OK*p~m15+nAV}UnOZ}^h$(9Ncxc!QsfVz z)m%@h!4ib7T=uxdUDfV__Zn+`@o~h$4#@kIA|TsGS+DknSRh9w)=)tdQWiMJwk)06 zk~N=4>7x=}3o!_!8vGJf?e>~MgW4y+x={6?N%7b{_X*xzyw$?J?bv3`)CD^{NSzye zk-%t~z!3zKF3{GK!Rp~o)O>n#fxAzpwv)=-FHSSl`5xGpcu_+Sx{fgx~Y;9UH9=U8LfuN%!no72Gv zw^frKD;e`N*=)+NvtTEJ;~8n4F-?@B*zp?HR$S4TOZG5@9 zvPgy)uujS2!ABs>0$zC5T#KhuF^d`n)PV~22AL}vm`PYGK^W|V922;@*b5E}oyNaS zshW><=sKGzEHY3AluW2_2i<7S_EzvCzq&4@Z=ccm74-9}5zmZ*f(=L~kzaXh>p|xY z$8tJ?8wYEgsOW+x5vlf!6Pi+;SvU<_VWBdJ|HXF z62XoqWlh}mpv?LD`a;urOql@S1xzJry}YZ`y_e3~9+|@!AphFBmsq(CS2t|AdN^AY z;wRFNYpJVmhDe3~(ak=}UTS3sWvgw(6N3w3U%fg{kT?y35?3*NNCr2xMcVf7SutAf z^C;DRcsLeT`yV7RZlN9C8f;961CPWA*Id*qu?UJVm_3ycw5p|HT{RIfm913b`C1~e zY^)I%4hV5SJUm~1g_}GMHmDX8WHvg>EG!8K5&AB#HaO#>VPU>C~(FfOf36 zsj8-w0j`@V+zesif+C3x!+-^s+M@7-x?St6>p;q>V&qKW4dVS{E2kwjX&q~e))w)! zzWDL62^@YpP-dWwwvQh@O*%Ok!ytMflZ+OohPn{6+xbARbql9oRa^-svJt19pzg)$_ta;vP@WV@~+0vADJjJ!(HZa#TN`)#X|A zcN+W-GDbiMGE&-vyO(_PjZ&Sxaf4O=jAzGQ>3xS4aZ^~S-_eK3Yyz$i_%2}oKIz29 zi(X#-S;iWg^pIS$Gch)B&4tP4T4b#~bT1I5s)<>fnZhdUnP3?Gu+Z=xwMBv;%>(1( zA;&-OL8nClQKu(i0P8&RGvLS)5)ve(gRTYigv{fkM`8eT05$0C?=Ch0?L!O1Dn-Xb zuk6Q?B%uuwR=-9=GCJ_S#G~|~XJh7#gy?MiS>Kzg`KtM0O#_QGn71>at(J^4OsBWls zXnK3-t#KWK&BWqHeqF$17Zav$Guy6i+F^=x_qviEZ(foAZF zi<`rPEOS>3Iry7TA}~QX4jB(3@j$3?-qqCVs6%AP4VZ}F*Dy`SuTGT4gPY%+ZnQm-c;1h0VeBuNP5H)E$*edYtW7Jj@(d-b_LRG~M zAhOKRv<#9p5JX8U2VsHT&mHiuFg5vzPTlCt2ac=${|+5GguVW9fzKRr=-XRc$#X?D zft6Ol#mJ87;&SU4GRTJ*$Ym81%(vXUdGiV~W)SKV!vnH@|Aguf+EOlhS7b+a%_Es@ zMk(3|V`0xA-iACp05Uprt*y=2_V*cy6-*AXMi?T3I8H4_>_3Ew@H~-b!kQtYg&b!& znb#pmiNY8P%)!A4We7^l4ZieL;<spyBQG$U%VTJlykNhN zq4x|C#*B@P$w(ju97sKbn(AA=vTr9Vaf`@|HZ16PkG-AtTSAk#v@ZW*xF{b5kMd*o zkpi%JnA^6H+(<|U03QPf;)jP3`@$RI`diWKdvVYm!Fwe8olm_2Edd0=gl|Tq)YeWi zPz)m;TXjA;sS5HpWEHegh}px4??NcVgMj}Gibl%*XB|GqFmaQ<^4V`pc#;2$2xvfg z0#?Kb9-?hXBA`TLhOY*=qj}#64#-QsGrx8p*7Z-=UUze^yAT_^E-q9P0<59H*tEL; zH9n41IYHdm;dX!~iqHqPZfY@Sc17?M?}Lm@$=k!lS}g7@amukE5l(A! zAA0>9O|NY6_iJ#%A~VJO;i{j(y^mypm5HH(rhph5zGex4AruZK3{#9WV7J+gAGDd~2>dSOMYY;rnOR?=CF55U6Itzg0DHI! zsCM`pjv&4j0}8)sdlI@`RnNBDdZqLRuKHi+RbY+c-1+JipCYt(*h+c^2BApGK~am1 zkN0ZZxBT#K`v{*Xr&O#Sn!S`#%hy+4JK#_NL3rt4e&EQuJMkfGm`_1GEe5EI1cC7$ z$+-f77DLIGFJB(fC|fU_n5n83-O_<|4A| znNLsChsC{Ze_)f`)PHze*4dULqr(M>4v^qUXsrF))aDtm_MskX-jnDKmWT@ajl2M$ zCk&ixZJD~M(OXb5|^j`tjSYlL?a zXD;}yI}98jB)x`dR$t6we{8>wWlBP&kQ+(sc5J#2GN7+Jj`s^=9Ti;;y2uOC)<#@9yV`yic|J|?}8D>O5jV_v0_8OhG0J0#1mH!O}Mbt)dwcob0 zwQF%KOfN0njG783jFPcGYFKjAnCBp3(#+sXI43~{M-BjGeDG$VNaKYbtwn~&>ce7z z!KMTXR>R7_lFI6nvTZy%P?_-mL(&GtK`l9*E{k>u+2#$IFC-ZCWnVb2bB-(c?Ctlx zgC3Z~t+AC>7mLFeldusv^#nz?<+2Tob|^yRBYBC{}OXA?Jd}Pje(fe`CZL zz7dQ+B#f~GNmujq^hA;Tb-DbsJo7<){fw)kVVW8__pyZr>Y^zi!`KpGU(QwXlh}$3 z@H{db>fk|;IzDD2!j7**T$ZR0cp?160)b$qvQQx3SUU4O+31sY z0XUh=Y31ZZ{W9z5?9{^K7NyUa`U+F_v(zRaMAoQU>T+wfAH5`s1O`inw$CWn*~(^Jm~s?HgCq`*A?S7qGXf*Mq)UC$I!fRX2vyq<@a!2w<;uN z3`(DyXA}~}jwBPw0W@Ix4)cEo;b<$K87>9KCrJgefFTG)ybtPLeZEmw()E@~x%Rq5xekFXIk6$tE z1Cr~BP2RQVPbIen&G@RM1g>0yB@c^)2%s0go~I)Q5Erk!>zuVyuo1GMK=4-iTVS)b z26JGR5uU|4KD7Wo{fF$xgA$_+P?|fO*l47Wft8T>3urBuXtW3BoWW6Gz5wg}(&lQk zs10v@TBeVz5!A!e`$!WXm@-Zl)oi$Vv)t_1v9>Y~@k1i9LdZ-*1_jPQXiOXW{*`k- zF0boPR&e`z6&RLV;j23hcmfj~Kz$EFBfb?`RR* zQXF4a5m*m89YIzs5*S5AxBr9=4GobKOK`w~Mqr>CfLF_X`S^qc#7zgUd3@I>Ykhhx z5qk%18h##Gld!=ZFit=Mc=&0PW5>MS7Cz>3fJ6u;%C&XPyWfuiaCFRYi;;6JX%cenAm$R8+5L>WZG z$Ckzf0xb?_fs&z#x&~ZZbx5Hn6mutJ^b8DuK>1_=Mko+?(70aHXI7DwD>HcrEfH6S zv{%en;12pR(m@8(lYe3Beyc=P)q8tF`tRVyhmVLGW2UuM>L}GAg7i964hB{Bs&qE#$HU@xo8(aUpVE zMHuh)ay?O{EoR(_i-ojeoSz%qoM9XRwvhBzi5owP0DMuLrb14Y`kWY}lL;_PkCB^b zOhRPBS4Lk%oEG?H@3sJ2TMo)CI^s@X5Cd8L(*C)OOHPdzXmHrOApn?!Nh~iflXy27 zlA2&Hz3}MJDe+wNQX|!QQDl3FR>{G|l?fOPrH+`ClMP#9(w;y66P(9Izt-5sCdOFY zvrT=ysTXpgnnG;GXN7=Hier$?WyNR;ASH%RWJEpE$>ACp6F{p7AKP2_<}Xd{yIqeS zmw=tzXY~Kq?L;z5;3y%w0+-fevyyY4@PuIVhTv=@4ixy1@3Fn!-uI(}Q9cP%vQ#eb z&uM*%+j&J%ke3%hcoD5&dRCMEnQ2Z$((ky3UkbWi=g3JNm`sPFiiwX9y7m2~Ln(`< z+fKtuGcsz~%5WZ2M+<&h>1_OBIDrTr-9vsP*RCShfX*1irujuL;ny2v-4W^6`gh$z zGZ<96?Pk&A$HPhO8crVUNbN@`niqWp`JzD zQnZn)57t6)BM}sd#0$q1r76;gWRQ`qVL4-H^|N(v5R2KBBjg2{jJzX4tce0EgQF~n z-ULk7%iE^FG`O~6#DAi@kPc_Ui6nI8UrOWr{V-Sty#;ZE%cd4_Q3YtXB+%c2zjW0) zvlGeTHl%tY^1%9Q5Wtn4OkrRtx#P&BG%!;9Zlvt77q4Laj|?=zrP0bh`hi7GO-&H! zpB&0?kjbe^VWEKxqSG$qtIX`)v=JUg6xj}D4^m`@tfJ~CX0g!8=?Yd-vzL!~D%>-0 zx}CuHZvVeY%K8Be$*K@j6m6EAAoaVyH~#J5AB}-?uCdpWUqtF;LM{YByd591)2d+8 z$iZS_Ib#;SsrBfAQY-QVLfJllnekD^4|ys{?%WwoEsyc)aB>*Qxr%u)6uA*E=r&~7 zT4D_^_AbhoE#`p9m@2&QHF=#(fHa!)1StRl0J{Q^IQ$)M`+IU|dj3E1=G4r0ZBMF_ z*RRv85^--)+hELI-*M;b@1*1J2rLERBM+8;a#6{jj?wgl)F@^h!dZA9(|95I&&O)> zZ+24@iJadybQBtVUsb~h*Wy;Dq~;DpfBt=_+PHXFlAGCDF7xL-S1fpP_}V0nnlcf( z);OpMbrPZylsK#dAjW*)@uj}?fmN8`yMZz^0AU2Pi&Z#^?gp~0#HEK6NDk!~-tC~k z7*(sOv5SZ|n3=4s!r3r`@8Vw$rCcT}(E&%Fc)t1_xH1%oI5B~nataEmp9FB?3kZhU zn0VhMenreJKwL)dI^%Y-+FG0@0KQKSin_J(s}w;goI@bAb0-VL2Q*O+Ke9H0hfh8m?T(6V*oFP6cgO%!RuSqR2M4ykSEbb&c|Hs}YABU%r&zaOxg z61A)1>e(+Z?7;q@3$O zg5~Gm)Q_>cb{5BLVAy&1rpfm09E@ZJj?B;DK_K8O#J++<%*enpTBcyYoG>zIn1-T( z4u!IWmx%Ef3^gECY3z6l)7pKIXd$r>iCt?OvQ%JEH zN5S%hAAAkA1W+vrhrC3q#NCO7euqDV8POUzY1bhI)2|KQVSUf;@fcd;OU$n$o{E(8 zE~aAW)j_5?VThL>I>drR8OlCOdbb8Gr%S^hyX2i@00^zG>t<*hT%06PcP@uZmoAZW z&p319UyxEf)8gm^tlrSALE&Q5XIM+`7$I4Lct~g3<-zIsALjxWdC?z+ z{QX7Q_l{5CB{o3jItbosfe_f~bLX}hX2n^zOw7i)EBe)qq7=j-bC&Y2oD3hDmB@dn zGXCsXXtHNo@IH&z;Bj!Lv91DaT?%F?azqmCz zK@p77YKX31fd0I<=^7 zGw(N;g_0VA*ve+mNg!dc;1Ek=C*GZ+p=o7hOLC80y}kc!;+MBE=8g34!t6BU5;Si! z&pp3gWW3!b&XXsa7-@$B2aAI{h`7GDcA_V#2A8CEpIjydkob4ghs+2LE! z>2}%!s{r|~-^w;uF7`}3PGaP3RzuMU@vSqxy&KkStZbz-4CGHG&-eU;U<= z2%|Z6!Th}8vTc@WpoRD;d;9)PyNZ4GD7vHSknzYnch*6D#+@UXp3F(b-|Q6eFQi%s zwEba(Wo@IO4EtWF`dXmvHq^>@UfbW3_S(>cK z*GcfC$`Kc83kQUcTPN}5x$KEQ*Aq0cJgO$9ZuUQu%ms)?K`uIQ{u9kxDC&xM}$g_+Uw*likTz`s%YvJ`#q{*Y5zpQo|9l^7 z{tOb{f@gZ`Eur6<-y89ShQ%46R3sYs{#Yp@hEk9dj^i0gTo z{G>gWjuTEky3Cx)d7u2G^FCY(UbmNPFg;Nzbo%;^4m|v2azf6rW5?ikq?O*sF;XC( zBpnI#3X5HXDuU@e)g8-(rZ313LtcPP9xiA9XWoyT2uG5c{TK!2VSid=EdB;Sbqy6} zc8{_=KwJ$7@7>-F?&y5Ta_D5nDiz=mR>b%t1Ou*uVJFFOK{Ed@*zo@S`*z22n6!B> zFZCYuum#Blx_h!-`Quv^2s%2MNSO%y&Bw;3{RjSPb6(q?tN-QGUzDnrT4`o@oNY*f zV@L!VG6X(d>h6CnRd)YJ(wRW@xVBx~lrbWtGLuY|REi`t37L{ql1hU}%8*3SNC-)h zCe=w&No9(XA*HCyLL>Ff}9K)*xA!_{YjbcLw~={s}~-%ZGSbDlsrB zs2IXEkiEW(OTApu2vRTN&e(I7+OhLRyLcx-f2l&pI=Q6G4Zl|+;h{p95G}pU%}s^* z<@p1buL1d|dm#@2t_#duS{gWP{gN3|_Uzs*L`!hz`&Hz*+ccC+AQsMG0k zdvnQ+dl$zBI(qIk7(t?@nWI}}(iORakc#v3u!D0^yrN6j5`)|lc`7|t;w z3Df?qx!HGu>#%CY0S{Mc&?#7e1AP8+BLvrlr@AJb2~JOCUoiO>KD~$i#(EgcxLMmo8 z)xrGhl1mK!`Jb#S_}%O>rzam5#-G}kA{jBn8C5TMXYV-!`uDekc?~Emt30iZeJlFJ zgoUWQL+0E7mdK*7{dA$W7rLs>d;)w4is2wc zN4LXHR!HYXH;S)T`4ee0pztDaNYqxBUnNbOGk##NonJUWl8JhZI5NK&V47+`?EK~X z*HGD0Cb;BY;tSwO!L}Ke#f*!XL*;>8`P}lW7EAKx=Io6rz}ZvA!X6Hw*^ z-cRcy#>t2&dzlOpfO=-;{GLcBkW+T02GfygNI6{Kr~lojh-0hRrK3>V+S zJ_zc#Y9gr%%O<_9g2ne~Yp%htP`h@x0-Z3B|`O*BcOY0{NUsw-0NgG*vvLKB8&YQ{gWIq#<% zCdbTt8PntJy166!g&Mhhas%$?kWfCwpWP~~_r#3uA^CSWC|sc~=O(ygE-M@hjCKzt z)FH`1j(;dJ60w=E4aaSdc|vj@pPEr4EKY@tmu7Nocs;CYBW@}2U$$^+YtKD%(pvt*bDni)KSzr zkn-G^ozy~mtkMT8OPB5*Bu=6bqwue<@06+yU!Xf>%~_ia^O2gGOj@;r?IArA%^pmw zh&)QIR2no2ps_3ZJ9&-Pp&iI1YdU=h(zzjY4 zFxebL0CQSMEa{uyTUzelZ+;M}K@8$CArRqE100)$dA!_wW3{4T(jp)Nz-`Q~d?)nC zLa5K(MwUSD6mYXfI9wymn*O_f0kq6@?BVC~j)gK-z?1aG0;orsJageJUL^tmiU^u$ zI{+lJehsi9MZ#d2Zek$dIA4fryEC^p%f$kP8w{ zU<_wU;Q*$Q(g_Q_9z7l#F(+~1m|ZrRqrLOvfF~ICm`%GbWQR2L!*KuDb>o$~ni~>H z6ft3Qk`>g|!^cFF-q#&~x)D`7V$q-L23)^0@z8Ya0TIap$>|9p5}psnIOG4|6VIj@ zWBvolK#(=F2#C`Hp$A8V85{Gfemu-C1%jo%KL$e)iIhKh@2Rk>Mis?y8FazGz0pL_JPq*Hwjw?SEMS z4xK%eSDZjCM|R|E6TXDizMwDpF`%sL%PVfqS@rX`FJDf0KMfclPI7K;)ar>jJ}b_} zW5}Ffo(4lW?C>pkGN3ssuT_<|FTXL{6if@ttvT`V{qW>nzt82{fL>BkQ=lEo$mj{k zKai*Y@5QGcQRf&xvv;E2@ySG3v#( z4!S}CuS0&kkZamPzea0{>NMmlCyZx8b0^A+y|NQK^cbLGz?%^+=rJ)d<5foMrgcP- z%LC+C^R-s;Vu*M63kBfWpzxeRlxsyhjT&Sp&SUW7uWPIA6vV0o9)tkIgmWHIEd<>F z73RID`vMk*zNHJ{?*-^R*l@PQt>CvOVL1^>0@VOsURNoVM2#wGzj5;7eGxG+lHey~ z6w1Xr(=h}>UdTnd6NY)h|Aksmq2xjejZs(jAXn`gw{~LBJm=;no{zBA2PFJ!W9LWV z0n$cMCr0|I#2CkC6qdf6QC)OC(=Wgc_q&P^aQ<2$xdI}97EtDR{DWgqlC|Eyu-nNY)mC$5_0 z22vhZ9(>~GQT5nA=_YSlqRy8%sBswL>gtB0wRL)0trAHNo1UUu@$!r$5z^C-lzI5r+wph4hNhQRxV2HB82^CDQ z+zme*wA`DiOumHnwk1m*IzF==CC#wtontSu&@R(Y{a-O56ozaMNj9VN6Bf>58WLSB zx$awzFgGRXvu{BtC}>nE+>o+#xmytT^ryURP)x>Fj31EH>_?!-03z-02xd3=yHKAf zH}ssG6sa%iHojm;FG^q99$uLkqYCWScm91h?G?K>=T5FGm)FC$5)g{eh7XA7d2lEs zpTBa!MQZVrk`=0w(CDHpTedF0KW|i=dg^6Sb5O4nnTP@-kk=s zG|2+_*y^@wm8?c&)X}5e@t*r2q4Z_iGpB>b3Ww(I{jCc;L>bHQATG(0pfBvc8X|T| z;8X>rfnc4^0z!@N&Zxn=*C>J$za}faTFZ1Yqc1&-kbJ{~3-1!Kw1?0L_4aQ9fAePE zd<_FILNuE%Wxo1*wzfMuzUt2@5zb%*h$KbRCZn>vq$o}&{gf-pd)^Qc2n~Qh!Y`EW zUYk`A4`@ev6Yjt_7&}`ddUAmmKiEEPQ&>t$3N4Iqmg6Nba&l59yVIuh(mWU@%mhRk z?@?%zmXs9mXYhxQTyG@*{Ujkf7OXVImkDb&NPRziK<3W9WjJMGTEeY3Z$jdk)k7pZ zqon|$6l)IPS!J8vlwB9Im+;zrTcQ+EJ^q|I8YeF%0IHkm0$i$B`KbkTWj zynY5Q!8@Okg2a`(7`q0t5!;KnEPP2STB;Q?7#+Pk75Q~E4Jh#F18JDRcSxkds|DjB>_&jkzm;A%ES+4SnRam4P`J^jThyz`Uw}BjhBA|ND6qv zad`fNKVK~eA6dITXZIJTbXPyM4wVTt61Ghn)Xh&Cs2rvhyzkda{5ZOwNoQM#PEMZK zb^_Ct1+%!p{F-In1-_s`14CNpVmLH}Me@n2nxIYnnl0xh3p+V6w1lxkznsCLM#7Z< z*qOmD)87gzho^Q;zdApZ?1sbJ?x#P`xi2PqicSWan`sQ{6#=b`iy*vExNIDTFc4?7 zAvBIqDB^1zfiP1r+ke#YrGr*q&kY@BEOJF=42a$3 zx@y0n>qt-GCB$w_A0{IBAmrQ`@qrTe#LLaTzPiLY!FQi7fdN0jNDKuC*_U|yewAy# zi3cvy7cEv!0gq%E!-3V-p=4DitAq&{ll;x$>RYMzA<6OVg(On0_B=eF>)`{)A!onqmOylPHa|t zKB)3R7t!NBeBn5|lS*NwO#umznLKJFM13^2LPf!6U&)9qVoiuM;WPt-N!iNa4|8Xs zj2CNfx)Jw^1z>!1W(tw>;({U)oaiCJ-9%XlA4aVTG>T~N;Bz>>qrQRH?W_if@mRtj zzYm&;{6Gv@oBtfKnDEkrEHO_L{~6)~Bxcke-a7|iGJ1IdSCbK04lr}ekJRXHZRh~I zJU7El5RWKC7*L07!kLW|>%qi)-~}#$KQ%GUF1b%aj8rHm_Dt=VGyJU(Ea3Ob5PR24 zTPH=6;0RE-%MfH3yjiZn)46JUpnHhRSu1uzh=Jyq-!B&HS8x*;eG{rQKr0delzPIJ zHS}E|fyc1JhK*GfMt%mH)1B6i=_fo(_`%fr;;E2U&^(}P9l_rR+XJ%RXzU-`1rfE` z+g81KbT7EMh!G`L;32^0^YFxb4bc(P&_oxm$VD&9x4daRWcw60dwgxXmL2az5y3MC zw?HN*lvzOsvt6XxmIq$1Q9oB$`N)njuKV@>?T}<@2A)c&L)1J^XF5yNR$dg#0qFji z(H74Nv8b@m7R)wy%%bJX=T+}tKC@49m<4IOq6~Ezvkj91dDFCqs}Wf3Hvg)%_Gus%ozz+&j#4mDD7J*hIIa!vuB~0S z4%5l2++3yposjS3duf}`TKn!@VKp&WtqTbf#>KN;z zyyoNBTg@9S<;;Qd8C}9wz7Om!U5_xuH%d?9` zY*&gPwqoksG8qr<2tU5(lqb%G+O=1Hp)>B}30O*T4l^c1oNVe7LQNL5gt^JnUGgCSB6vM1V+@VHMN4UtE)TYik3q^*FH?oEh=^5;vedE9H<(U=PKemuhyo?*BIT*`ayHjC?oiV!fCi_ZDOsl7+U9K)!omGvV$^(V@Q zQoI9Yb2E?q$avNue3SU&I2nzZa%G087a<)$>Des2X@ne;R)Gt$V8crQ`4Nn`)1`yp z3z~|W^S2HJl+4;#&>iN=-uR!0=vuOOPDT-w5dL=sIxMriGdY z&Q0(A=E$@Yb-#Ss>1mDhD7ELufcEXqzQeoo&}Un&TbOI-?JNg=6p>WTmrJxtRV0%IF zv-P_^=alc^Y#;#Foepo;+c0xXWl7n<=oUvE+>>%$l%OeN(Z9(iR zUzU{gNkPFaJQ2}b;b}jT=Sh<{X=fYyipI2Tzy7JAhw?Cy(`ig?MKhyR&JC8g8^eB2rq{8&L zXRfeS=nMEBdKT17%a3+!;VDB)aBsNe7Wv+jf*S_T<^1FNgLN`zks>kK0m~0DHk2_C zNeD#;#qB?T1vtn6ASqxfEEC$dcHVb^fe|La8HgE)%cFyuSaTGosr12!Pj0wriMhGy zD~aaUYq&pKlrPzF`?_Cu*l>r!mxILrn$!C)lcpnphQ&A+{Vs2ioJ@A1I*FP8R-(^T zN$UsPL#hHku137W;Hm~s0(CpA>?vKn6l#p3j1b`z+ zoU}Gn1d}~pb>eu^Uk;Pm15h9g`JNhiGqzjb0vzM=qHKObmc|v=tgs~q9nxNZ+?t&` zW#9{PjMxH1gACOQA_YH5eiqEuf3w5gnWvdkWPASE7x6ua2>hqF4SkwApR?%v$rE0S zHW|w*zMU)vUu=i@6R^SD^D445Y9;-&haxP)=YM+d8WdS~(SB==(vvDGZI|Q=lSl5hj3F>fC~WV?-U;ljSMs;A=N?CWxeGP? zgrC62l2j|;4WyDJpN$B|L0ovxoOPW)t+lOBDtv=s)8aLCuAaMxF7WUEAkWEz#6b4g zjva6e!WdTnu*bd`( zrC0-KTTnm4$C4q0g`A5OS{*A{>KlDslZ-*P{?)V{-CrWHf`c|wmCNhC=O^&f; z7N5s^#;PpaMEDjyd?*?bFdWR-z^(=0M$NW1y5adHF&BA6)vg1TTIZQs6>qP6bhg!+ z< zjVt$c{@A1xR38k?+uV!(Zg6l`S~%U%%6 z9mD^g7gma@2zgHqVA|&&jKx$RDIRFx@=M)!XJ6)t1xrmo&ge}>NiHm`a39nn`j)v6 z2gsJxIJ9&us7gJ4h?u@H$92*L&_tkGffG4J?y;*eDl_Sh#1Et_K448*(;)ja;Xt)Nw z#8vznhlCDuUsLd%jtguxr3PJilC|9__fq9J)a}W}CxJ=Xz}!n--mcFYrKDt(Zge%F z!+Lb@E*Mt41&}=29U=LML0#+=GP=Rn4W|fZ?#`b=Teg`8dhy5UR5HSU(|+R1Ou;89 zA7h)k4LurPMC|JUNkUoRbRb}--f&Uo8tKaHaXe78fk{S&>`r@sUrjP3C<-tK7m4z! zoFHx6D;fu}dcxkv%4B%Z1k9uf0^bIVNjQDFH;)3%BjPf#;SutTH!S>bMb$#);w9}H=>&+0_c4WM z_u#tMud!&yK7@%ML9BC&C=zzyO@VFgmM!Nx$6ayNxEg2f>an|_n^cu#<(OcJji+T- zMfwCP%V&)qbjf87mETlH&FFfsRg|1yCSnHy_vL35Tm&+S-mFTVE;TUH$-%)ES>Bo6 z6fnYUiZ4$4D0l*4F1||DbEUlOoScQTn6tv4S%7^%kX1 zUXUpmeh5_aw-)6El{|b72ZYx~z6`uA*Rd6#W=J!U}fnO`IZQWM?SFQv1^a`LFD zvTE*UP8Sr!AMAjl`Qq&6mKLeq@^I1_MLAw?kdwou-|<+$$0}L*+o_~oyB+Oj8_YBd zAASFYH&qb=O|fH@-udH;{DCyi{d$z?uGCx?IVXAJp@*)Tc=n2YxMT&W38vZ4o{i}K z6tqAnAn5pLoB!KTMU5xsA%$v(UhALx)2L~(4?a0Acq#m=(7m2Hz=978GNo`Y!Lfnu zFlMnet2H4ogyH5t6ZK;dO@X~R-Zc8027!umh9OQm2ec1YoTo=p1__3VA*V7yHaT&(cfA_}DL#&Y#LdZ(6DVj!s zWl^*7d*k}{bx)Wxz}4dp`$``jL|9IhfI+9k6wjx-mDgu&QMfdpHxm0`MBtqc1JmR- zo$T!m^dqeS;T`)-Pq5xq0VH&wU@hxvI4NG1p5-(&N-_^+50glK_AKXmN5gW62Ra2A|J z1Y^m&SGr@VgBt1xanR>RQaKUNqlR1j$wV77{?x}*4IBjol}OG6qo;ro>54@L^S8$@ z-^KEWxb;e_cP-fh+8Chdp;9=zi+r@(=~d%A=d89}t=jm_Jk?AiuWo%VlF2g*3xn&@ zZMCTz-6^ITk8;AiURMEe)qATg#{ zU(dlzyjDrK*sCjvk8PcT3l=3aQ}JRlcBHy`UuMv5;1dPX>_>j&mI$9snNW-7GWcfF z9wbo`-n~pkXgC)5CH$O`;&^xIM|(%v&S>b;3eP(_^+D zCMl_RiyoL`z4T+_v>wC8Dk=TvkU@Bw~uBM1zZjlsV$S2>}+*q!{k5Hzy6te`O~G#1B&Cdw|-6-89OpI zDL>uuNo0dpk!NSiUUs7rf@U0>(c#R-g~xc<%F1W%+^So)YOF-}PF({P6z^x%<~x*R zpKW<`S5A9t{kwO@1=H8B$Q)8~uy)3d1J!L`82u|ex2@>yci)V+yGW0X6*7Bn(4|Q-*DpJC=-aQ@j(?dE)b&;2XqEBf^(PN<3QaK^CjqqRrBnWXFB^pS9vo3x z{op}AsYxgLp}9l&Y2E+sfcY|$6pk+#H$uT{*0kr>u5|!*!@pff>wYl`C2Hf=*2&l< zfFLocz%G`F8XAFAK)l6I8O$k%nJ8L%kBKg`hrL-c)TF)sp_|rE?2eX8XPgEha4wIr z8=OfHI%`$-X|KYA6w_1)5Pp!?pBZvfAGga#mIvFYuC6Y5e7N@*M?}TNx7CtOOQyOU zRaqQ7I-z^%OO$cq3+^~xz2Jn>!8gMVj?m0N_eJ*rgD27ma}zte_8e`_I2~HuKI4Q9 zsIp|`v3R#`4^*;CfJ9atufIY^js;bOa^CTK8Gax**i5<6&FSy@|a zwb`ZjUK%J!Pkg^~h=#w@8>citN8S_}T~bo=bpCF$agL#xu9|!9-d%*kKvgPg*mgL) z@#Du=;x_?({N(8y_%k6VU}=(jU8!IoDai!im9ijSELbPM&5zd&_48}rYX=EbxMg0b z-8Z!n_4b#yb5D#*?0rvl&m%>Rc^T}3c(~e8uS3t&cN_*uUNLSpx8`MVd~cEy6%u) z+W(EWAsy@MU1Fwfl@UJ5$hE#5Z|(H=T!+WZpt0gDz2;0_9{Tp4bf54&FUsz1Rf{sA zJlj=Vt5kUzRaa!n@Hae5R`V5$L73Fkv7hW*)*9Hi0NMzxbz0iWx&3*?Tq(w^b_W?a zWTvLt4Zp-+2?^=ec{@)vvN$=T{SW0n-ww8G_g~6Gg2kmz7kdDKPTtzAL{~%VE{~ns zg(D;-hP|G+iaimab`!O=ceB@yS!E^~S2{XMz{vqXLc{V0+BP}5eA<5BJIGv*3@L_U zP!*`^=t$;TUtZmyH{i+Bvwa(n%IVD3x%=T*HUhe_CohW1(`msptHshSa6Ea*~rr6{X0B z8VS&<#6kKWj=JOF$dccgD6K|$ugXZey9(lo)<~+I(wL!LJITH#v@*0#!`c(q`m0xG zHqS|nlW865^RBMW5meE<5-5Xi;q9^Mn?DUwR8)M_(4^ZGRbk6h5=#R4>@<9ts+~J` zhA$4f0}Zxaf^IMIgcyePpP#M>=LveeU@0kUMkE(%M>R zjmqPu-O^hR3@SNI;re918CRkrhkPw5nSE*>rNED=*4Eaq+ua(q@U^0>UQ!?ZkU6V^ z^sd@>>erZS|08f#O7re<6Xaj^=@~XAkx3Ax>bdSW=?QG@?fsX`93UfO;kEyGO()IV zzkk{Ya$jhYW1PjddT^L7aSUGfJU?Bl-&ssX+26=H7sVyqKDlS2reF%Ux$%-?dSk{qJcUs&(R^CA5At5-KTf(u3u6kJ#;n3`O^@xf>i_m#JF(?bVs?bd zO_w>bQ|~6%9JW?*PVAg;_wR>}&*@Zl=T2{7rpJu5z)U_aOE*vZQakjoY>C6cI@|CParr>?H_ZLN zZww%xa7d}Ht_WXy(KT+%o2cl>;zel>1<=(Qr>7@REG4C+)LjpLJ1Hh`R&Lzb599`? zHEWO1y?b9mtSY&3#l^fb_r^Nk`}@U;pkRyojq*_HLOa_w45Nyq?CgWj)lNK4>fdL| zIO%RXE=!#}baQxk^V`>t1Vqo2YIbC7B)6T11>j82nRRWL4yn+ zkrw~uA>D6*9{~Lh-nmomobisfpdblE7r#o?F2k;hjdE5Q!Fma{1N%_SLJ}IZ&z^yX z;wr|b0#>Cs7K60^AGDF2`=64>$YHbSae!vT>m?ehc>Has{u8R+YkBuWsr40aK5a_f zzrxazwL@<=nZ*D3^Jj^v@xW07Okaz++KLLSZLg#FUFuWZ>Ep5Zh#t?ZZES+^+##Ki z8rU_&wuprxOp6LN9I$DlH{*nzfQqbup5Z%&^%6G$&cap*CW#T1pJ0#>C0 zi1gW@QNQnoz3h-7MHIWjYnw_}96a%WDQ(Zp!KbP{Q&UlsJbrw2B>!9Iu9pCvST{vy zBL|azfe0ozIC%16B^4F1r3}u1r@{`K$?zIPf6M+J%9g(#PebyFB#g@tV;E{Q60%y| zk613$t$5#2_|Wv*dwI!{LudD1S5!Wg5*oD;e}yI$P{&zfKe<`x8QWX}u3k~orhNr@ zLp^Of*%^wKpA|mnk-o&;8(LagI}tqefjZ|VImyWp;xAeH8%v)K?hyKD?zgrvtv8p3 zcvo5*$Vh0gw0urBzkznK4=;#Kyv627*Vy|C2layYC{|v!@t!si_=h zznvOUlzQ`1ayK8J32ieKYht-AS`#N;-KAAfU9n*={5x?|ebT1YtGiQp(a%$ReKhk} z+}>n2q~bu3GG-1Gm_(>PSb7Lz4667#4rSD}5lhN9Y;Shn-Wi}=d;{kEVUL6j0|@at zdFj>JT>AZDXIlU)>r$C+r!qSt@>h3MGNuwb{uTklqv` z;2coQQeiD#MfE-(r&KVA|Pd zmRA2OTo&q|BS(%1v)|19FLqD0<#^C+71ny7tPvX=6~l1}gTS+Mb>%Jfv_1~BpJ6(? zb?sp9VXLHl-^Ol!g~i!)rbDMp&6i{MIeRLkDO#x2d4RVcja62@(b&jr>?A&NH!x%F z2y_SAUPDJG@$fmyCg%qY8e|w?DON?g8UoAC@R)w&WjX`2+9#$kC(1q2kvsou!@d z_J-=_5;{%v^U~=>@Rl~9`oscU4MjH2d^mIizHte@i0Po@@~ZzVkMq*mC(hX7#f#C^zOk94)%5G+&gPvorRjGKy*zPup(yVv$1IbmWv8vY3qn~osY%;Xh zjFz*~R##`3^mcT>M(wgS*K2C*P$AknIf=S>G6gmdLF~@s)(W#O)J0v|otr3e<(`Sb z*ZA02$KhSl9+p-#2DdlawuqLtUCVjX&+vklITt9g=$B#8g`AVJ48P0bs3&YUTk-e% ziY$r>>pZV7%GXbTubD`um=8Og`!y{FG^bzx{xz4|KIv1QQpbz29#16^PB~TVBM^n> z?qOgobq>VB+&twmW3Eu&3K)hhRf+lev2Dht`3zH5EnS>E)2i&7Ziy6wP~AUot!{IN z1k4GVeX3&psU1@#M(3LJ&O7Ltlbb*8`|0jM{xe6(hrU@Vf5G$sAU{eZCMWQNN(4p< z2oTF25SQt5?>UuJ4Ip6zNT%+oScO{|tqE#8^{60~9PmwBLKPe}LMXP_8({TI)^Kum zUV3*R8!+6NP(YFGK4n42N__esOW7ud)jeJD=ale8xL@z!DcSz+=1)~2D6_3EdG_S_ zb7w@*q5em!$CC^uNq;amUKbJ0f}%YPp+t{sd88jdm~x6tPK1x~89jP*0uoR(J(16I zk)1=UsA_2yx9h+=QesQacWwvp#e(fW7ej-Kur`LRB*fy56Cb^*1-?IAi0YWq9!w6p z92nP#2rlXxG3x+7_Twf21}X53zQ%*%{f{a(pZQ$2Y5TEp0mn|~cIf|Mu)oQv^mJR) ziAr&IkdV{H(JOLAD6mAI&*$W=_~WTS%S+k=l!)4)HgRHm21=<*0R9>akM{PY#M!$f zr8=76^8Ni1vA-ug{+Gh|-MzaNx}8^F6B4|etmXPJ+pZmF4gmnQm#D`P>1@AsK$Zg{ z7J1d*8^xBmmD{!r!s*K4^F#Ke=_`g$+_|3YSfSG-X|tuSzj?oHX+s!~6CNmR-*cVr zos&XfqetM{yse0Le%#IeZ||UT#?zuqu3;P|A37p-K8wtt$G9+uqO{H4-=A^7!I8b3 zVlYOuyHO@@F24027Pe6?EcQ)dVJ8YzcYN#!5}YoD@YzIMnX8*~t%Wpk?)%0 z9aZi7QG=nTqvMx;b;=LX_^rgEMu4D1VQUM9mn0-4oHu9}{%y?c2;!L9c~5G?1=GIwqaq3TTklz(?3*7Ul{Gujbjby=`gRG6_Sh3TZ=5E`yZgXqFKsDvlm43az3m1;4)D%F`Snt5j1p zVfn>CR3}AuXC*DVJb7i7EX%=`E}FkHIM_lW_~dc-$+tg+pa%YRyP~WtKe*!Z-60-G zmnpT7jTA0(`kDT4_3$ub;gs~UX6Mi%@)8TUx``QO{*0vlZ3nfq6HWyab`Col-&XZj z*49H^Sz>l7HUgYa!Ab$8SGN{vd1cR;mQh?=lRm@SS)u;moMiN5vRk+Q{N_UJ85w45 zOA`G5$_p{?cx#}IUg4R9Y~1784gJ%&GYh?quy0AvHFRub9s<^h>1zf=cL}35I{W+g z@3$V_{$qepoDS-BokbyGvxdy%jwdA~YE0^vFPNYI*sSXz)3sCU zot?UY3}8v|Q&#*aT?&vZwl>Gvys4`bRR$AHt@-_5E~A42>{B;oqVnQy;Y`av)PXX4 zf?_2g7#IRBTCa+fPwDck^dq80qk;zhr12zeBf${UyQ&%*o#3rFnl#TVIp)B$zy#C{ zbSB%EpRxLMC;WDMP*84DuT-0S&rdZ1BAFW!>NAb%3WeGoR?QNIfj9k522$ z05bITn>V`>o{JnWnx+;~2mbvHS4JOMgj|3i%1o26khpLGI}NFV>Vnm#^E262b_}I6 zJe5gN%KHWoceKigV~VG~_|oea&Lpqxq$M;`LkZH5WSYB2zT885O_lu5{D3CDVp_`} zsS8~lzhl|*il+J1W20?rPjg2tJ)ojiz^aaop7uWb{#(Vn-3L~E=o0P zEgY@u2Usz{xgySxe9O<>3EKNo?8YVB5A?+wgx3F6gZrw`(?dGP8hkq;iG6UIDrsDn zTLz*`7O|9A`OQnmb=jPcV=s1ckb}r_0_|rD}qcEuk*ByG|qhbh=L7`3%}{`gcVMy1CMSDmmk$m zQ)6-3L(!d_*Wkn{MnSZygbzNvqJ!r zv$)J~nkNEGQJi_z&_**jfLeQp&z|C}#YIW$(J3HM!RZS764*egi zBxX`^aQ=lAE4_f=K>q%S{~H1x0pDwWQK8!)0ZBjKCt54IQAUfD#>7LB?inyak3|lN ziT+*ua&1ae&E9KkjLeFEH+436Ap5HBfB;hQ;*qyMKMMS1eysKlueP4*rxD&VkRZ;k zpod;|nQn15vu$l;Iqf*D>AzKM;Noa^g*)%<8?iAlg)d(Y6GmfXoPVqRrVa+pr-{pp9{{#v2X!IGhq+bGt{$lr{@FQVE%>1}c!HZcK!p5Wz*KXoevKmlM#t03l;kzBVjj~s$_OrWEq5{+*k2zF%J zi;(N}F{@aZ3t%YJbbDt%ll9&Hc2Z1DPj@(b8?l1mcvkd3JIKIULp~Hh(mHug)U)m3 zif8F0pTtwepF6?bBwAh(Bkwe>L@07V)sguYOO|}U_-R!^y}tYnih#j(iM7FV4f|f zBaIc%O@i@=ITljvVYsgUU){hv$2>*qu>n6+ZcBaCcqxP{B0D)ve7Z< zY@Mdt>Ryavv~Rb)W%Kr(1#OoKu8Ya<3EOIV=tz`T zI`T|QTAVN>DA@M(BXQ5(v52RZJ=#Mh{vqZk{knV1D6-F5vahL=FKMmapW=4U(1#%n z!gfRDu=>l9Q6~041RqMr-Kh2Wlp~CZYbX5uIzeu~4Qwpj?Y}KOPB~tv75CGtIOJGD z%-T9QbOO?=a~2gn*H*Miq6-`saSa7O1~0c2dPvI*JMoGs1R9OTvUR@Ulde&71`GkG zm*a;#@=)|1(Rg(1MLfDdeJ@_ zA<$xefPBcptuK>Kml{pK>2^Me7WRp2g*a(g~F9du9&LCf)F+&i`hR(N0#Zuj`qav{^h7kl2e*CU5sh1+o4 z)_Q&Dn*_cCa1IZ2?xbow9kRm7+mZLD2BE$p;j0$VNgm(1eb%IC;xL9hg@57upwLhW3 z=3s(&joNiuWM?-Vc@RY!WU82R<;~-(qQ2$t-=V#R@8JW`@bkf@FU-lw5eXEw>qZ#R z4oO4iP>SDxGk;bZV@w&@U=2~ zMwZnecmU=CVf%VAP$LQeg|>`_g{3A^eBGzkJnRf3`gNeWb`&$2zTeZdmPj8r9V8dllyn{jtyD=X)OBn zAVrt{T0&)$@4#6}cVV}MITR;vfQt1D?bxCe-S1=bu$HD?GO=l8In1x|^-F(W?%YVg zOS8Dwalr_8U|T_!wl*b&h_N74s-jjqeR>Dy4GOt2!1eqy%aJF^C=@cI-09C3OO{Ml z>fQAv>r}noCG0%%===*-;(eT_19;EFhr1CGo!9L5|LG2DA0`#&J{_XF?a1$hn=#7F zedX}d*PfV4#9yvyH`C)j~(!ST&Rw@fP^W~{(b6x`(PQ6r`u z_%bM@2M5wdiy+IX*0sqN@urrhoD1>fBLDS8cKGzlM?+`QwGaQ7@Xi=r*>O8ScthomGa@?+;Y3Fj7b* z@mBhJaTP!(qyns>phP~u7B_xEE2nKIXN&{w2@sc?I|TuXs^9jcxM_#2XB;lw{;PO9 zoYWVq_gA|NEsRuNwh+Hp$dqkg_N*x6Qo&~m6_r?KiU_Lp{Ps32CLbWf83f(1Bt$OM z=*#t?e|jVMxz=TR>&)i-lbM;tOACH3QF#u8FVN_AI->>C%os15N`I?=IZ!e-%y>j$ zw4y`Rz?q8D!`{ETu}8>xVF3YRupeH;LbptxhBaDd4?FFk72uVD4XeHj?pXP`C3DVm z!WW9VWR>mrciuki@_91kc9coc`#yA#)Qa@QKxW0SUX8f?qimhq&0Slj4$l6;u$eIP zR}8PQiIl7yN+Bo`U@K+$O;c@FvIakE0D`ga9LBsMqDwJ4TZK+$@`g8(Krh^h4IZ)c zM~l~9&dRjdos-2%XM}g~^P8@ZUGwSs3G+1Wr)7y)&$9HCwJ@{7LE)y#9-&mizeNB( z+kd8P_kXzh28(#tQdkQ`VEx7sL-w!aAQEp$G1T|{Rp841{I32x7$S|@&<`G6Ph4NH^{Eshj<2?1!FhWNwYUG#!NdK>lBxMD zy;|b7x}Ze=K?aDl*fWOM!k=gK?VAQuiuAKcD7I)@Q9fABTlcoxMrdwC=cf@P!81XS zQDHwit|lvq*6XEov|1GT3=|K3>qq&-Upn(4_=D%iRkb2Ayces zc<>}o$ad6`S*K?FG%EhL+h8Rp9TX(XLf}H;I*W(`3jgQ;^TVP-6h4*LAc?V9Vf^a4 zs7x7RjC9W5_CrmyHT(`+Z*O^4`G~eyFq?c19lYTf+`h)YVGfC&<$URohpZDmDz}0GrsGsFMgD7h7=e~!`}Lax+U^$%b$x^$II-oJXZ1HJNxa>)tx>PO~e48B{!?8 zstV_CV94^-$P_5n5diKzJz~x>z7Lhj@#DvFz&njN4BV)eeM0cbpb2MT? zg?6k{o}kgCz6oqmU}Dq%)J}(-#Ws-Q*4b}(k&kW0o?4Ps^9Yl`+r7VPxLzN**9thf*awH&w@efDI^=hz=#5( z`cdCi*x#~5JST5wd~|fJi;BBth~1p*s;(=uZZ@Z|M9^T+A~ zPqG9h@!UCK$0ww;4pA(rnV<5k(HZ2s25U@#98woHVxFYt!{~r5AP|Li%&=XFxp{fF zAC@8B?Q6K+Cu~FYGjCH}4l=lg$%6kbdvUENWL5cPFC6oFo*3H}3^oc%l;3uAtXi5{ zQ?Xa9-nP!*X;bV4lCdJ2gg&blT+p#0*xk_`$Df7e}lut;J@O1!Upc({o7C|U!oRo+&A#(l)nvPAkq zV1j0J8Bqaxo`uM>wr`)lRjY5>s*GX7EuWcw7jg-QK6;NcLWSlLE#HO%!ifp6K`aLi zLCrzs)Ya|5`E88>LqQ4tggV%gzY`y`r(G3U2r&Z^rV&Y)4Da*;gdYtEpiJY=?X5eh z1&+kVVj;d|S<7XFfvs1%=rV37klq>iv=094RXk4qY237}Vp@dn&YerZ@IYuOxu1=v zHs|+1^}Kc#GC`pU<|S%A#9l!%7@ZNksM63Jt}oY}=PPc!A+Sd5<`*FpM9oEsuI$WW zS6TL_I+G8XL{>nPoUCYY4t!JHHs#8En_;QCRAw~S9zKz9jy}QF$CS?R#3=c|uzIZ@ z%D3viF6nJjx=UCP37={3obr843@V0)Tn;8fkcRS+;=(eGdtTe^%)7)4-t! z;XqMoT`vw>A6m7`BrN(6lybOCSIHdIJ7hNuT3-TCWR~1^Gecao8lXr~s^H17K*oW2 zS-+md|CZwgbA`kn#yvMMux752G*|pHmr~=x`nvA?K&D>zA%Qqw_sw!xZ!wdB~la)2+|kE$M-j`DT>JE5luzhcM=M=Za=qXGnWny;eU@oQ9C-LWtbWSkjmNWk;n!AWnH~(M0WCqunn#?ZhGzDJDspg_ zu4D9U=H(X3_>Yu!+_y1)YCS*+0(I?x2{zV~?mVpDD5QE1SHo|t>>mz-9XB{kZn9i< z*i0`s>$_DW&VNOfC3b(XI--3$Dk^{W?a`@W&j_tR1a{LYgkLpTzBOk)L#@bbR=T1k zsGPEn3__J22#ro1`QrDd%{X+}6Q$!JcN!loUNC&f{+z_>XAK7~z9!g_Wk7F>%q}xA zAaG>QzLsXI&su?3vmr_l^AKs|0`6AHu>WQm`e0w92UEb4sx%5TH{(h%6E*jo04z|F z5LW844{Qrz7R>+Z%ag|tQ46gl)8V1)Y_J1<5IqjETDi99wwDM!%J&XDYsf36t@f(e z`r^7=_=*3Jp$e#$@(;WQh}nZ&_ozAhSp$dQ%C&3PdQQrFXAj$WWBKOiL4W^z{%eFO z?rCGSD2I$R>%l4`rn){o6LM8SVHogliHelN{yFOHORt=f8R55s5eUv=u$Auj2a8S2 zK?o-M{M4X~gww$(M(dLwd&PBIax2(Qo$n%7iRXs?(4W1j(3zp0P+t3G@|u=EnSZ|* zSR7G-uHi;8(l5-1{w@7`aii0CisZLjyKDIE941Sy}cQq#Wm=H6Z^9i$UoX7hIak!7Cu^nGzDf4OJHuMud8 zgslzBCj^T}G1^+Pz}FvKo?0G1JX<$6n*6S)JosKpGVV#?CW?Y~BI4n%_3BymAI*-- zPCTVvgMFb)iBX1yM}s4Pdax(47H^i4j6U%mEQKqUTai++*41@y(EHh8;T;ZXTyx8< zlM!Mr1~(cE4@X8;R_*ONj`D7xv#nKQfn{XkgY6A$T;R+>58Ubv@B_U;sG?XzY!q`V zj%19=2}oY#<5`N~x%KsLKi|h}79+EdflEmfLyFDT_qp6k(3KoBOxZp=<-geR5wU`o zBK_ksM?tv6&_7Lc(xjRH7Gq+ugwLeVRZ>z^;;=6!;!t_YL~WQnNa^XL7cVZWc=_1| zixCZaI=U`q5kw*RRmSgR-ghglqs<*M?XuwyMOZzzcM2yeU<@$?Kx^p_`ThI%!<>7_ zZOv(TB$N=hD=O5p_o&PTe=%S%=+(`^()UVpkO~6aGf+fy5G6?Tf$;b~a)<678{d*B z-L;dTVIsyaRh>4iY)2?yg;*wqSWHNl|1Bovdlh(en-^ZAm3^k*uGz9-y-yFQ0H$Zi zkV?0*8Tp=|{NPErjlme+Qhrf2fryGMCfQl6+&KUI=`O`1z*N8z0{px5;=78bkD^jG z-b0XoG`@6HT=z8hh1D4CTI|YCO$}*zr?7ucr!E%inC}_$oqmkF8Xdas>6va2 zXTZr6Unnrd00jzqGIPr}dM0Fy?XEnDwCW6-jM8<0#PY-kZ56j~+tNJ|pZwYWC$@G8 z(+bc*2elee5rKb7E0NTdsvOm#%;o0f_c6AG5+`er^DtXzEFGUIcC&F#z*mtzGBN3R zZ==qTokaMVIYRFzRH-;fPX8JLP5?Ca%q`TTCEZTT6Q{wTd;h zbP?D}P&cCZq{V~BWe!*_X6CBV2g65=bfesB=x!G{c1+3aMnwHQw!N!M^BZR`_ehG| z`_Ak2LI~!lhx(&Ng}rn6-Jk8<7cT4q2-0lHX7c;|x}ivx8~voVwd2AI?55)Fb(r_b z@7Uh5$-QV`PboTZF&fcpba;s<@(I$+BeZS2rchB{zW$c}fJ4|s!lQFGNJ}=E>|~lr z$Mkp0N!2j}Cnzg_88xS5ezIxSp#{w5(-eqBX@m)sH-9c`O1=cn1HAzVuoX(pxHvZK z{)#AkK2Tns@;C(1A&n#@q=-D{&u{o~e$xkY>kICFOZrgR9=7MEpo4LC#Js2FoSapk zk{xa@pz7W{coOioWSN}g@-%|O+PzB%#QELsRK{WGf0!XMv zNi}mfzy(IR(8+MuR+=IOFaV(hO0(3Rq}9$%V?3m&6krk$wC!!Z^MSV$ zB>MQx=0UoPeoAaj(&dqg0+QnhgeasQNI>#zCQx52$XTaZ0|g|r3mi@I*cb17C54&O zyfJ}y=!lw)6WLSQuZYxJ?Cl+lM1bm2MX!6sJ@Im@tSwL*2Rj7-3qV z&9Fktitk9i3^m^qI1P0@BLa@mrawQXW}eE-d>h+%@18W*#qw|)yzA>ocj^wnkFvWI zsE6CmXpVeP^OsE5e{56l1)Q5SS<4jCPX?YKU*7omZtA>^6pA1;sm z-MXyi|AOj-yu6s8C->_$h{1f-Wrr8;2s_$w(X&<43>Hphrc9cZD8V^~l*wEjU?^co zfm;ivVUHDN8;(#?QaVQ64kf?`5e|nO5xkio&KdClao8ies{Z{wPA_ivwmBp7yx*{g zCEtTBG%S}-I5sBye>9y5SdQ)1_cN97h%!W(QjwA*WhgXA(Ikn=P$*L>nTbfG6e>xE zj72IDNfc6sC{lkFLp86nK!>Jz0`p@6)sZ%Mj>1WlOKSbHwyrOy<0oiX?*bq6uI8 z8x!|l=~s(XW?jS|310&qF2_6q_UkgJpQ9IaWf_Eei7(L#B3X~$CFnj%;`EqBi$A4y z{qSf-3b6fY6gVIaX!e=csJl~recLWHj$(!szHn|%`JYo{&iya`g%2o%Hmmf|@Y3D!s*0VqpB@|gp z6_ek3EX~twzlARZjWrV9djH5v?~?lZ&`AWhD*2W}&LAj@uCj^ozH$yh9ONcKWPXGt zjh!SUm)Ka7m4~NgKk^^Z8*^VVpE5x(L<@l}wMuwKvdB+iaLeKQ!JiOPHvBLu3y!c(`_2~rmwsfe~q`pdgK;JsAv=C z&4~^ht>>~#S_k-yY)^U&0Tb~31@GT~q@yQK-p_4aw7pOJcA}Li zs%4TGt*zajiuMju1DBQw*LS$?vfgGZCx`uf#@62d`2oQRSO2Up3+Zs3gMi zO_3lVs8oABb3S_0!?FcZtJ{HjWiWv=`T1E)BLoErnp9?7ref_4B$e2M0UpbI&G$~{ zPx#G;txHHY1el-~FV$@~&(1E}S-i2L13zvxX7$!D2o@OlgR_-pOVOZ^;Rg!^~~4Kf-BwP=>DayW#SIo9mZovqL5b zZR`(^(~oveUvXDw*1ckoxYrT#i>V-+p7L!ZMG2m?WtwWETl@0kYTK90#D97c4W{p0k99x;XJ% zkwUL$7hD-%IyjH~RW@h!=(%-KF$O=A{v_moWakU5=-s{whUO^i(>e_qIU1fB{)3!c z-T7T5cZ#tWq#Go0H7lOpgOrOVN2kAQY|k%`$vM34YH_YGxbg^&3G;!CdXre)bidy? zS}nj0I#yQLFh+^^M+Wo;G1!UXW}-y7&q7^%W)+P2m|>#rWHa5sIY|rdA@~((*b<`G z;wr$p_w+-lvZLjXLBJU-l`RnCA>B&X5+&nf(t{&GA{q1|0Lp>i)Cegg)J)i99 z)bYu3GC<}jiK-)Pbh`#*^$0ZS77ZYQQ%>Wsb&&j3KMtw*+5vHMemhZDq_ilQOmf%P z*2pF$d8pnDy)-ay@;13?F}AGIF0EJcITNtkV#$#H-2*_>=gLP*+FG3p-x?WM{zo*< z3_$4gR=-0o!PSamLHHYBxw&HMb}SzdYcpKDQBwxr>6^;(Q^psj3ci5GBULX<+Z{7g z7MeW>m@AI$-ipjssUDnjRHZ_kL-z#SMk~i@x{MDQ1%eow^~WklN)T3oiR!&r48(If~Ia4SgbBb^X?aZSa{CQp|0*!Smd}Y&zIcRemP4%#I*^L z>4Mi74fxSs)+zmFzhp&Sw!7|{L#pmgkSfOWr9)YaI#+wRe7EEej22}T7&IR{F6|yr zeqZt(MFw(%?vB=meE@>49W#5`aZ2W|{}}u4f?MI47JRH&OojW6k7Bi&`&OW54IoM+ z8g-x@uQE8#=z?Up->I`U!_ha_XVh)2Yu?;8!|NG0zpy3|gZ1B)hkJq*zw3T_z3ij0 zqH}R!WzglGQityJINQ(CuM!xS`^eq07>fwH2D-|or(Gm>Qrp1LSgqxU1DAUoogj`# z8#wN@6GFH>=l31%Kd|_G&_Dh1?UpyUGfu7kmFIKl>Usx-JI`ko1l5|xe9N)uJtcCNY%IfsrWpyr# z;oR8L{L?iKtZ;!m*#P*4{y_fQ>MVmxAajo%bzwr`rR}$skgqJ~9rLsVKUen-Ln?oK zJ2ZXt+BC-=zv-L=7>fsK%{yI`iYP?DIqq-%?K27Z4XftiEw%58jPo<;A#h(LZ<_t} zj{mXxms|Kt9^t?Bch-7%2%Y_v&t=B-)4O~E7;zh-)~gH9PNT;rSX%*Zt=v{vH9gvn zpi4nI=eaRVI(N`klBUOR>f*Xe;@$A3s64k4!hnB1+rP~pvFOOmyzYnhY>QOyZyoT; zmbA68cV5a$zLbnHJ(Ct!x&>s5Lm~vJ^@*Nay8^6;1Rd~4Adj$rUiG^52r9|^JG^nS zzJBn;uh)0VJ?F7-XA$yo=Cj%Uy-PO0BfOHzf$nuX(mu+fq?6w7OS~*MQ)fA>B&2Hg z>`tuyChL?ted|k3FfTB73I7hMAyi-!HBWDdu~nnk3MhnTR*<{Q;ice^&Z)Bp0*U%#%*}&v~Io`*UCaJvh4bk*SMdHZqBs1)pPbNKrSItr$pa9 z@i*rLNQhxlQ916kf${D*=$H21rNyQSBJ6L|is+ME$9rj3T+_;C6CSgNoQsH?X)m_E;%Odc`i#+auQ^F3}BN`&w zMBrwJLIX|Dh-ipcb{@DT1C|2_>mQuAr9rOABL0QW_*rrH8|QxmjsW-jtzXS;Yh6(HkKO5;7CPxX(KAyBFhISBdW3x`Ag$OF7Z> zg8;os-!z8a&B1f;LsgT2c}TatrYVJPN65BU5hDmliEyveAJavizj*OJHc}8h&yo(q zWTgqsWT9XuY{8gA0+@H4!ykXd;to;g>N+`DMMbUx1cL?$k@1+2JofU`wSKMJ*l`p# zTAlt#cxe*Bn;ST;eJQ%Q=Xwu*xn`HI`XI=7&{!gBe#}wYk?92Sh6PM&Uu~Oy!_f<)s+`lFOW&T`9rXP_>{nA z(OIR(wXV-_9%eG6?BpDKd)kBga|m;OKJfiJO-O!tXR=SUygmWqBR0$NZn;F7cv~Mn zapHl%6%n*>Vg&7*zUThvc>t7 z%|kyYuoBJ`10j7%e77@vi2UN>N86G)w4i|{dOclNk;o(*I+uI^X9PDSEMQ9=P#toi z&%5O#NH{Qh8ik+nw|}c2o3!;vzp=MR(Xb#exILt6m_oa*owWw-e*gdk z1F)&%sS`RCf7k17{UKyDYfBc)w}}!Ieu#yOZGe{&kcycAOQ^mbpUBF8U+BL$?i^JEh;Ip`0ZY(FQ1#LJ5lzycBt8< zAI)rcTjvn=+ikjh+KpK{*PZt3MXNjfsr8#kODufcNJ;B5(GE3yPx3uJC+_)09yF~r zdgOs!!s0tbNy$bYc7uUD+G)^y&Ax(;W^St-od41^(H;T(r~Cffa2p|a;iaZ*QVOnp zrgMkL7XM!R9ZX#0LQRQnDWd^lCZ$5$`BSYi5GKuVl?@ucHd^{9`Ijb$J!*=uQK#WR+rYvK$e)1exqe`wmM zpYU<2)-w#Jk$qNgmhNyV`a;(oV;+vAqY=22z`e<+Mx=2nnu7zU{nReezGvGdv9J9>rXR9wil{A2p)WVUpKnX<<2g=j@EVS|2T5-@YwfculS z*u1mOy67WE1P@IgDMr7S8u1AU6$1~CP!1~_jaY)8Lu^b!W4~kSo4}I!sFzx6B*y>t%OSKF#7YH6Kcrr3~rOS^lg* zWz&lg-B5c%9$!I7C>LW%7C->L%CM%Es5VTHe z2(;E(yY@@4TVjyZ{4OWhvI&k@Y9nKjm0Du{dvm_cqbJ|y6%n{{A2)iq)e4uKI{OCi#&I$ij+@g2z`KgbWpDhzN zESg$E7!X`L%s-(O1&+%v7!|-U!4cf5qYm&wJidoUKpJ^u?lcwYDDp?6w=KQmKp=ts z-})?3-v;{B8^SpME%JNca%~@T9}Wht;7rYCy<%hA^>XZ0QI=Jh9^b=nDJgT@3Wf?o z%nYCBusGPimQVJyKS1YqzThBDmCfr0>HgGYC(*k_`|CGPt47;ldVdvRG0Q7fVeMQ{U36K8Ml z#*t4=W%+^emx?@yfgKaiomL|PHpNj)Df5TP23}7nIp3!AvZpz@I6c2^fRYqKSFyAg z)v+O?can%B;R5To2XPxvG`}~h7ZEtpB)n|FLYU_|e7RTX1`mod|6tP&{sd!JlGg>5 z-fH^%cggUfw)cmY?SNFr_ar(}A#nWoI1-TR=Te*cf*V0E7uap9`uK76{d3nmGM;`j zwVinNLXO*&gn?088jyq$A`DOYmt3!r0^bqBSNc|Qzt9HZ@a1`!r{XgEQTr5+x>>fe?*8c9OCf5q0D z`pyctlx>o0KfITQ_oZhZkpoSB2VC8uxA}av;nNo~FPG_vFRw%Wj7ZJVpF)yFcO_(_ zv^m@;^g@dI8xI72LKrmR_`V&A9b;5B{1hT66<=`r*Rm?3M(uyL*k4=eXrB>pbL2K; zc&}Y`cd!y%rm#@Zwy0mOxMq#SW6O#zaKt%h`?WuvUv+)p%Af-aYAs`yM_NX9Y#C^@ zIn;3pNY6i54keP%J_`W}sI5p=5X;&Y?>E>@kz$w_eQL)POW3Jr0T2sjs^Nm+=E;kO z7rzJ{zevA=_!3k%u@u&%%Wf@~h|L*t*_B7cr{=#%^H)6}WvG@D=BHQ!m;fNrrAwDr zP7i9oC+qh4)vo96wa(5WP=!ApG%6E)FPThm1tK6PJ!XqD3llIjePD&+6bQ3DLHc5*r`~K~IXD62Z;Wh|BrOJTGw+r@sxwKbKv+XFBto>9P5_kxJGg5g|bN&Ri zD2wF%wCV$9q|=K7)`<88)ML+<9lTex%tj?*xly2HF7YnjenERO^CH+?kT{l4PY!LH>8&6JBGUMnB5j?|jH^WfH+mi)mH z`Mb%p6qzHFui&EAW64DQySA2h?mY#zK$pSLvoDf)cHsH}(8wq;)cX_^e66dS27e+t z(vp(g?H3>k+xjg^9N9$wwpe#eR5xbpXKd$bXjsjY7c2$Q9ZynG9^ zp9O=>C(fQVJTOzX(EIz`8Zo7%7GEg{UPGP`iE#&>~){H z%*WcK4UXcPJ;7tNGVa~ik&FO&FSMZy0}PR_$IpQ#_38dF&xi{PD3%$D>E1ISflTi->B?Kfk_!jl?6vu!_v1<0m8S#tf7V<+5LwkFOpT)V2(YVg zH~d(P*MYv6@cx!pf+i19r%3%Xk6OzL+Kf z=P0w2_)>+pBr?*WB;@bT;f0~_TI;|0Oukwcyw$3MAQ6sVKfR7&5PKk zIf>^wW{3-SJxHqlp7@Iw5giJl5L!L)E&a<#$a7n~G#`Z;v+2t_N>Y#bR=^p4`D)h} z4-{<<)=NOx58i&iC4Uco=oH}Lk!atQ?)iBdzXQDsS1Cr9@^3YTV(P;|fITBO?3~?& zz4sQmzrC^U?N~b6MbDo()g1VMVMM4v4@5?ab#;zrOa?@JcC*hJ8L`}eUFt=yuaOET z8byaDn!%%F{TBe%weMg@skp1$%*;mfJa}arCkSy`pm^`rXWZWWeZ_H3s^-CLjYr1^C#5cUy!B~Xwq*Og6|EC`j)dl?KWZm| zgB2T?TgavFvH59j)`;(uijvxIPRJ|1x`AD~&|+>kd&AHWx$-Zk@gHK#JR>TO2y-v* zd;6zY(K57{Hm&Pe+lW(kBmUuJ@LzOr_i$f{O$J)>93e8bcVnlB4z{+mY^ciYCv1AR zpRC$fDv>B4it$sBSn#LFbj8Ob1Qd7*a5p4N+UoTGnDFe?tA`l`Ot^R_J0?~Z9MFoX z{m*AvhS3qL+tTF?dO;gXNhuReDb9Lcba)%Fahl5|*yG6I!_E<{-%lQVu5Ba93!*>q zVTM**!4RGP(p?z}wJO(h(Ewn=V+Y?WMJ8PSD$i}qg00_Y3&Ig^!N(Z|`gWZhH2VC= zzJZwveFv1WSL@$RNF7VI{rTeF4iuazOoIGa)7SpjP6tFEoG$&|aI4CW9lCycwo#T2 z;U^Du)Hyw*5C96XG_0n5zfgL;b%DL@F3xheNr3y{z4ZQ)ClUgF%XRKZVy{mW_cod< z8W!=_;M`+8W~TqVtP#xDne!<$*g}qg@C{Lg_K=SVnaBpb!*1zqB5JF{pq~xq zR-LmtS3XwLtAkNn@q6=gg$dc1onCb}iL=x0y#3N`o|3qoDZZkSI}h?a8b8CU^K|bW$;tkb^T_^xLv1;ZYC#t5BRX(-hVmkY*FpU4Bvm&kZE-0WBy$8cpD^M4S`kO zJPhI#of6lsWL$?TtFF*R^0HADs=q4FmA-_N1MAQ1aQ2lTdS!~Bn$=%--DPyB_^f$p zt)Vgym2l_&%guv+Lett1Yx8|ugmF+5G{U^ujA`(3^~O{B#NlNUQug*c?u?i+IjvLv z5kHUCd2^;<<|dyn(?h5KO#CXdw#C>=O91Qov+)>32C`G?6ReqFLqTnhP28|=T`Iix z;oOBzPJECZH1sryVp}!vKe85N;(v4=<)Z7FS_hEcZu_4t{E+}{q*Wi7@o9(Jn}nkF zJ+lwC54mt4<&W2NA{2#Xg0V3&gDf%8CvVfnLwazn|?#D$^OTimnI) zutgs9lTCa-HZ>KJi2@ES=>C;D`V~+33p$KN(M$#L}KdnhYjxc*EnY5Ma#z_ z)0-6r-0(sxd-N^fCN~Ag8?-}^>poxCoY+Z7uqfvuY1{Wts_)|$Z@48z=eEEBeX$t# zppb*QsE&zumP*1C^_0jZz zjOh+aE1c5aBmB=>_K7LD^$DGz8fW!avvaH6Y9W5YrAMEXv666<)zk+DtjbvotI!8r z#+!N{36NiqwoFO;xvI5VQqj#7s$f*(ZKQ9pC}OU(D*|}*IuJX-2MSlD2(3QC5R{e$*g4MP4Foj4GMv^xat{hOUTO-+92>Cev}~fITzVpq_riu{rI8=E{DdYQ&iUK z9{p%Ay3?SB^G^&u^(=RmFu%xW9?VPI*<%ral z%M%LBy>Gr{(Aku=6P(IVq|KH$q@QZ6%Ip4~c^Ugq(_sH(wX*dL)pc2;D7UO)$XMVz zV6e-`YlnJR0^_qTDz_moum9C|0P+9Gc`7pPES(OCiwz$)g9V@x^?s_*O;X5Ed65LM zCi?e{`l26$1CY6own%WhSXtIgL42kEt0G)T-)U>={LVZZ-gQuc8X?8;1N_2e)q`G5 zS1z7nudO3BxNxG8$ISIlI=Rs3AqqAP4y`Al=nl*TrJL|eDdsi4Ew2;Dox-Yq%XsP> zkA38ou|uuwq`AtN)ZAh1^I?1W;{8bocrb8FH$XUH^bdx;ktRM~AbkuwA_6lV&m6?d z#0}j+*I}UC4+%jqk&un+hxC=X>>@GCVcPU|YrAp=`@A{**=UgI#eSC`e-y%iS+Ui+ zoHWFekkUURK94e+XUX;=z}Vb*zgIaQ%b7{6hLAxl?PoZtF*%2pq=6{@&1LwmiOd3np2UhnCbk!;IRE*<<+*>rRmKPYruUA?S_3fN|x6N381>gCQ9uFZ#Hy`DDFr z!T_!XvC57?1U;{?{BTu@399*a+t!Opn0Mq9qA?YU#BrifvqdsIU>2hVm_r-f60pF%gYKTD0Xk!d28UApy%TQ zm8SLEFFDP>E+$|EKF+Wlw(9KX1Biu-F#M$BbGr(tSGf6;l|wM6&;WJqv@awCXXIUE zCDKB-=eqH{#<|>S6PDSnnt3Vht;ayko;j0JPoK;g?~<<8p{jM_hPyLXjyz%{cRoP- z3E%G+|MS@@GQ&T4=G;9bKZy`YJb(uKI=^YFUFH^=+B>u$*gf1ui(i?j=oTE^o;hDI6EVy(7(k89wHR| z%SHpeftxCN3Jx1qz;lAl&PNp%svVf0;VO6QJ8JZy#G-vTjW<*n>wdgmD$>44YzC;h z)Vo5U4Rj#r<&us@RGSX_G$>OeZe-4vtYtlxz}bMcGY?u=S$)7(B7}&5S)@D20LXJ^ zgD8m|!9-}3)vN}V85+Om_`#ed{4Nv?G$2}B(wJohS;|c+IFtS%8YgBkRxqkPIkeX0 zquVe@W@J|05k`#@u*h+W+QwM6_|ERWL#MO6QH1obKI*-?3=DfVJy2U~KJp+*2s8R| z3uQDSe{Vdz^S5ZU;Tc1D*L4>Tm^m)x%0}R6p)_RLL#`-cS!97IT{aaG@lx-xIel+} zz+3A#bmaX-MTE>CX|cVEY@kAi@qAxIB|cqfp=qQ}o8QkoNa1-AoDx5zah%)-ia-EMWaBetV96@ERx9sB)mV5JWsVBqF`_7$+<%=$9Csfspx0ncn65 zoA+CnSk~{COjX#d^X-oU>$~>!{)~<@;iZ|Ynt9imu`zu{G~a#P@|Di`5<0rss&CUu z%AB<}zMVQf{o~Q`2Q!zCJLKeDyLJLT-SUa=esw>S`I4qmxDoa7P$J5`4r;Kb#_4|+ z1=5m*JJ&tOXx63)oXwpY%!C{#Wm(YVrgWUBIXWm)$I!)?g~RGjl$do#@2So6ro zqMDrq?beH!yg2gvvNS&{9H+AtE*4Y5X_NBzG-#8~%1ptb(GI=EQYt`Lj@3VqJzLKD zl8s_u`+^C(dSKh>1|S{y*3R#O-3U5A#2Spdw=BHldxv=sX&PNHA9Z&Y7lBje`VAYd z_>O7Uj{gF-2)G{?$EM*Xvu%}S%Th2H8Z#FNQP;ks7OM7#l~3JNC@akIRpT;t)~X+2 zJGe*v=G}MHQ8x)V_- zj&755$=d*h!5c!M6{bqAiQq@Yor}mky+8dRbTDVDyxnW!;HVq7FZPy69O7A>3+}%=v=GMicE+>xjdz6%3TsP z)yxmwEZJ#Drys?(!P93I45+QGUHoA);$yiiAx?VsEVDx#${Hx2Z8po7T7JC7r3H@z zR4kV9U~l8NazlH+F?V4&}c!Q&{z)_GZs{!MmkzTRHrG)83IAV&S>rjiyf- zBkC){tryXj0<81PLW_3QxkRs~g;!db#ba#C$g&bKLWt5|ZQaV(z`Fq|DW=V+D3?4f z+5gU3nBUoQG*6P0UcNTe8nGvyt+Mvt=xxmvAncM)-3qRp&a~$814Ne^yQwS7j8WkU zVp8~;Dc+P{vGLx%hsujRVhtT?fB>84Z-jG=ns|W5RPWx34k}xj4ACNNm?8_+LSCjoIkq;eHvr_^pPQFxCTJfv8;Pi#;nzfDk z_yI(+uaGUGeWJAPopiBf3j?o6CgAe}rTvg`QolK)Z>i&mv-C#4-ji6%|FzQEp<8{g z_2P1$+kwWV$=PR>dWUjvLvmCW$gT`{zZIrmR83va(p=4L%DzU?ie2~|U5 zxYeZxTfU}A(amBa0=5@_B#0XmI_)7%qEm*Ig++p+XU2l5rujBuj;9l*$*nh9YaU}8 z?GFEa(e$BIROf~=uftV3Iu&r)i`P1Q-EfqgW3!mBWtWqtxeeae025f=*K-slUy;j~yr?SbIZQp8I%py6uRF z+E2Pk8@Olr6~E&CKOZaMJr^Cr^Rp~QN=R4D>}}L{P)fV(3Z>Io#ZM3Dq*X87V{&?( z;ed`V^WSvu(RsJdy=84PZtpMd@adE_77E`vnu8%tWtjA@c%wq4zn`z&Qc`j_cjO=G zb5?qevk5D*O#yHw!3^Kz;qQ0Z2mdY zLl^Q??MC}~YuC7b#l)j(`Lp)v~m;uy&T(!%)8JX~LTnFutY;@N1Bf(kI` zZSs#TgFjUkC$B|Q?lTkcn0hE^>6jST-nrq+HlDp9fEM(jWh|sbd77>7CbA2`f z$6yE5Kd(?{F^Vquo>0}(XN(t^Y*$=nt=1j8oQ|8QwlLpoC5Sn#V7zmT&b;|>B(q;^ zj?8gPa{ZlKq_8AW>MKNhyu~N)byv=xKpU&k`pvqn*T3GHS{?3D(C5q@PY;g;DZb}U zt-QEac7Tya*u`e1tgdbEz_|$(**3C6p~>1Wyzvj>!B!6yEqXHGaofALKfa?0_<|jJ zvRiNBt^{Zo1awcU7N?I%^fS{`WuwE2l*XsufBB$G%${X!unV0n9{{J;nrj6$YZ==$ zV!BE&#E1+AdxK?q{o5yQSj}7~Bo*97NWZz##Tp$_q_P+*l^w3~K6Ua|7nE1q9f6uJihLti4`pVsb&-08hRllIPg z7Lf7)rz%|g)P^;aA1q9{_ojMl#f`z0ZeMAigkp&aOJl(}c1)GMt7D}w+q_U7aLZFA&4lDRpJV@k9b7aN@it$e4#EH`B%7>@40rcilI2mZRR#ZwTxon?f~) z0{iaUiSV2yf9tuhl3@-uji1yQy5DP1A$wdwTdg`#;exwQ5ILE!Gp5h`f0HOY3dIy$WzaXlx2U+EcA-f%8{W1GT`yXQ$FKJwMo_ zMeo%{fpd@ay8k$GBTgy|fDD#fzNkF$-s~Qsev&@C8v+uK%ilm$d2@09M$=J;Raap| z()jYrejI` zX%M8D=oZ*{BXR<1Cvg#jV|5>>zlSSVD2isMe!T|R{`ljfA>$#S5RcI7QB!FEk}Wg>_Z9Y^@o!72)*8mTgX``{D;$4g zU1?Pux8h_SI!+k@Vm7Ea4h-%8QgX&cn~9r-%6(gtQD+xp+Ysb2(rc&cx3m+hHQ%W9 zjhbqBWnfG_?{4rUec%i0;-apL?phVv8Kl}RoT%uuR}%s${%2=qD%v^Le>}gxQd#NH zWJ17c0yfT&^u#;W+Bm9Bw(WOPTM*8y+HJ>G)0TWodG=@RI7MN1y*Ul6je;P^EwPVE z%@d4Q?q32Zb=ap|;oTGhu#m=dLvV}2Ev8`DkuNsSSIu za*oW(L3XECv2KxLFWpmyN^7zW75ik%-8`xQK#(m;u@PVCT-ru*%C@IwrO3g%qBa< zztDa+mjr5LZ|^Sm)VDoXcsWY(ILhwzeLF5FoOmWfvmq(R!9Uu7A%%56|Kem37SBDs z^vjU5hnN6q=jTez8xcYKxJPRKH<^}h1qwHz{(x_8w{fCTuw>{~3oPJ9rqg$h2YcB1 z0@T$?4$nEft!Bo;yR2)t?&4tb%B3-d{g}8#gy)-CR;1%pKH5jj$cPB-vo_f6NXHdP zE%0AROZQ;i6x&lo?hdLmn3tYJL4XEmCf1C3(%Wn3hK@#~LJ!IPm*+ib@QA)@Zz%a} zD+<~nfy>7t{~bFU-hE2b)*}*`iQ^taXa_Sme4)buawJbvOL;Zv5Woz)5z|liviNpH zZD(b7*JYkoTjtxwpKZ2uPhm&PKz(jW`<9c*qvzx)AI%f%0!^MT(QeQ@K4~ZliNczB zn;0wCy}dEK&)&opL)Fw5ARI8)o^agbMOB^GI3(8ct2Q3eo*XM7v0cL4#L%uXH)`FM z_5JN{kit#ZE|4f0mGdfF2d)+Fj6<&-7A&ajcu7-sC4L-1yv#kTu^}bQ>fD?i&)%(= zMKsA?u?Pew!@TBZaVme7R81lWbnn8}#>jZdf#{<j%t zSekd#nj9u zGX2@ysfFlT1`nGfeRv9^GK%=d>~@Zye&ehlXh2SMJ1Vd?+X#T718)#tNWUIJrYsWnkef2z zh~1103_Qu#vv{K1GOwhSQtent6Ca%G?E*R;HtZt8+=UZ&*mf>YEUMkqo9*gz-|zcv z+P|@fTu5-o;m@dRhd$=T{7Fo;b=*Axm`WgL{mn2?i6*MPchO%r$|K4ljyNMBw+4Uli1ZQPTwhYt?TB@K>C=8LNV&+ z6y=VeLO~xN-=UvCs4{s67%5i<@Hr`6&?CqK2!X*=6D2<|#1u7hN46WH)9WO(s!UV3 zi|VC(>!q%5Y*1oOkP63Xz#+f+VE~(uF~%h1o6j;c>e{n8vUQNbVB()gjZ|u5eGoSG zMQp_zV~qlPmeDX(RT~?dI~|7FrZDx3wP8%Xh;RipOsujJ`?1QrSl4|8aV&7&)jp_z2fQ&oU zvL`K4b#E5@In``xx?E@DnZ3JMITxF1lPsmGW4;twTEPv(BTC^K_ z$Bf>%>wYfHP~h<5mH3;n%Um{Nbo1lQ039G)fpKX7?#nshSgx@;c8jA@6 zsG3_3bKKvhn1Abu=2mc`_Fi~jHI30Ta{;@eYWhU z%d^_=-P)u2S!Au`IBY(MEuR4dFta`K_cM@EB5ZM?7MQQt)B9je})Y~H|gl;q<`G}ZlRQi`NcXOnWDw|o5!X9 zN_AI!-;L}pu{Or%eTPBLp-mJ)_yn)wDUAWo^SvYT3faP(5P^+)*o2aY`r}g{*jQ{#v81frKdU&pkHAy7INp~d%`_V z+Qtcv^T(Y}gys+|$npiDbA|BYw`iLqZi{x^*m-QkmSsg2y^TGj4?D$H-rCT`z5NLr zFS3jp>q(4Ewm>8O^%f;r@JenC{s_5=m*0aPYTUzdgHXozk7<*!o6WFlP_h99Z|SiBsbbrzJ>l5iu`}#|6#J@hxHr8 z?S`C}IilWVJ)hTsWih)QjGz@sXu^T~{2let~VU7IDWvF>N6nmtWgXxHPf3kka zfwdbke{46U^c33gcQJhK%H6X9&57StEeNw6-s|rec5Mk->utdw7#b0u?xXEmr}tYU zf2!ds=@fOHt%n|ejMtdwJ*K^UX~VvUJeZYj3Q3=6E}A7SVX4wl(Ypx!job+>rE~;A4z) z_Ir>{CtU7D0_3GBLA$BdZ_6l>+<@f0_=yroS0AA}E3Oy9ekm+hc&fN}Md~cy0Bu2q zN}Gt~2c%-gX{`7AmURBXnOlLoP2i@F4d`rVSZIzsWSJbH z*!S%g*(pfY>_>+K5q0DB?6=n|lab2e!>}bUO#@X^z0a)QzBYIHAnQ89xf1q%Qo27! zOWx=0dzIKjRa9Is)!N>?HN0GIwM#XEHKZ4A;K zxr4c0(^-e-88}+XA^h3<#nwdPApri$au!{zl5q&B)V6zgCaRat`OkK7cHlY$ z1@R~c2%rVh3UwK)6OD)WN)Bum@%b#tpyVYLJ-)aB4lZWHvIjNIe;}9GmG&^anj2h5 zXXyG7e7}{iX*X<#u6FX!&lj293ALst;owC60O_F8a#`~w#w)LpRq@rg-}%u?v~#wk z>UBv?CyaMA;#Lamiq5@;^AYg_3K1}u;}u&>JarOVHgBGLwzR70-!mlpPo(D}lP>iP zn}c#!Gs-BYqrZr_Lqh~A2PFFH-K?{qt(^jcdk(vBKMw~PXE`Y5hk_g zm7h#q^!DM-y(8MU56i!>;>$^^e~C3s9gpO%=oqS8CGF=f;+ChZWgRD2vB(c8935v7 zJEL5razOI$Et++6%Gz0YgJ3c|8VEUKmfX*}nQ`ks%QLHKF1(d~(X-C8Her~h_Z4IH zBm3Zf$gG85p{s)a0eIp+3y(jg>_gzF(8{bbExA7Y47>mU$_=;Mn%gRK9r@vbltm1C za~N+cFS@|8h=c1odr_0kLgOuyWp5`XB+T12GFIx)p7M?-A12>zrb2U9g}(o6F(GX;ZYd@4?I7x78g3Z3ZI; zqdxHj7nJv06eOvCb?JYSV#(~hp2RLi?-9YFDUQ2sWy1o0-sh@)VN>0Bvdhp7y(4el zj**Q{4=Bp&gS@kKlDD3;e?)9ItH@Pk^N~Xafi`9@9f8B-y~p~}Q*&@o&Wa5kAAG37 zw7`Ggh9016Bv$70eGy0G|6q`>BDP;Q+uwDva+VV>=}kxwP7(?k07h0P^YC%l@c1!y z-8-MH6f5(+1+5V}J{lXbNE=ZaiYh`1u>S^-9(>4HWxg6pKV*JEe-T#t7^kMBTpm3~ z^Nue}p;!vZ|Fny2H?7vl_s4Z+cJJ6ecVCd{88D$&w|ysrCS0F}d`wSa_ski%>bNw6 zL@)>`<@h0)nCRBYg>~Ml##Wuv-X=$twM72Zndq0cHq!0Q)*2s4sO%oF?C{h~>zmf? zE%FCOywv37?dDB@O~Txo@0+PA`AC89{s+*%C`zHrt&-`x#+9r!ENHeg`8BT~tGMmS zfUmY*vNxBlaz)2NX8~6zzYy8z-7MRJmd0N`-E^=638VU%Ode4APbMv~*_@UoJzXFA zqUrOLwP%pvX$yZT1tDFtdsFJBmv{9&*Ly`+sBT*1K%a{-P;6vHe6 z#w4SNg*ofC?}G@Ub)fDKQS45g6%p@%j=XTR?BcZ;Yx}jsiqtp4;Ulz)R3&D-+`YJS zL=oT7+ww7k20Fif=2KTmxE7%juM5$hp{cFjSAS9J8-nM!&`6NA-}Kj>RYF6q8=XQ8 z=G^4Ly$ZbXzj#^HhL>K%GhZ%MDcZxv)`pcsox65g_Eq`#bf3o7yq2*y*4DHbXScY3|RNKoA8%FAX3HiD=#B?Z}|86!R3Q&aOh`LEcWaarl)#*_JJm*1(tT+E>$U=$wvwxN)9FA?jp5m*>EN_^SXphTAY?6`&w&8)=uA?;3X0p{BXwljBE83Cq~P z3ubaI+PeoSC>X<}z-QE&L)$Y{is^nyg#Q>-X0nA9^Q+%z?yWgK)(wfDhnX81Cf9 znyF{NL*GC0#{F7Ihr^${#Z8{dlE1i>*}~k3n}cRFO8ugx-KeyuLJrq#=AM!o-ER z2;4`Cr3P-rXFXk68#8|AqY0k|#M#9Tk$GESx2-TMV&eY62W6uIVG&E!3g7z=)JJD} zenS5v42ObBQW`qH&ys9=>OSw&%UAtbuVU}N+3?d-X=f2a3Yszf`u_Uj?Ap8-smeu% zYK($Sl-xH=f8|o#yF-%slLKl3FQjTO^f56Nb~zP`{(Wp~JhkL;$IN{jw)W1N8GK!N zcDW!qs<-o1ObTXnl!?*avV3{qpJrZ2v}LNdAyUX6Zd9F$kGpw>ttmOH%u}CrwMkD| zHP~6uZiBbqwv9`-8uMFkE7S8oWt`Z407oZw3BJ7=83xEGLTjly+~BvSVl{&B zBW&VsY`L(Evl9c+Ft!<l$JCNNJdx!fM- z%nZzJ2&<1MP~=gF+3M?84>Tc*iHP8p6TGj#QQ3&4M%p(i;QsOx_Xzt-Z4COes5QHpKd*6DKPKEZ9C@@k( zNM?E87^b`@&~oavtGy~Zp35CMK5pd0?^Ds?mjnc4xokVRWLJ^W^_4otxS9lzXd3vODSj*e&bqi)#<|g)`<(*>-XX^7h@2PmR z$^RZ*rreySx@$&=X$PDN=y+JKLckVD>O1C-WYh4lDnFmXQ zu$^lh59x`vC?%3xn6vWJ1tLu7G=>>l3~5Y=F&sMHs7IWi;>b0FUV`!J4#KWq8P&dD zs@#NN?IEMH2tege0&+ZTXd170cdXnqzpDv>bK)ri+~TNFS?YSo3I{OgEs|UgSNg7p zyUt3sZ`bGY&?2)DOLM~6a^+<@8wDzBzo|BdCw^)E^Xv5E*^A@wZ~!x{uPY6qkv^mj zL@Oe_#5@5%m#|`CLCv?2Uv~f`M1m?sMQhTeZN_;edVjD#Gtz-ybr-|W+QQnQk&o|P z>_qKrnN8Ryt55GKE@T_VM*Sx$s^&Lo$>5_KLk9A$6wTjo>e{tK=X^1$h%B4Xq0?Sn z4gP(F`iE;@{FCg6#CVG@giEC&ENDI92zxff9!xne^98pd!4e3X>tffu{ZMqlpPVmn zWa>wH<0EyZ3BKC$Obi~2eb$pdXnTuvT}xNL5z*tm@uxPd5Zr{Hj+~H4A zlj7;+mvQzby+o% z=9^9g*latIZG~t{xF1r-pPlPSCZf%7vD^|;!!0cIXS>-w*N*OyoS2##Iko??6V~;j z0T*O5eHn#7EGOInp+g=$MwISSG6S=j!n>9_+Q|4C>mudK@W1`*=zO7B~es|x< z(+0fGWJ{*Uk#$BE1C8P+9%s_hqRyq#hYNCn*nyMxxQ3mtw6qm(sk?OT=^EdkSv~fg z8abrLqQHz*9dtJ?ns{yT#3z@^m94DS7ykIBGvM~IYv&9Gn7>(jZs;oOuxrZ;RM*9p z_pURzvooVv*38JPz5kTX<|mu?&ueb^<2NllIy(ARU2WBk&0|-)Cp2u0uDUh3d1?0L z3&VPJ)-tjzF!X+k^qI}wGJ?&^e(*pH@hGMB#|-G--w#s*%5mYhqi=$x!rU~b2mT%U zM?tJYoaYYhg3{c7*DgjdqBsI(y3mpuF6OKhb^8ABaI{+-HW)?@b93{Apq$*??6fsi zs6|DrK3N6k%mT`JjF&bxEb6l!Wscb3^*Y&AK)e9nQ6|R0 z`YshK=gyVK8nS5d;-3Bb(SHLLj`KXIi<6Y}p3`KFQHU86jljfY<>X%59vDGjd7le?e<_$;yvQeW}dFDtu>~40V4bP`MtWKMl|DnJugZL%g^fTSkizrtWCZ1 z)yE%yoAkQ~trLB&iA-8a0z~3AH#9VG{4MH+I@yCy^CVA_Go^bpL~Z9;V?g|SG`8B> z+TtlstV@>zm)0m97QGHSju&CSh4_GnUM{*!H>fmZNIOU~?3@5?Rd0+BXBfw*=F~tJ zV>+AFc5zzMnSg?-rqyY%OyfKG<-bZu&^UPLkP_0*S2yrHcy?N-m%# z{NbA>S*z1XtW*ExI{mZn-4o_x0#oRF)sOGX3D&5jG-FGhID8ln3FOCy+x@;;k(Vl$kXI*t5_~c^Cb5A z3K-hmT_dtfTe$l0;hi~IC+60fZ@N{{#D32Jz0!>x;9B^$=HBud?{r2sE;{TB`5~yH z#IYvk6$5k`tKJFwugFVXQ)ML#sRjlEI&|o;@z0l3;qSR+0Aq>p|K@npsZ&pJ)7&dz zisT<~|7@C})br0S>8%GdNxa!jN9FTr)``1f%M10>yZ_(s9~{(7j=LSBa$Crs%-Y>~ zxCod_nL25Ke;A$Ww(T{N6Sw^x_xQJ2wFr{3AOGAlYzEDEb^4pb5*kCC_CB{=vS>fSw7IQi{!Q}`n*Yhpm3{T2xG+{3yOd4u~~a%yq5U`!bY)7wYe;k{1CM* z?6zn5{SeW6aI5q5ruk9PcTeVpxPNrl_Ndn@nVaUVf+I|d+kiuN>%U#)a*Nb>5X>-E$3(V}g=)Ev@&v^_ zci;p?btCs8JKMzcK-}{_7Xpopj5dBYHR>2+JKi(=UG=Xakuqd-Xe7}I3#Qb^=h&*V zbUn7Vwy#dHH4wSul}ADMFMMn1*G*FLw(YE0?84k#UA^pw{tPQCw_{7(+;*D}q*kqb znUDz+@Jcbdvg+(Xd}DkeE;~E*4=wfTpojRCRfl!A+k~DMEm@n4`tiwD2_KB!KcY}$ z+X#>uB!IeLPlO0UGROHA8|t{si98QIGiYr8|3v;gf3Gm#peyIgnw!3HwEe|-2w@K) z2o@=N@Wdi4&C04f*_UFYJ>TqmsEyp3tq?lGcNjf(4*L%H%zxfrx}+5x96Uuee29`# zd!aS48+`f>-;?^BJ+0Fk$3XVb8v$Ysyxm!5JdbFb}g#7r`(F!%{@=f#RB=Q8g!`MC&CSy!3vrfparQD5&BYgdprqs_V|&~BGa zj`f{>4wVa@DjYHOJ2=)=Oa9W599hW?94B?Z>g_|_ceX}tn;-w;N!O4ww{Mc!8sQ3B z#o3$!-$x)6u2~d!BHUFBlKE2Dtl?je1U3kL*ot|`xRt5M2@Y2zU%!4$vDSA1!!bq` zMHAhlS1*4g7+(#ArYSml-}&>y3BhXEF!jsc{1J!l>xYJhezq~ZJqNWO2_G;=o->M_ zl^X}wjgu1b%$*8*!E??0<^^2S}I|(SUf1>g@YGIZZS0>z(xW zGkKP6={6xI6IQttoSLiQrZ5!01&I7*xvtVn`GG~sEia4r+L0Nl_(L#;m-=)dZ)P#XC^vK4f&c!R&sf`NerS4(Sk46V zHhR*eeWK|T5nSRWVrfS^9QEaCeYtxVa2lYpXl_~eQBI?11x z@1-;$hE|J!B8VuF`6L!2ckbFX%X(X*++UU{3E$VY#_$s&vqI!eBcdTzmj2+dKN>ei z*^Sq&t8Xy|qvDfUbfkAd;?y%z6Ee&@8>($kK0dTJ^u(asCnz||s;Vq{IC&k>0EmWqov^0r35=R##RoO2&^;-Y!*XN5hKOgm6 zQaQZi3HmV->HRR(7?D{^2ZOZ+&n$QJ!@N8^ULucnPUHfE{O@%#{s`tu+><2`6qA2K%3rZ*7Xp{-eC4XMyQ0a^S?x z7!o3^+afMJ{YUZ2&r2@5WJywv-b2~2yGF5-BiUp!-DB=z;H#*tT-5UWJ8f|8!Be-y zMl8Hz26CFw+kP+Sx8MkM{!*MG(ss~&Ur$w;sMO`*wb$3eqr&J-+i2GsSG|k$!y?3tj zrVl~7F-Hw&gsKD`?PihR%6ZbQ)7Wnf`u5%PrnY(W=JIAt`i?8;>!0{QZAEv~on0@) z#l?}P`{mfQzbf2fSqWZC1XR;wm6^8{c*3p#6EMu2cZZAgjb#3iB7r0Sj><%Mv)D<|GpDFnt`8 zvySe`!43|IIP{u6rb$;}XoDOAVx4|{b4#b8L(4|q-I?)cHeN6Jk(jVLjXOSI{``(R zBQb5w7&?CG#BVi!orVo_@b|YRTeVm6nEheSx&fY364z-M1e8DXZs2FDZ*Om3=@5(G zpJ>g#Kc8<{{{FG7;^6U+jL5l~hTafQ9QtrXhP0Zv<(-BMSx@>F> zj^sv1p71+%JRzBLFOqz+1~rLgr=5XT-SZ|4ndtOUH=$b5Tv=T^LFwA(pGIb8%Idb2 z3N;H{hYwGkZr61uqKcF?vGwcI`y+dcJ?gUeqsZ{K_#;+cSa4D_YXU3q5QO(y@EGGr z=(DTb%eckGbW`zzRJJ{=ZLekhr|wKkhOZxHjBOKAW;T53U*)>+`T59!Ch&sHf$-qQ z?c49i14AS&bwRB4?+hx2@~H#9ee*`0`kp&&diRcbvlS`8U_vmY z|9>=Jd%(yJnzla=_;&I2%6rc{wQz-#Zj@5()a0FK&S!?Xl=_=L4BxzY=ClPRjgmG* zd!GWW8~OCZhlR2xhDDXx_HKxU-EwXd5IBelM7AtUnsn!~&7zqza#X+Hvj4B;Q@y+e ziOxehMlV%%v5(Epc9Oicyu`t>oW2>AbYpYG^weR}KCf$|=9G{OQL@mxhlD5{pO8`? zvgeO=Gu@Hom{O6d$qbbSo|Ava#EmbT=uM;}qw_*4NZv^D`?+-)Gmgz75MiwGs}Jw+ zLxQ(mxS$PDC2DLYWLYH!XI(^L}(cmTr7O$;&=2I4C#lHBf>TSXzf&K`t`N9_Y;z&pRtLS2y>FeLU zI*p3xe+Pm&b@Qh4>B13dYp*B00ip|XAp29C#pfiAn3EuBnG2Jc5_#yl-x5s`GLO~U z$(~8`g#FE}+LrXgfY(I69byk6ahgKC@%shqAo>nyC;Qf|tJve1R3rfr5j&-}ElSW2 zlRlzpMB+yP>(+3$!wXwP%~Q(R91=2JztLgnIk*^`kYdB3fsOE=zXv6=w|O}!$m!Y$m5&(>w$H> zG8>Kj3E?7~&B~X)jf?)2Tfrd|l&r6Q_|0G{Td)2m^Ln>e1KzDD@$)KL91B|2UJp^Lq0wrZ-amkza?Ag3c zn=nH7;|90K`9sK*lZ#8%=9Zl*NlaHG;#sK42fD3>*=YS9f5-il@7?WoXLsM9AFMF! z_F_gQ8SM-26ndUpwhEhNAdf?%@7(|KTTUM#LPHvqFkLG{GqX)5yK(bUl|819rMQ-O zJwG34x;cgznCj~4{y|12g(34Tbi@V1R;`=#fXgJl+QlZJ^RD*sKK0p(;YjP&XWocV zl_eXVJ$nWUEhFTQEuIVzkLJJb?RY!p1w1nxYU2L=G(q)1Bt)dkv?cBfz@9B_etamD z+XV(<@90<+L(??Lc<$EHuyH0jD&~}!h@OqsDlYiV$`(^i2KOx+q3c=?GBPWjP?pj zE+Rf&4IYjy!N7ckYXS3lHR@TI2i2-})U=tIv9Tf&N$YaR3{*j6r=b~k@bj|)2FKX+ zg{i}9NPim}6$b2AYcH0NwkFB?yqH*cqtrEizX__ZeKbVrZQ^N%84r~c>bw=2sfK!1 zK$e|-2wdoX!~>nZsud33PGl3`NYKpx~8 z3+=KN%}{K{ls3ul_29kn1*6DuAq^c@_Z(}$pb`=iQarU<3KUsY+j=ZrwydxR zi)amc9D+PnzrwhGsp@VYS-$eKcze^9eyL53nymU=G#T!Vri7S(hIw~|BO6w^kU^xSA_YI?rCq0V#%heoo! zvfQz1KAJSE?JPUk0ePzNukl@c=}V(6JwlwdH8cYDJBFX?dkha-T%`K~fNi?Ks^5%o9EdaZJUW(|v&G}Q%+!Ngw1!yQ$T)oWZcZxr# z&5q7)nK%6I*;Y02^Yg33qGZ^mOH+VU$zp&p!@XYMXl-Dy-b(BGm3$8?5Ov+SW*+rR z-Y!SGZD?-3bm|GM9v6X?B((6<;L?*1vKahy=+-kyh`KvwocWygZ$|AJaeqyVBzxXx z*_TMq?J#0QGsZ6M)`aDhPU`=v5ck~*`Y#-0?`%`QMMN}n_wZOYx-LVz+x+gWi+|Rj z>&odlU{sO(dGYec>bjM(zj7AzhqAIVphWqF`6zPq{oWdIDu6~xMg&tLO*$ommh5$p zrfyeEjg3XLx@yiQ`v4GX(S;!XC(cJ+nij@$J8RIg@|P2Z5uE!`bZ5+OsDA(my1rNH zZV+Dy?s-npdt2V~9XDDUH)~jAcbe)Lh!|FMqPNO4yQm`V`bumq-`u{6FBGXtH(nM0wO*S9WWf+ z>r`rLbAA1-FvE;G2)=uEOOt0i4YdW!7Nt^Omw~kdAaUyDJ@=ZuA4p=ECz#_vG+`uFcjbz^G z-qygvVtcRa-&SgLiGG_9*ap{kxMd|Q{Zvw$e4jdu3Irtzxn4Tl_Zo~F$AgaHmZ_NbKMuj9{jyd8rm zOi=N8`T6X8xLP}mdoAllP+U+?*L>UGR#rt*8E3t@O|{}$c+CvJzl)AGZI_&IP(*Ov z(^d9YCKy;op<;aUes;84n~C41=e0tjBnh^BOsxb*M@L}V9`h0E$n~Syps`3zl|_N! zdj;9a&kx}0o8~-u`0!-^Ubo)y-UJ}bd%m{rFUdT|=Ox9*DRg{#lD7VD_^sI+>L05} z&^#JTJdi{waFB;sc)s&fI`fc-CD=R&IjXOT!s&;3rWSD&dG6+qY^F3 zgp4(PX_NsXDPM&O|58aOPu8QNrVtaSwQPZf&c>=Czf#048Rme{BY_1s^!*e>$)850 z1RQz&)nC38cV6B=k}n2(ds`g(J@QmNx3wKLRTwmJZ=%aE#7LI`*(rI^6UjKtxlu7@ zSbJmRljqmj#?4h6WZdNG756L$tA=~tc{^`Q3#91fyP{)e5wNzlHn`Rep+Y6FS{gSn z0bVLG*`BQ3;oQgE=uqvrgc04l#aPbe3n1($EiFBt>z19|{=e=Ch2F>VTF;{*Gxx|g zG)N+TN853-?}F@ri_71m;E=#)36^Q5gn}dR`)&8zj`8J+1^-RUh_Be{{-{&vRfCOM zDfNTq_DVT}DoijWB45S3B+D?s!oc6dc(za_b)J^4hm_;_F+Ab`)49vKR}f*)NzV-G`b^Mk$2dj*-+ya$e_najrw{8g;)m6}ZK$SulL)7*wm895(pJ9e4t`p}@~FBOtVu7&p`y6VA+US>#QRwoq?J zT-=@AGt3nzp*P?SH$HqgMv#X-ec~E_rZHKM5*eU^$)!)*sNCbsa{S0zg61l7hmLF~ z`R~pLce~lEMQC<#FwrzfKi~mI_2Ir3t#26v$n$f`4-~}4GI??n#HC}8ja!nn1uVlU zy4%`ccdAR&*Y@t#Jm9E8>Ac}CF5UBozV6$%C;QTnMs%6R>BL=|@Ohzyj$1Vj(wnB< z2aX{uLy-q>avj-N@0i>Yui`t?YRze@`4^Oq>5I6PuC>qclPSRx?_(M9&6>{{;aJm+ zO-u%|CQ{B`50h?}VnQs39PRELrP=XlNNPHTUVQhm)>JT_14&8jmWXm~&TCn)eL!|f z(zDonlP+4SeSGt8gl^uv%h5eyi_2U`dzBM$OA{6>Sm5hk89vp(z(73sy=Sa@UM&zh zXkXH@Ky&C{qt2b1fZj_iBH$3CXv(5zmXe7t+p+BJ6>H1-*ES$o;Z_N~B^5<&bDHvh=m{n&M3jT>Bma>naY_Z>`f-Ib(pz#l(0C*=QY*lkaq4kmd0}PcXR2m}#*6 z3e=8O0L})vpX>ngQHW0e)-LKTMhD@MMS;^O#_-MHv=?_%r*0mVGNCi$ekMlIP#)eduqzS_aK6b#LmVsCmS@2TCh&LUy4MP8pX6T47D8?nf5*$QR`;0}(&;*#LD4ji8M0xHbGdU%U5 z`jxA=CXDbJ+d*;Byq=>i)PJ3B(GA${-F}5MnvEZBWMw>=x@uAUQ2jkR+&576^Cuc) z)jdR0clAj|?Q&rWMfmcw2q7|(P4K^2MU#1?yziYq-UnJ}T?$R0;oL^!D zSS}&_ck{sa?AhGFbbZ5ao=!u;%hru9F{>Moy^F)Tn*918&KP=}p5vd+Nu7a>Mf{C` z+?#k#X_F2dH~Zr_`=IL>tgyqsBf5rYt5-1=Ewv4jv0O#SPhq?3b^5%weQd*mO|weXI?X8t~_1A~M2CnT)TXi?HrI;c4tx0jz^Qs!Xk%C@nK)-AR?S2-N`+fmWq(cWHi z!LI!N{{9PTr12L{y&99@`NC$-d&W!Xsa?J&MRbYqO`B4^8(C0+P#Du^X!UiJe04!}~W@ZLyg0M>Vob8-K10q}17;XwW`GQMzzPXCK z#3qE4g@xu0Q|R>Wtx5K{!g_GYW*KJM5#<^1~8^_f=%rvw!TknX1tIPo(?2v1^a zfS(o{udg{HTRB_dmk-HigJW};-N>=I5VMnbx zzBuwUVNe>m!@26*2r1U27nZ>aEL0g@*6ykJiAY2MgOU@O-SSKvh>So^3f8UfT^|U6 zjK!Nq;kE%>WTv8?+H*vh1O(6H;$5X1-;5ZRNF(D`s&o z=mPN55SS)b1nz#W?q<8ohld+^1uvWmOcgsJ18XB)rXJ2!03+L3CZp4ssSzoGnnkr} z64JN^1@XR&jJ?O!@vfsbC`IWq(B0jbIC|k3DQB{Snli_Sf&h%#Zn?GDmqVw*Vn*}5 zQc_ZOYdINA@}AW^c7wBrhE+r-wjFh-F++%D*s-GuK;Wgkih8cj&H?P)8g<0|*hIf6 zSU3+@^ztbE57}%lh?3@}we3sI~o}{ldn$Kv;vJlbu6%$Hc6o zdtv4f{B>*OFVWdaXs5t{^3Y52s|emNNY;UWcMwc1L6}SKUmeo8)23F3zb4du zyC`O+4S)qRYXe5HmQJ)Wn8RUOWwGVSH@#lGqywjVUK)~}HuRs+_lt+i^of)d&g&Zo zf?yZT%>wQJ5F^*%R>gFK*DVb$AiS*8&1)H?pTlQgxbMV>4j|!51Y{$kWj_D^1z+&t zsBZp$`y|bbb}x**Z$8fJ#s*6&H`B2`Gf8jYaU?TLUeq@#B#ue%~ zm~Km89D+5yG~>vJ11+WyDUt8ID~H@YaogaXepT&aI0X@!pkEB^ypM$I%;_(Az_7t5 zA9V~H?=&6L2Ws1`yk?9QMDQs#Y{;|+pW23JP)35~%fH)t44ywaJJp&`2KWK`q9rnkL#m33z47n;QBC2rT4ICa8=?A;4cbAzR8r3MEE zV&FFF+EtA^#{uz5a|10$ttVN8F=;O5wti=k-uv8e9bkA+UuAVVu(VdUsi6Q4LZbE9 z7-f1ZW!rNLeov*uj_a?AROtV%PH^KLJ8pCt1+@lyC6-!FCft?a#U)G{%pqK`@_sM4 zQRBb4T6H(c$AS#w891cg+Iu5M!ksZ5eEIfZcvJSK(jvU=5V`Rhfh8^bipe62+5bnf@m<)n-3?E3JF6@ zmlfNhqJ$%xIP>d~Pw1M)9lMTLN)~+)YsAcz)xvr0`AD^x0|8n06PHeXJFAVpz8V5M z_r2J0XXNhc1+P`|r#+WJE)0>2PkatmP<~d?dFrLsvWCXDL(191&KN2se3mU7 zSIVOZYMlHXD)NPYg9p6>zRs>t{Ix3OxM%`NojP#9m@x$n00Ij7;j&*Z#DJ+k0oLhF zp-w^)IP=1r6UncHaTzu`gj)yOfR6E|(jlC4avMFh9!{evkp&V!>o|;V5E&IlwZwD< zKYESE0PN$nmn7Q8x}b+a;U75c(4eJD&A~2CojTR5RV$3-VLa(&(pk_rbAdSr-h2-X zoJa@w^;!$SQ5t#T`j)S+^Z`$N!W)Ck( zrWln1LV9#NdjI|inz*}FtCKV$e@}n+pK_pT)20aT*5}SC+v{pMz`G`I!)KypMjjk> zq;)RS!|dlwEcLc%bzw<-z_pU%&ohu${UfP{@6$a#>em_r1*pf3|d9dA(GK zVFKj+uQZ!$4d=&o5?sm9uyofQIG~LU6g%kOKrRK4i4cMTCUp3Lg9qhcq1$3AonGd* zOSL^TC%^I;CL?#~+=PQJ^u7=N)K1-E?#E6zL8*mGujp@Xu7MPV7SWy+Zt;Ha%w&tV zbU1M4eY^G|{9mdjl!T}yM2F7PX>{sarQ_hiC9>BW$U#JGu*nb_UUqwdgpuP@3xJs5 zGbBTh^HAjTEj#2HczAd~Jai}hghqnalXo7Rvlx5vHCA{DxY|_L5^tg?E9lC|8M)cd z``D<@!+P3v+;SwV#oZx;?Cc7@KEf||Cfu@H*pa{k>vRi$Mx`~5EuE&`z->b06dY41 z**+})Z%oG?Ha53|?dc6y&0RZLyJK|plRN%jYyQgP4ZsCk%V!ahCMoX~<*&bedUh^+ zvEOf(RxdAQeK2~s`}SMiDZ}i0nM9BG^z3?WY@0T1l+@JboNZg-(@JA9(pV2g&aHMP zCX$gKvUzjBvybuZwGwN0c|SI;ZP&HG*)9v6++3@Z5nG1bRb4wgei?6SI=jb>(AxE{ zDZJ;!k{O4Yx1I*wzl+SlHO?~SqA~Iv5zWs(Bk~qX4=Z5qW&MPxL#SA?U|Nli1YV*K zAfN&>!4e(#GO^yIp5?Y6(u>%!L)PRznYOl#dcW%8OY16tQAFm+lRyU9?pC&#-6>^s z4T2{#NYa5-|9+COb^Nr|(Z8N3){VY5#N-<%Q_merK1Nq=BhWvqru{r}>v(`vd=$(4 zWQA_e2VItKimzTB;CqP1ml`g*hOE`Yq(UVRn>7Oi6VW+m@i_wKRELx3wXc;sX4 z+9ka3u18>xC7k>NU0j+_+Lmy;s6R**zgEv;C}wOU%xp06t#$oWmVjaEv1C~N+q$HK z2TM@-2-}B>m3rCzUKm`I-q0PL*V`q?uL-4&hKM*F+RYD0$|NMWU~joEuUBT`;4S4B z{Vs%XSd-SjOo?Xd2 z-2|_cbVcRmrxH6E8qR##2&GWzxKPWm%D*aR2~QeSdY*n;W89$kQ}6A%E~x~F+R+p8 zG<*Okq?*c`0f#5J2hB9n{(XezW&HiP_|1ze2S(`Pt#4zYfG08X=5C>1-{_3C+)m9V zPqFY`ti!O(M?~wUKQTTJTD_rgA$(t>;N9C^EMl@l_3OK0*m_?)yg4N#q-FT3cQzh3 zy83(3ojupyf1k-pt?F{C3}tc8F;P3B7U{=3G7}lygwt~+ab-=-vmz0R{`X(lkoUC3 zyAy_<^8(QBd5q>xR-%OZRhI<2ee*o`tZ=+_j~2PGU0-tJ3Jn5! z=X}%7?;N8xQO7JYxMg(FtMeWHtOCFU_T18o;LdtDTc3{qdd^T;FS1uiiw|(vu)@J= z*1i!BoG3g&Fc5{OwzrHhLIw0hnk>Y<$VBBVuZKKSPvpan83Xaw=7?OP7yG&X!TEtU7_{T zm&_VD4a6kXpf5RKl1hpIOg?G<{%zstn8qF%L#7dPC5sij=!o=J-rwZhT%Y32e}`Sk z!Y9<=e@mVN0|QCz^SY&j4x7PhhX%=E#pG$sc>fq;OfY0dU*L)~?Ob$vHG?oV&VON?|<$S6d|HxVLz@*%c~Cj&b_{fHca^s}*AbyF4;kd{+I$2*u) z{!@C5CpMp%Y=FL=BYIQjA7b0?sBISUHxN)IPGSX`5j%!l$coCxlNP7ASzl+P$&Ro0 z#&wMDJgz^^i_)?(;reQ8Ykk$$!g&KZ;w-yHeL8<>cWmrH^q-DlBf53e+9I-r$&*6` zI~BZ@OQc@KR%h9tMGnBFy4o6-h9<@3ezQCF?QLuTYe~dTptNa-`Q8Yju&`~Uy z`Y_4stn@m-&i}r4uFC+~QYeJ&k~){zF8JHHsmS%9XD9B5Gs-;l`3 zl@1>UT>KT-wT<1HUyyqO9in74fH=c;CMm$Oly0}J@7Ig{SNi$sP~2U-34xQrAEn=r zhG+D%niwU9O2MqemIFHZb$|MwyLm>-sEseLGq@F`B)pJ|zwDIF%x7`e(a` z-Up%prrd|;R+Lsm_n^2?!yh8nTs`X{H|8+?u%K<0+ENa}4J!Rfs$fwbl)YTHlJTOD z=7^MK12&Tk$4XW0*l}~yGvRIeY^1tFQdpW+`CX==U&m;{eoK@jysj9z3g3PtlU12^ zrFw=eJ5xsoQB?cm+!Kudks%`@?Tw6eg7dCAreF<~Ng(-nPJbp2|>vbfqu_SKuup=;_G(uYjU7l-?Q2`Q1pvyxK zlnE=hx_@f+os(=A1#zl!bu{~mlF`Z8d1h2LbdS#uBElMhXz&9_1>O&zhP@as3CS1^ zRBVst49rXPhk+8}oak*m-5|>4Ts@P=sN-?B8gS~uMvskqywlgmUK&+iPzAse}wp>5natv&&q^ajjv$hB8p1A#-!Bx#A{ruyc| z@GL>m3|h>4OOl#vn}~|e(sD(-*B;BP%vxJU4gf695;_lQwP2OpCdggt+JzewJJ`cA z&ev$vQ;B6MPzrEfnlQAH_2v6-9Qw_A)uHg;=3R+I#Gc6+`XhOmtLt@U`Q|!0BCzHv z&*CuF-#q+E>suNm=ivbLs{8wMe^S!MmP5U>BP;qY`C}Hk=fgIDMwxYz`+Q4I@CRhW zQ?sjmtC~sUnCYSQ^;do9L8mHECu5j+Ye8Ai%7|}9`~xDtqXmoiE!Rfx6*IQO_R-HD zJZQka8bWUvQSA~&AcDf#F<^gQ@vB?gCZx`3x+N8vLCnHh(2uXxve22P5*~fBA*|pG z`P3$n1ydRACY*9t)=h74g$AdxTiw@g5{dCmfu4lBEy*+Uys8g{&jNd+YwZ`L!ZZcOf$iZd4$}YV;2QfN=)BdyDpxhp2>cSszzOz9u;Hr6sLCHJ@?)3`ea$E&0FX zm(;>PDLyR^%+_zy=JgfZqZX_C6;1oRelH!?iayK~rA4+?(RW^ph6J0OsBZ^gup@DU z;K2``PT$Ug1TMWVW2fvFFO%9vsYf*`?OUV0A|9dbZ;G5+^waET*xN&6HMsx+^w}?_ z&l%~KpV_Ek!vqwWBy0@I;fXy&>?hX`!FIf zZ=%z7zu%4eoBRC8lNL{J5op~RFpSq2wdwHwe=S6cYu@2B-x_H`DRMuwdDzoj=D;3% z)K~bPcKXZPqD{|<#S)uspHJr-SX*1m#@5Dd{nsIw;;k;e{PShmwrSZ3rk9t~lu-Mn z%sRJMkcM|$^3=Y`neE5%h8Uk7x{fm z<~y(6UOfJ?6hROQ^r9DywOd6^bn($1>E_&_pIdQM>86T3V`zt?E-+3)OF3d&6}yE2d3=rl!&B6C{6_9oXW2DS_E=bbpaxM*wH&2S^& zS&$Mg&cf1K*u;Mx4e;p@st_eQwb=l2!bf%4dFRWK;j2|hhM%39#$6C~!}-0xhbE|I z*d-K4oRVwIurAo>gw8(2>l@2yocNg^atVJPb>`K(83sRh6gXOF8GVcWod;*W)x+dw zG(rhE#L&>p@t$;eg~rxFPN)0fIY4|}`4oQ;xKP`}$_LY%UvybBdQ4(|&%twQjWdxg zUy%qW0Gv6Ei48U60Nc<&>G&l0aw(A5YNi?O+V^=P!Q-CiW}%q7kK5RQx0 zX$ayf#J$KnSZ5RR`n%r+@87*Uq(&&TrSE0i(?3XqY;EfI7u=mXIDnEfYHjEk#jlPy zGV0cs%w9G&n^D0uB`2h066w`|u?dNZ!YD&F^P(}N%-ph@`%H&QWYx)3gP>WhG&N5) z@7-f+$VmIiPgqRJVF9;SdOu;%zZ@QB2ccC7=a1j1HgM`?xc;2ZMsbC>l=1jnTWHC@ zsSP_vj{~owV=s=V1i97gvm&&U7KX%Y!RqX_<{X`IXGfRbq!vi0w$=%j5;yelMgz^*Y+H+)Ik#{B&TihrSW)6e&9|v3>D23D zana~Sx9C{xRW&O%Z%qjPC5*zgfTPXo=g?DZ)w9n0T@du6f7_jkhM~O@z5%3`(J!PAX;bSVaS#CtU4!hK}azR!@AKTl%3yM0(PZOt!1RiXTso&5Tyf1U5~sh4$l zajsy5CSqEQTGE6ntZPf{oIvuF?fcOa5kcQrnNc$(J}v3Mfy#Lmthok1LTtc3^OlUL zf7bh57U&4IW%ny%2;Bn6?`qIjU;U7#D7Se)sRu*%1Ag;o_}dB0;lVBDF4~%E>*|&{ zh2bZ}b21P)F7z=1V)Pg{mV34ze5wZk09}RhOuTnhg67gSuRq46YFPyb{3ci@9xzhY`vPN70xeK9p;c-oYPS@s7K z{qHMUW*|i5*PfTwSE$ku4(6s$UGwX8T)>3wF$0z!A#G25LgD>>cYtteQ6KhOEQ?&Y zKi>}Zzm6~|xu%L5_i}CBYT3s4rS$sqRD@PvgX;?Kt=D{fuH@BfcU$v^{l3r&d>EHS$EF*+xFn#`{iu-eip6{XLcF0m1 zQ_kcES~RW~?6%-idy>OkX|POjU-N8xb@SZNig7LOZtH&}R&i{nZxgdK-;zRYhop>- z@WSWk7{xp~skZ5NWK_bKMh1zUjl1T;(bCTCF^& z$*QH*zb)V+$sF@Md<3o8bsRS7U&0p41_8I8& zsuXAxO>Jb!p_m-<^D}h{=@bcx;hlk}OE`C~mE^^(sfsI_4m@BR-)_s;i(#AEN#3J) zz&qz7;Og98Q@K}9AP}Q*^WE*%@0_2 z%~PPg0D13kMH{>H!tRV@9OI9#=YI~vF%iGrKQgH2=%v#V!t7&5wDpMWl^pT(*n^7R zpUps0>6FNlAX&3CD+G2N&ZjcS`1t`lw>M$#v6&;aUep>!zD+7o4%`;uYkS6Q?bxu> zum&(`QZAl1&I{g)x{l{sM6R|<=O%Qneta%HNPMBH)2i!B&JkzIphFL?mU!(ubxMOs zsBvZfb@cLDhRX*K`5*|-x<+dnVBFpI^ zB(jFCR;)APJ*MpdqY;Qoevx|%O{*Q5bm3#J{ziCwnwJ4xcc$uj8tk8sUJ{S)s3YF- zX@;?F=AW4eLpX?JzDbBlY`uoHzTsdW3TB!v3%Jn$-!* z^E0C(A`}!_v$dLc9We&3kQ3pPJ1_o`gvN~GWxqw zb9cnHZP#YUljzDgDR8%5|229Z_u^4lT?0@OLn-AoTtpQDU%3^c3B$P@C%w|OS z02-3Vw}Ewj9{$^I1%iIc+Hw9o0_$stLJEV=GaX8!M9aa>1XJ}h#;0jnn(69p0reF* z8^_{@hxGOv=>{Y-$_`43HaD1&KtfFxX8r^Pms?Rx1A-mTSIVC|-!q4-mLmPl>?D!m+?}RcO2RzwRNMr`G zq{^d@>dg&SN7sbZxqOc6zCky3YP0#nN99wHxow$DOvg7HNukg;w-PyWTE$haOL{>eLVu&DLTqHH) z*>Mib9@KZOa?VoPL$bnVpe0pX9ajYU5W*pYtk=@&P z0P1tw9{ct>j_j@&GO|PO==}%AXgs~F;`=i5iQ$Kjb&n4c-u?3BR^$#0I@1wPvBzNb zL08ij)ZBz|$M|tG&`KXqCR%Y`wELG%kxnO$9Xr(2mhK!tUPjTA>syakEeaknXUKn@ z<{hQz;l`-_+z34O25>T=CQSGdd-jBpmqN={#{Mi6Yoxt36utfT#7FamDf$-+#A~~D zEskazS>%Hgm-APWF@}%qn2ZK_NqLSoH}1=av**tJ)X7w+7z*?yB6aup@|diZ`mI|J zKr}5;U*+Wxa#(m~v{bz?7E4R1NHeI)l2X3ERsGwT6N@Y~liyYSqUv}d`(wkmXo6`* zq(m_3r}%xJFPslzqdz7uu$`kad^P>asA@rE3w?0Rom`_i? z7^H+t>e>=rGf&<4?S$&5fn|Mm?VR-AxcG@VEqY%1azj@<&7=y_&x#XV*aOMukrmHt z>((%^R}Il74&?LNx|)CVH|znkE&dcH$zp1O1iV-*0a>R1*0b?e66Zc$T`izLORn{y zSox<)K_OvQyCKPM!w8A^nv^uj26-o{BaxKDWyUpVHg-688DGCF#o zQ9F$X#~#gRe(Yfolzxu%5Z@iA#ynMSdw|3KL`?R}Zw|MjzjbPoQ@gVi0}Dp&btx)rZAvlO2rPIuFvBkiA2u=-HJ8*z&`t4l=tJ=DLF*C9#uL{IkY*@6)=v{>VyIRFPYR8AEL zTGhYG^GuC*PPPfLaXNbrab$-sU1l$JrJ5LBVMpuYS5P&zv3!5x?f)!un%aK%A(fy8 znYqK0U3>2^oM`0huNv!rdVR&uQSMFJy!9U4?R18Ts$s|Liw|riRCD`aiwG)dkr4-88ulQbl%tiVj{yCG$!8k6zLp1Q1zjfxi1kUtlTamcS9;sJhoboTuT_7bOdkg@Sf$YZg*8bflr(V#Aqwd6m!0 zzML#+HF(%v3PovyzLx}{XIK~1UQCnL*0HWv@afRP%A|PV8hnGxnsBTtc)_*}atd8! z9$Q_W*g6Vmg=!(P3to6JBVjRaed|!~a48qpucPTMf!3hZ20H97NzY;3<)urztxuxg zyuP(fpMp-4Xzz%=6c`*bl0a+K_8snos11O+Aype9mN}(5ko}IGU(+&`cdqg82V4Hw zd7J)D&+@@rv;>=Qxn~iVTeZ&SsjdA(d0-Jp%aY`4`(8EPd#^DhSybOdM-`(HbB&^k z*WI6G*k7q$H=P-IgNEOY&P*9rL%uv9ZtRPe%D1g6Y zi4RmTZwk`+UWhUl`m8jv_vL_v(i&7_tADSTqS>kRgAKN+9i|9?MyV<8-Lr>m|EWF! zd7BAzEf>KrAW`MR(ZfrGF_CyTGX810M2NZfSB{4SQt(aHJd)Di!WiD*)Hr`3JUsj+ z4>iq+8h-y`ywd-TdwG~)Ow2>HjGTU3zA8^ouyQ?EN^26nhhLEW60)X)S^2dm4f7RF z?!w_a7rJjcKX7^;sZtCZpU#)5$redGwoR%b_@~2JEKjSbsDN)G#UzS>Qfl8DI#}dmvHPM59b2b;{SBXb&5SR368jZ4uQlF(8^BlFAY%>4MH zdxN&;GVKmapo}2T&34p1pPc;p;<;p&pU|11no~fZAj_nx@2*V0mKSbp{!g4Oxw#Db z5^+pWfkH7!?Xw&7_#J>&h-eo?z7UKILLn@S_6m1sRPgFi=V<4I2Nja91d$u-V%aSv zKJ)}4(g{ZR9X1!$)&6`k;sL@gk{j32G?jNu29y*}uT)4nZHKiiC_NU!*) zj?GW?Gw79%DqkYbX>3+}JfsnO!|0pG;h&w*SHn`kK9T0UiD{$!V$|l_A6Li*oonYZ z{w@;aZ!q>~6UQC1ezgd@5a2Au zMJ{R0oChB}H8DOE4Dh9KP<7>gu;cUNnlGG# zwcE^896{ltxU3#yHw}7n*`!tzkFft4Xs395#_yIw#?YC_Bu|*6Soc8PK{?S7Lf|a{ zY5enPWyUr1B{x%ZsUly>_Z7IZl4-yY4hSI8o$cJqhN!1G?7u3xXvAwNuBMWp@5o+2 zIY)uycGmI6(ua}b(`n8{aB4b z8$VRiM&9WfyZur#LYXAE7i&l4Bdm&?h+54?OmjLAf~kX(Wu})`M6aVPs-ma^bN%S#;a&r*Fm`n}`cHMSt3j#D)*W`|RQ(obZCCjA2>{^zQASaR{&P` zzeNSuO@37-#O?6(DyccxVKg-EkD&(l`hf+Cu9P`Bxs%6J?4G<-)7qgqz$aIo7rrKt zDk$hT-G#{6fN#Xu;7@ZCO!oA)d#tsly8zfjh$2%HQcQ^e1CNpej82nVf_wp$Q&r~0 zw|BaA?Mj%29q~vY4h)FW%2P<82m?Iw5lwN^K)4$rf<%YUjC|$k3x**Y6a!1k9g#QD zy3?=?B=V6q1ALl>XCRjjb~KEW4owVq*=w>=%3{=(q)fOiw;$FVm3Yl$P$6m%V50ew z^_^!|tj;$>+Mz<{Oq0YDo8$<{{udqAAi7zkvtlvl?Iw#`w7i_~Rer-~cF6m0s6nxN z7&~JT1uBrM%#S2kJrNv&E^bchuZyeq-#0Vw)oXeDpKXd2(CJVD!K01@jyon9p**vq z$+>ahSKpN5Z+Do?X&c!xu)h`JUvW@f%D=boiGANQCtoG)ZL6=}FWh!!_tp_{ajM8s zcxuFMv9f42+0Hfvak5T&W=i*G$Ix8An8lqX%k}P`h0Y(F9lb=cN;XTV=*2cGaw2wL zuk*Vcf99_0O(#3MJ9h0t>(a%nUI=u?!0`6sncvc)(53?!lveGnvjy(l2o_2-ngIzoL!%%eRrjZ5U)UhVrxoHAnjR#%thiJa5& z)~+xI2N6}ug9=$BH@cKt@0s?|Qf*v(TwGb+W1cct`c7O?xN}Z2UlGhfzmlekY@kZFtlyJaU2iN6g)L`M%|KKo8nu{Po1LzOFpZ z@h=h#;)e*v6}%f^r-ydrvjzz_zcn0v(5~z}CHU;qST={njS_))0miv3ijDQ6spQJr zt1f8>e#y}RF;e{9Pi7oBo&h+twgmkuL)SHko62j~Oh@N-&r%-beoBFTdj?&P%pu|_ zYLe!z5fL-mF3RWRV3y$7%vrVfVXGF^rcIVjW55~jC#w4zU2W}Ke`@(AVu6LJV>S~f zfz3f+M!YYF%$l_`Ic^wuN9=4!eqW%>`1nMblN@_&j;6;ss(()$vZ>IFzrvXFK zpC@bowi0F)OlO5I2e!KaMuA)q<#oC!=s zNCDHkB)2-nTGI`*_v5}btMMSkadO9zbue(OAh;f%K{c3sV)W-Bsk27)Xx{fU1rDRC z_zOo!us^W59b)SCmvDg?P=X<2tQtlK(4{cTpgY|0>dbaRC;ZZrEWZO?&&*?y5l7}e0EH&>7 zv2JC=jRPiL#U9Z~AdXzCBKMl;jB@|(v>oi4;&l|kITO?KWGY^YQqlY~cL%-Fs%2$n zq;KDj^JXHUrAlI~EmUWn>?|I)fy#F0%(^`;`@%M?y|Fz2^QgTbzC6?JBUb^C(~dJx z$M!AzSyN~-GWkgvAB6H`PdChq0T27;37oN!wY9{iLtcQd_?IdF%6oNh@bp}2J1?Zf zCUZg`G#Y#;;+9UX-L1Vt=Nb(?XgY65MZJA_n@G9@5Tt+>xSmQ&i(B6!QCTJOBlSd6 zODmDF2!YUKyZlS1Xzv-S^zJyj9K#T)ws!xZ^+y%o<<6^h-r6$jwqdF@?dXL1k+VZ) z5Ju;F{OyV~w~vD!TQexajhKXi4~xcxc($}4YI-7^8A>ITS1>z`r9Y-*i@LW5-7)Lk|<7EezfhM)z12nQD;!RZ^#^)^>x_3*&wP90>sKgMdfBq_VJDx@| z$chgI>(N2VY$>pmBwGWC*w+22lAIAD@qVSW(BZ7P_Z%tk^`6IhZcQ6=B#;t4)o4=M z%U#6mBl2Tj9{M|^r%Rauf>yYVt&1)#eXZhXZKJ2Q)ss9|e?Qh9bJ&P%MG}sXX>*Rg zTvM}%foBFM5crw6Zc%6KJdT!vYbPfkd~&B4#>EM)y#NhCc<-KX`O@}ukDv{qoI#xA^RsZaanTU@6sTnPMJ;Lx@o4@!hc{ctEb?&c~s4S z7ESCHR4DL4%cJm6>}rtp)Oumeyj`e1?U;7|+ zVYSZMbi2PNys}LE*NpAh^35U&e#**kVf|W_gb9L8)p>OHlCBRJyu{FYVL&iMpH_#| zEeQ4S);rr-8S`FcN8M8$0+{F^9Oog#l9L%oqpW+)@86#D9adac&zx&d{D7{M1Wij8EFdTzVvy8t^J-o%F zGVu8FxlKTCN&0Qf(!Brebmd&ByrK2-IH&cpO&yw&?-$_lgJGWe?p$de!r;!AINiz*1QRk*8e*G41&?vJs|d81-@@U+h-2*)n^f#eT;Y0TOzDwB z@sh;O>y4MU_ui);S0I>KwyNj4q(jiPTv$BkSL)usorPNUy0DAgFP}br%CO#( zaPVMIUdg+6vo$xBC@5%~53wIOWCbcQrDoS=Po*X@YjwaeyS?nxrD>#1K1d%NXe zK?JDF$(Cvcr#QB0ByO_n{rc6>%$2sMbO_VUowI@bD8$Y!>Zp!DE+h%5(9IrMN5R90 zK+XGKq7^|6yUwkz=;dsz@k1f$r=7ZG-_Qde4BEaA>NnHwllco5#iJ1};S&`S8Z@$= zaz6}N?2U5@1{cTVkFJ>ok=ti7_g^M=R7>Z1Tj`>ypZ^$T`U~hhk}jt!c;M8$vo^@C zN4Ik^mS-Yz1}a372xs2x`}e>_opbb(HiW6IyuE;Z(oO90-Uj=5;&etRZqf&|f!2eD zmEp(;V43oDypvU|3;VVHe2DNVcliB^!YG!QIjKE9(R=us4>XS8fChHy`m4fie-F)H zdxC|@zjrq$rx=D@g3XECto;2xIkmI{yD9(@5h232|6Y9k7*%p68h3QOJYXXqpFDXo z!?(e`++1>xSz3n69$$ohF-EcStx<(j7)}J1z$pR^F&*|2fa^n5Tzvf9O&csU9x|E#$ z#Rl)*R1_v`Z#ds^Zs_TgpAXa)Led|=Yi4)sQKq}OET;k30V=&g{uK1SbfTQg_--f= zr((#8(1a@M?6+^g5|!Vkeo5Wi75i=J+JX=$`c+0eaZ-_}pSSj1QAOXp|Au`Void>b zK-!Tpzw;XPgKx$cIc>?*>x^7E_k+?(rdm?swh!eq-^Yn_cp7 z7n7{x`(e_5Gb+M@bqIE8XY>#O>slZU8hjOgD5=A|Yxuh^! z4=QvwPnmWBviDKka`A>pp2XOp2mR}LOv38Y1>7aGn?qsi>ohkzC^_ub~*6qDz6b?(|)adO@@*S#0K&N_9lSe^3f zUH^Xl`gK!V!OKaw!qBdM`?441&qd2g_L zra`Nj31;;+Sso5BtlA{~&e@2bO{UDZw>Mnc;{aO zM~-3Q?XI?`=WRE(pI&i!w5Cd%HO3P~$k;sZdtfW?0)nmQd~1GTqos6Zkf_$y=GgjXorhzKPWF=Z)cED_3XEJYJ325rNLijl^aOetHIv1JRT!dM$ic)y=Zzu!4? z<}aN*=kYH0bKlo>eYOcl%&hU~I%d^#W`y~Rk8bV%N2rqdE)A7Ca$WF6bms-1iPzfG zHi5ASMp<}$=Q<)^H{E>IfLaf&Vn%LmT$Q)>Ip@B(Td_@c{U43SwbO;jk#PI=WmLR= zH8CcC06&b9`lFLTR)>isi8G)_Jk_SX{4UTy7&TD*Wnslh)6&=JFa!*hj=$$ym=YK` znv7q=ABv!Uv_J@PBKZdPId|Z#J8uI9XU|Ls(RIy2A&jD&vQ;os1hh;eRm?Rs6mz@` zmVFsVHx5oB24paRCHu)oH}7p05IP>-nt|MbYgI_=srLO5Tw|Q-r76asEAas25UbUG zlMwJ+H=O~=&_8>(-p)805!#D=234(NUL7)2(EOu`z}`=l8QKkm-9h0(lUjh2#61H& z0S)iV0Wv+tnGABv<>X`)%;eVv$9=E_2qx>q^oOJPD~uc;f3Il}n8 zko_`db#ZdZqoF*Q0SGGsA%vq9opFTrt=F#nJKgBOwqA%K@WecTr5>MAPG)8~P69fv z)uFp4OYOGhh#`MloumMn$-5SuxFFbLE%-E@0_9+rv82qlKRSRf__%J=p4nKe_hjbY z^|U-LP(tCfN+SroLz;cVL^oR6=66}6-;uqO9f&gqGXid4A83S$WP{1VjSHY52jd}y zK}1d8h)hRWd8QaDb4&!)Tc*6#4cmX&pYS~BWRo5j1{P}ytc36VV_Eom^6e3<2XLtz zeyG#@viX2T@A!2?iQ39-k;lRA$5I6ZYt^Q;Nwo6<4RGPg0P``*h=oG|>ceATuhah| z_GKT+0txeQH1p`ulh)*7iSGiyb*)Rf(Q-fGCfzh!Y&Qlxo;E8VB9N5Kr%t4!PDyEL zkPMmCN;CSXo#}vwhqfO)^p&5e+ALlrUWF}vqGHL(kAq|0aFdUL*#GIkggwUCATEG~ zOpGyhV~{7YZ6XjALbQRs3Rm9M->`8K(J2t#?izkcS}3AbVDFl>I{NveoWlDGQ0!sR z>!g_yq==%jL)o9aq0q!ZQHOzJ#jq*hcwld?rSm`jIEWF+Vj?ph2B1BnSWRT|Wyw|X zC*r_Sd-kXn=59-i40?XN|N4I6gR|AxF>u$X;+$LMYLHJl65!`Im(jY;HxRPAHh;6n zn1>ST$v!a{BG;IMhEpQ~+HP>wfq9dl3u_Zozo#J6UknzoVAFF_mNdU9HB_bHx_x-e z+dBbs%@7C}-U}qxIASSdd%)g`C`b8O1OMg4u^1H*J+-67!rZ*<_=6tA3MhaNSKrxL zbKyD1+-uHTHc_sn9s{-FcovVWah~AkpuMm{bWB_Ul4K#r9z)1w-6 z7>w{KJ+B4|{y$S!Ect$Nfjc)7L6cKihq4YYVOFGK+J;Jcfj2nmNWUW?K#-DVqe9RD zDHL!cNw#S$0{|%m^IHt!znP<+I4rdqeC4q)gB&+xD)wpJx3q|R zyLO2!^WraCnQXEjdYoNcZk{UnX2?H86P)jhNlfkYB5791U0asuFOQIHYTQ|Lw&M6$ z(VK9NkrsBZ%E5^673MCc=e3Gei#PcmL304c2ylEWa50G*gLj0?%|(t6JRH_aV`SbN zZY?CsZE;ZXu0(#_=c2S=QET&1p&p5ZQ#;M;fi&R60B`Mj_T>Q3{)nK5dJY~weJQpQ z@o&?@M9Y!uLm6=(ciqvx;?@jAwn)Ms!RA8 z12tg5*G9o+!~Our*_qay0Y|Ma&&Q34QG2Cu$h5AAmJ8vUWx1)0vP4#2wQaEp?h#5$ z)fYU;G_|l$;upL-vn9w!MXgEFF!{Nau#R@e@Sue|dRz44bKR8>E~*Pep{7 z)4P}X{-@^jiOs$6xB-zTfb}gBF^{@=7ESGR=6gzb91}EE8}K;5CCr)>W>_l zJl1Q90^MC~9`r6fW-KBOK*RRz!BQY@&C-RWW>iR^-tWNJS7W3l0P4M^pxV|VVzF8& z;61oCyb#@9DcUrR=t45}{{50&K z=Hvp}$>#V-6>SsPF}u-1&rE>;#fkHHVE%Awok$|E6)?Z(hTykwUJQ!Wt_F)MmeZGzrA$k*tvm-i)=M&Y?I9q81Yo4wg4?QB8aB z;A~e!Q&(5*4g20PK3-lxkCeWgI@@&D9SyC|-K1x4Nyk=RUf!x6<7$VN3~nobf9J2U zDMob9BDB2*ekxfTijVR$sQUnFjCmEty=SvB9_MSkT(@S;G*kV;^@njruBZjkAw6cT zx){4W>%ivo0x6q?A~e=*0ejRQe~g+~P{_{X8~6o7H!K}ZXw4T>PPFF`Ma~T<5uc=v zMEp~P#*vhW*k|hVlZ|)@Av&uwjz}4Bdclu%i(U*xYf*S0&RS&l`~-J{#e15N$>)}PcML5piMqkQo2Ai#S2__{b_RI~fpBGKnT`0Dc9sRBX zxUU3BWH3L)A{4MqHn5AIX62Od0H(u$kPj%JNeD){$Kf}yp}w`VGn)D*{Bh_N zA=Jd8oYb4u)v`mpMYVw&5$vJ5#5{$-VK8`T20tFMK>!#7j%p6f?8uH0CqK=h>ofG= zh4CzgHxC?VZ1hAG2l7x-QdgvAk?S;<5HWh2wHX>GG=8s4n*KF2J1mD$#3aPB$botY#k>bQkEy)dO{+JsV z2Rkk3vO55VPk!ZE(1+uX%3M*OenCjBgDiHST%A*JG8dOp(zn;n^@9cgqsQ*{ejh2% z6tSvxg|>WIfAN4=gz2+K4SW?OufFmr4RCVehLWquM}Gh(#>*RR_{@RncjXDsp3epq z1fRdSlG5iVB?bk}*`FhnW#^+BKL#U-Tzu-qVR|MJ0RmtgEsw@O>;|e#08}c5k}N&x zhEAdS-sUIy3*8UB3sg7o`OOd7Q5w8PJwLv7GFK-RF)U?^X5J?Q!FrB|xXot9_cYXW zt!R&xVkTKO`n~M@>VaU5Fc@TV0|fQwBrAF$CdENudX54|d4|>iG6|Kr8gS2`<|O6{ z07o!A$O{yWD8M}oKWz)oEhSZnNXh6@QgYy8GQBuszHTW_Pr@=_+Usx*6DtQ_EW&%g z{l@h2no=?=f*b^~86fh=UHe)68m7K&x^c{vw_G^Oo|x>&GRW2P>xV4Gq{5>(A75Vd zDjvVB?cGq6Vn@Ru21o_<>O_shoTqcqN=LY-kvfg-EGhFjJ(nuF*~K1i{8$h&B+CvgmE{7p8F5-K*z5S(cXLb9IMI38x9s=EmG5u+Rr?wSc7OQCl{jy5S7I7>0|X$77QlF8Lv=75m-uV8=X!clBsZG zCN=<0E-ut=!bDBLjklJQN@GZ)`KDvT8~RGPehEvwUWtgkUk0nFqqb4 z4X|>DSLgp~>(`!X4)D6%C$4MX zfjvHZ1yv;Y-26Iw$LJsb{~!PPsXUtf16}x^pDl-3{9z9LW`S3B)iW53{l4Z_o*r(F zo?iCn92gAm_K9jELzB-&hINLuhh!TSniVW%bDLCT>s4}1x${gbk|xv(^5qj z)+}DwTqU!gi_v;rF!93qM~84%ak{Iy(+@7k)zp}7Cw_2oaP?GEGqpeE;6Z;wfrG)2 zXUOvk^#k(G^TaX%m>y|X?-B~MZQucmM5wObw)F%Joy6~SL&80Z=^?`t2s_&@tD9t{8h diff --git a/docs/images/tutorial_graph.png b/docs/images/tutorial_graph.png deleted file mode 100644 index 9c42f0389df4486b40cd3172535d202898bd4063..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21770 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 KMNUMnLSTZ3{6FRZ diff --git a/docs/includes/code-of-conduct.txt b/docs/includes/code-of-conduct.txt deleted file mode 100644 index 7141c712..00000000 --- a/docs/includes/code-of-conduct.txt +++ /dev/null @@ -1,43 +0,0 @@ -Code of Conduct -=============== - -Everyone interacting in the project's codebases, issue trackers, chat rooms, -and mailing lists is expected to follow the Mode Code of Conduct. - -As contributors and maintainers of these projects, and in the interest of fostering -an open and welcoming community, we pledge to respect all people who contribute -through reporting issues, posting feature requests, updating documentation, -submitting pull requests or patches, and other activities. - -We are committed to making participation in these projects a harassment-free -experience for everyone, regardless of level of experience, gender, -gender identity and expression, sexual orientation, disability, -personal appearance, body size, race, ethnicity, age, -religion, or nationality. - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing other's private information, such as physical - or electronic addresses, without explicit permission -* Other unethical or unprofessional conduct. - -Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct. By adopting this Code of Conduct, -project maintainers commit themselves to fairly and consistently applying -these principles to every aspect of managing this project. Project maintainers -who do not follow or enforce the Code of Conduct may be permanently removed from -the project team. - -This code of conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by opening an issue or contacting one or more of the project maintainers. - -This Code of Conduct is adapted from the Contributor Covenant, -version 1.2.0 available at http://contributor-covenant.org/version/1/2/0/. diff --git a/docs/includes/faq.txt b/docs/includes/faq.txt deleted file mode 100644 index 4490b9a4..00000000 --- a/docs/includes/faq.txt +++ /dev/null @@ -1,130 +0,0 @@ -FAQ -=== - -Can I use Mode with Django/Flask/etc.? --------------------------------------- - -Yes! Use gevent/eventlet as a bridge to integrate with asyncio. - -Using ``gevent`` -~~~~~~~~~~~~~~~~ - -This works with any blocking Python library that can work with gevent. - -Using gevent requires you to install the ``aiogevent`` module, -and you can install this as a bundle with Mode: - -.. sourcecode:: console - - $ pip install -U mode[gevent] - -Then to actually use gevent as the event loop you have to -execute the following in your entrypoint module (usually where you -start the worker), before any other third party libraries are imported:: - - #!/usr/bin/env python3 - import mode.loop - mode.loop.use('gevent') - # execute program - -REMEMBER: This must be located at the very top of the module, -in such a way that it executes before you import other libraries. - - -Using ``eventlet`` -~~~~~~~~~~~~~~~~~~ - -This works with any blocking Python library that can work with eventlet. - -Using eventlet requires you to install the ``aioeventlet`` module, -and you can install this as a bundle with Mode: - -.. sourcecode:: console - - $ pip install -U mode[eventlet] - -Then to actually use eventlet as the event loop you have to -execute the following in your entrypoint module (usually where you -start the worker), before any other third party libraries are imported:: - - #!/usr/bin/env python3 - import mode.loop - mode.loop.use('eventlet') - # execute program - -REMEMBER: It's very important this is at the very top of the module, -and that it executes before you import libraries. - -Can I use Mode with Tornado? ----------------------------- - -Yes! Use the ``tornado.platform.asyncio`` bridge: -http://www.tornadoweb.org/en/stable/asyncio.html - -Can I use Mode with Twisted? ------------------------------ - -Yes! Use the asyncio reactor implementation: -https://twistedmatrix.com/documents/17.1.0/api/twisted.internet.asyncioreactor.html - -Will you support Python 3.5 or earlier? ---------------------------------------- - -There are no immediate plans to support Python 3.5, but you are welcome to -contribute to the project. - -Here are some of the steps required to accomplish this: - -- Source code transformation to rewrite variable annotations to comments - - for example, the code:: - - class Point: - x: int = 0 - y: int = 0 - - must be rewritten into:: - - class Point: - x = 0 # type: int - y = 0 # type: int - -- Source code transformation to rewrite async functions - - for example, the code:: - - async def foo(): - await asyncio.sleep(1.0) - - must be rewritten into:: - - @coroutine - def foo(): - yield from asyncio.sleep(1.0) - -Will you support Python 2? --------------------------- - -There are no plans to support Python 2, but you are welcome to contribute to -the project (details in question above is relevant also for Python 2). - - -At Shutdown I get lots of warnings, what is this about? -------------------------------------------------------- - -If you get warnings such as this at shutdown: - -.. sourcecode:: text - - Task was destroyed but it is pending! - task: wait_for=()]>> - Task was destroyed but it is pending! - task: wait_for=()]>> - Task was destroyed but it is pending! - task: wait_for=()]>> - Task was destroyed but it is pending! - task: cb=[_release_waiter(()]>)() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:316]> - Task was destroyed but it is pending! - task: cb=[_release_waiter(()]>)() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:316]> - -It usually means you forgot to stop a service before the process exited. diff --git a/docs/includes/installation.txt b/docs/includes/installation.txt deleted file mode 100644 index 2827492a..00000000 --- a/docs/includes/installation.txt +++ /dev/null @@ -1,42 +0,0 @@ -.. _installation: - -Installation -============ - -You can install Mode either via the Python Package Index (PyPI) -or from source. - -To install using `pip`:: - - $ pip install -U mode-streaming - -.. _installing-from-source: - -Downloading and installing from source --------------------------------------- - -Download the latest version of Mode from -http://pypi.org/project/mode-streaming - -You can install it by doing the following:: - - $ tar xvfz mode-streaming-0.1.0.tar.gz - $ cd mode-streaming-0.1.0 - $ python build - # python install - -The last command must be executed as a privileged user if -you are not currently using a virtualenv. - -.. _installing-from-git: - -Using the development version ------------------------------ - -With pip -~~~~~~~~ - -You can install the latest snapshot of Mode using the following -pip command:: - - $ pip install https://github.com/faust-streaming/mode/zipball/master#egg=mode diff --git a/docs/includes/introduction.txt b/docs/index.md similarity index 86% rename from docs/includes/introduction.txt rename to docs/index.md index 28bcb433..f3bcb74f 100644 --- a/docs/includes/introduction.txt +++ b/docs/index.md @@ -1,4 +1,4 @@ -.. image:: https://img.shields.io/pypi/v/mode-streaming.svg + Mode is a very minimal Python library built-on top of AsyncIO that makes it much easier to use. @@ -21,8 +18,9 @@ it much easier to use. In Mode your program is built out of services that you can start, stop, restart and supervise. -A service is just a class:: +A service is just a class: +```python class PageViewCache(Service): redis: Redis = None @@ -34,13 +32,15 @@ A service is just a class:: async def get(self, url: str) -> int: return await self.redis.get(url) +``` Services are started, stopped and restarted and have callbacks for those actions. -It can start another service:: +It can start another service: +```python class App(Service): page_view_cache: PageViewCache = None @@ -50,14 +50,17 @@ It can start another service:: @cached_property def page_view_cache(self) -> PageViewCache: return PageViewCache() +``` -It can include background tasks:: +It can include background tasks: +```python class PageViewCache(Service): @Service.timer(1.0) async def _update_cache(self) -> None: self.data = await cache.get('key') +``` Services that depends on other services actually form a graph that you can visualize. @@ -66,7 +69,7 @@ Worker Mode optionally provides a worker that you can use to start the program, with support for logging, blocking detection, remote debugging and more. - To start a worker add this to your program:: + To start a worker add this to your program: if __name__ == '__main__': from mode import Worker @@ -74,8 +77,7 @@ Worker Then execute your program to start the worker: - .. sourcecode:: console - + ```sh $ python examples/tutorial.py [2018-03-27 15:47:12,159: INFO]: [^Worker]: Starting... [2018-03-27 15:47:12,160: INFO]: [^-AppService]: Starting... @@ -86,11 +88,11 @@ Worker [2018-03-27 15:47:12,164: INFO]: [^--Webserver]: Serving on port 8000 REMOVING EXPIRED USERS REMOVING EXPIRED USERS + ``` To stop it hit :kbd:`Control-c`: - .. sourcecode:: console - + ```sh [2018-03-27 15:55:08,084: INFO]: [^Worker]: Stopping on signal received... [2018-03-27 15:55:08,084: INFO]: [^Worker]: Stopping... [2018-03-27 15:55:08,084: INFO]: [^-AppService]: Stopping... @@ -110,24 +112,27 @@ Worker [2018-03-27 15:55:08,086: INFO]: [^--Websockets]: -Stopped! [2018-03-27 15:55:08,087: INFO]: [^-AppService]: -Stopped! [2018-03-27 15:55:08,087: INFO]: [^Worker]: -Stopped! + ``` Beacons The ``beacon`` object that we pass to services keeps track of the services in a graph. - They are not stricly required, but can be used to visualize a running + They are not strictly required, but can be used to visualize a running system, for example we can render it as a pretty graph. - This requires you to have the ``pydot`` library and GraphViz + This requires you to have the `pydot` library and GraphViz installed: - .. sourcecode:: console - - $ pip install pydot + ```sh + $ pip install pydot + ``` Let's change the app service class to dump the graph to an image - at startup:: + at startup: + + ```python class AppService(Service): async def on_start(self) -> None: @@ -141,14 +146,15 @@ Beacons print('WRITING GRAPH TO image.png') with open('image.png', 'wb') as fh: fh.write(graph.create_png()) + ``` -Creating a Service -================== +## Creating a Service To define a service, simply subclass and fill in the methods -to do stuff as the service is started/stopped etc.:: +to do stuff as the service is started/stopped etc.: +```python class MyService(Service): async def on_start(self) -> None: @@ -159,14 +165,18 @@ to do stuff as the service is started/stopped etc.:: async def on_stop(self) -> None: print('Im stopping now') +``` -To start the service, call ``await service.start()``:: +To start the service, call ``await service.start()``: +```python await service.start() +``` Or you can use ``mode.Worker`` (or a subclass of this) to start your -services-based asyncio program from the console:: +services-based asyncio program from the console: +```python if __name__ == '__main__': import mode worker = mode.Worker( @@ -176,21 +186,24 @@ services-based asyncio program from the console:: daemon=False, ) worker.execute_from_commandline() +``` -It's a Graph! -============= +## It's a Graph! Services can start other services, coroutines, and background tasks. -1) Starting other services using ``add_depenency``:: +1) Starting other services using ``add_dependency``: +```python class MyService(Service): def __post_init__(self) -> None: self.add_dependency(OtherService(loop=self.loop)) +``` -2) Start a list of services using ``on_init_dependencies``:: +2) Start a list of services using ``on_init_dependencies``: +```python class MyService(Service): def on_init_dependencies(self) -> None: @@ -199,9 +212,11 @@ Services can start other services, coroutines, and background tasks. ServiceB(loop=self.loop), ServiceC(loop=self.loop), ] +``` -3) Start a future/coroutine (that will be waited on to complete on stop):: +3) Start a future/coroutine (that will be waited on to complete on stop): +```python class MyService(Service): async def on_start(self) -> None: @@ -209,18 +224,22 @@ Services can start other services, coroutines, and background tasks. async def my_coro(self) -> None: print('Executing coroutine') +``` -4) Start a background task:: +4) Start a background task: +```python class MyService(Service): @Service.task async def _my_coro(self) -> None: print('Executing coroutine') +``` -5) Start a background task that keeps running:: +5) Start a background task that keeps running: +```python class MyService(Service): @Service.task @@ -230,3 +249,4 @@ Services can start other services, coroutines, and background tasks. # until service stopped/crashed. await self.sleep(1.0) print('Background thread waking up') +``` diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 156eb339..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,32 +0,0 @@ -======================================================================= - Mode - AsyncIO Services -======================================================================= - -Contents -======== - -.. toctree:: - :maxdepth: 1 - - copyright - -.. toctree:: - :maxdepth: 2 - - introduction - userguide/index - -.. toctree:: - :maxdepth: 1 - - faq - reference/index - changelog - glossary - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/introduction.rst b/docs/introduction.rst deleted file mode 100644 index 44b69755..00000000 --- a/docs/introduction.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _intro: - -============================= - Introduction to Mode -============================= - -.. contents:: - :local: - :depth: 1 - -.. include:: includes/introduction.txt diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 001e23ff..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,272 +0,0 @@ -@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. epub3 to make an epub3 - 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% 1>NUL 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\PROJ.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PROJ.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" == "epub3" ( - %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. - 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 diff --git a/docs/reference/index.rst b/docs/reference/index.rst deleted file mode 100644 index e88ca1ba..00000000 --- a/docs/reference/index.rst +++ /dev/null @@ -1,75 +0,0 @@ -.. _apiref: - -=============== - API Reference -=============== - -:Release: |version| -:Date: |today| - -Mode -==== - -.. toctree:: - :maxdepth: 1 - - mode - mode.debug - mode.exceptions - mode.locals - mode.proxy - mode.services - mode.signals - mode.supervisors - mode.threads - mode.timers - mode.worker - -Typehints -========= - -.. toctree:: - :maxdepth: 1 - - mode.types - mode.types.services - mode.types.signals - mode.types.supervisors - -Event Loops -=========== - -.. toctree:: - :maxdepth: 1 - - mode.loop - mode.loop.eventlet - mode.loop.gevent - mode.loop.uvloop - -Utils -===== - -.. toctree:: - :maxdepth: 1 - - mode.utils.aiter - mode.utils.collections - mode.utils.compat - mode.utils.contexts - mode.utils.futures - mode.utils.graphs - mode.utils.imports - mode.utils.locals - mode.utils.locks - mode.utils.logging - mode.utils.loops - mode.utils.mocks - mode.utils.objects - mode.utils.queues - mode.utils.text - mode.utils.times - mode.utils.tracebacks - mode.utils.trees - mode.utils.types.graphs - mode.utils.types.trees diff --git a/docs/reference/mode.debug.rst b/docs/reference/mode.debug.rst deleted file mode 100644 index d703a02a..00000000 --- a/docs/reference/mode.debug.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.debug`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.debug - -.. automodule:: mode.debug - :members: - :undoc-members: diff --git a/docs/reference/mode.exceptions.rst b/docs/reference/mode.exceptions.rst deleted file mode 100644 index 11cfb5f9..00000000 --- a/docs/reference/mode.exceptions.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.exceptions`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.exceptions - -.. automodule:: mode.exceptions - :members: - :undoc-members: diff --git a/docs/reference/mode.locals.rst b/docs/reference/mode.locals.rst deleted file mode 100644 index a2119e42..00000000 --- a/docs/reference/mode.locals.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.locals`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.locals - -.. automodule:: mode.locals - :members: - :undoc-members: diff --git a/docs/reference/mode.loop.eventlet.rst b/docs/reference/mode.loop.eventlet.rst deleted file mode 100644 index e2cac7f0..00000000 --- a/docs/reference/mode.loop.eventlet.rst +++ /dev/null @@ -1,12 +0,0 @@ -===================================================== - ``mode.loop.eventlet`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.loop.eventlet - -.. warning:: - - Importing this module directly will set the global event loop. - See :mod:`faust.loop` for more information. diff --git a/docs/reference/mode.loop.gevent.rst b/docs/reference/mode.loop.gevent.rst deleted file mode 100644 index 6f50695f..00000000 --- a/docs/reference/mode.loop.gevent.rst +++ /dev/null @@ -1,12 +0,0 @@ -===================================================== - ``mode.loop.gevent`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.loop.gevent - -.. warning:: - - Importing this module directly will set the global event loop. - See :mod:`faust.loop` for more information. diff --git a/docs/reference/mode.loop.rst b/docs/reference/mode.loop.rst deleted file mode 100644 index fc6afc4a..00000000 --- a/docs/reference/mode.loop.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.loop`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.loop - -.. automodule:: mode.loop - :members: - :undoc-members: diff --git a/docs/reference/mode.loop.uvloop.rst b/docs/reference/mode.loop.uvloop.rst deleted file mode 100644 index 72fe42a5..00000000 --- a/docs/reference/mode.loop.uvloop.rst +++ /dev/null @@ -1,12 +0,0 @@ -===================================================== - ``mode.loop.uvloop`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.loop.uvloop - -.. warning:: - - Importing this module directly will set the global event loop. - See :mod:`faust.loop` for more information. diff --git a/docs/reference/mode.proxy.rst b/docs/reference/mode.proxy.rst deleted file mode 100644 index 64e2b90c..00000000 --- a/docs/reference/mode.proxy.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.proxy`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.proxy - -.. automodule:: mode.proxy - :members: - :undoc-members: diff --git a/docs/reference/mode.rst b/docs/reference/mode.rst deleted file mode 100644 index e46aa68f..00000000 --- a/docs/reference/mode.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode - -.. automodule:: mode - :members: - :undoc-members: diff --git a/docs/reference/mode.services.rst b/docs/reference/mode.services.rst deleted file mode 100644 index 49a1ab5d..00000000 --- a/docs/reference/mode.services.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.services`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.services - -.. automodule:: mode.services - :members: - :undoc-members: diff --git a/docs/reference/mode.signals.rst b/docs/reference/mode.signals.rst deleted file mode 100644 index 14e11024..00000000 --- a/docs/reference/mode.signals.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.signals`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.signals - -.. automodule:: mode.signals - :members: - :undoc-members: diff --git a/docs/reference/mode.supervisors.rst b/docs/reference/mode.supervisors.rst deleted file mode 100644 index d8627373..00000000 --- a/docs/reference/mode.supervisors.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.supervisors`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.supervisors - -.. automodule:: mode.supervisors - :members: - :undoc-members: diff --git a/docs/reference/mode.threads.rst b/docs/reference/mode.threads.rst deleted file mode 100644 index 2f60f61d..00000000 --- a/docs/reference/mode.threads.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.threads`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.threads - -.. automodule:: mode.threads - :members: - :undoc-members: diff --git a/docs/reference/mode.timers.rst b/docs/reference/mode.timers.rst deleted file mode 100644 index 0153fb9d..00000000 --- a/docs/reference/mode.timers.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.timers`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.timers - -.. automodule:: mode.timers - :members: - :undoc-members: diff --git a/docs/reference/mode.types.rst b/docs/reference/mode.types.rst deleted file mode 100644 index 7726a0a5..00000000 --- a/docs/reference/mode.types.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.types`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.types - -.. automodule:: mode.types - :members: - :undoc-members: diff --git a/docs/reference/mode.types.services.rst b/docs/reference/mode.types.services.rst deleted file mode 100644 index 742d9f36..00000000 --- a/docs/reference/mode.types.services.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.types.services`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.types.services - -.. automodule:: mode.types.services - :members: - :undoc-members: diff --git a/docs/reference/mode.types.signals.rst b/docs/reference/mode.types.signals.rst deleted file mode 100644 index ac4d4a6d..00000000 --- a/docs/reference/mode.types.signals.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.types.signals`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.types.signals - -.. automodule:: mode.types.signals - :members: - :undoc-members: diff --git a/docs/reference/mode.types.supervisors.rst b/docs/reference/mode.types.supervisors.rst deleted file mode 100644 index 3e98b423..00000000 --- a/docs/reference/mode.types.supervisors.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.types.supervisors`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.types.supervisors - -.. automodule:: mode.types.supervisors - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.aiter.rst b/docs/reference/mode.utils.aiter.rst deleted file mode 100644 index 65cfe6c4..00000000 --- a/docs/reference/mode.utils.aiter.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.aiter`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.aiter - -.. automodule:: mode.utils.aiter - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.collections.rst b/docs/reference/mode.utils.collections.rst deleted file mode 100644 index e5e6daaf..00000000 --- a/docs/reference/mode.utils.collections.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.collections`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.collections - -.. automodule:: mode.utils.collections - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.compat.rst b/docs/reference/mode.utils.compat.rst deleted file mode 100644 index 850e0279..00000000 --- a/docs/reference/mode.utils.compat.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.compat`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.compat - -.. automodule:: mode.utils.compat - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.contexts.rst b/docs/reference/mode.utils.contexts.rst deleted file mode 100644 index cb7454b5..00000000 --- a/docs/reference/mode.utils.contexts.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.contexts`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.contexts - -.. automodule:: mode.utils.contexts - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.cron.rst b/docs/reference/mode.utils.cron.rst deleted file mode 100644 index 4f02440c..00000000 --- a/docs/reference/mode.utils.cron.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.cron`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.cron - -.. automodule:: mode.utils.cron - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.futures.rst b/docs/reference/mode.utils.futures.rst deleted file mode 100644 index 5b0d5f85..00000000 --- a/docs/reference/mode.utils.futures.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.futures`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.futures - -.. automodule:: mode.utils.futures - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.graphs.rst b/docs/reference/mode.utils.graphs.rst deleted file mode 100644 index 6d5bcf88..00000000 --- a/docs/reference/mode.utils.graphs.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.graphs`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.graphs - -.. automodule:: mode.utils.graphs - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.imports.rst b/docs/reference/mode.utils.imports.rst deleted file mode 100644 index e363464f..00000000 --- a/docs/reference/mode.utils.imports.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.imports`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.imports - -.. automodule:: mode.utils.imports - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.locals.rst b/docs/reference/mode.utils.locals.rst deleted file mode 100644 index 9534deaa..00000000 --- a/docs/reference/mode.utils.locals.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.locals`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.locals - -.. automodule:: mode.utils.locals - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.locks.rst b/docs/reference/mode.utils.locks.rst deleted file mode 100644 index 8276b3d9..00000000 --- a/docs/reference/mode.utils.locks.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.locks`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.locks - -.. automodule:: mode.utils.locks - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.logging.rst b/docs/reference/mode.utils.logging.rst deleted file mode 100644 index 9b9cd701..00000000 --- a/docs/reference/mode.utils.logging.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.logging`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.logging - -.. automodule:: mode.utils.logging - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.loops.rst b/docs/reference/mode.utils.loops.rst deleted file mode 100644 index a2a188bb..00000000 --- a/docs/reference/mode.utils.loops.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.loops`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.loops - -.. automodule:: mode.utils.loops - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.mocks.rst b/docs/reference/mode.utils.mocks.rst deleted file mode 100644 index ed11f717..00000000 --- a/docs/reference/mode.utils.mocks.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.mocks`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.mocks - -.. automodule:: mode.utils.mocks - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.objects.rst b/docs/reference/mode.utils.objects.rst deleted file mode 100644 index 3bc6db91..00000000 --- a/docs/reference/mode.utils.objects.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.objects`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.objects - -.. automodule:: mode.utils.objects - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.queues.rst b/docs/reference/mode.utils.queues.rst deleted file mode 100644 index e12cf391..00000000 --- a/docs/reference/mode.utils.queues.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.queues`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.queues - -.. automodule:: mode.utils.queues - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.text.rst b/docs/reference/mode.utils.text.rst deleted file mode 100644 index 7ca42bdd..00000000 --- a/docs/reference/mode.utils.text.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.text`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.text - -.. automodule:: mode.utils.text - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.times.rst b/docs/reference/mode.utils.times.rst deleted file mode 100644 index e4d32388..00000000 --- a/docs/reference/mode.utils.times.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.times`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.times - -.. automodule:: mode.utils.times - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.tracebacks.rst b/docs/reference/mode.utils.tracebacks.rst deleted file mode 100644 index 129a0554..00000000 --- a/docs/reference/mode.utils.tracebacks.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.tracebacks`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.tracebacks - -.. automodule:: mode.utils.tracebacks - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.trees.rst b/docs/reference/mode.utils.trees.rst deleted file mode 100644 index 1ee2f373..00000000 --- a/docs/reference/mode.utils.trees.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.trees`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.trees - -.. automodule:: mode.utils.trees - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.types.graphs.rst b/docs/reference/mode.utils.types.graphs.rst deleted file mode 100644 index d4ac9eba..00000000 --- a/docs/reference/mode.utils.types.graphs.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.types.graphs`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.types.graphs - -.. automodule:: mode.utils.types.graphs - :members: - :undoc-members: diff --git a/docs/reference/mode.utils.types.trees.rst b/docs/reference/mode.utils.types.trees.rst deleted file mode 100644 index 5d5998bb..00000000 --- a/docs/reference/mode.utils.types.trees.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.utils.types.trees`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.utils.types.trees - -.. automodule:: mode.utils.types.trees - :members: - :undoc-members: diff --git a/docs/reference/mode.worker.rst b/docs/reference/mode.worker.rst deleted file mode 100644 index a5e53385..00000000 --- a/docs/reference/mode.worker.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================================================== - ``mode.worker`` -===================================================== - -.. contents:: - :local: -.. currentmodule:: mode.worker - -.. automodule:: mode.worker - :members: - :undoc-members: diff --git a/docs/references/mode.debug.md b/docs/references/mode.debug.md new file mode 100644 index 00000000..fd800251 --- /dev/null +++ b/docs/references/mode.debug.md @@ -0,0 +1,3 @@ +# mode.debug + +::: mode.debug diff --git a/docs/references/mode.exceptions.md b/docs/references/mode.exceptions.md new file mode 100644 index 00000000..2052d232 --- /dev/null +++ b/docs/references/mode.exceptions.md @@ -0,0 +1,3 @@ +# mode.exceptions + +::: mode.exceptions diff --git a/docs/references/mode.locals.md b/docs/references/mode.locals.md new file mode 100644 index 00000000..499435fc --- /dev/null +++ b/docs/references/mode.locals.md @@ -0,0 +1,3 @@ +# mode.locals + +::: mode.locals diff --git a/docs/references/mode.loop.eventlet.md b/docs/references/mode.loop.eventlet.md new file mode 100644 index 00000000..072f34a1 --- /dev/null +++ b/docs/references/mode.loop.eventlet.md @@ -0,0 +1,8 @@ +# mode.loop.eventlet + +::: mode.loop.eventlet + +!!! warning + + Importing this module directly will set the global event loop. + See :mod:`faust.loop` for more information. diff --git a/docs/references/mode.loop.gevent.md b/docs/references/mode.loop.gevent.md new file mode 100644 index 00000000..8f0a229f --- /dev/null +++ b/docs/references/mode.loop.gevent.md @@ -0,0 +1,8 @@ +# mode.loop.gevent`` + +::: mode.loop.gevent + +!!! warning + + Importing this module directly will set the global event loop. + See :mod:`faust.loop` for more information. diff --git a/docs/references/mode.loop.md b/docs/references/mode.loop.md new file mode 100644 index 00000000..385d6e59 --- /dev/null +++ b/docs/references/mode.loop.md @@ -0,0 +1,3 @@ +# mode.loop + +::: mode.loop diff --git a/docs/references/mode.loop.uvloop.md b/docs/references/mode.loop.uvloop.md new file mode 100644 index 00000000..c1f5044a --- /dev/null +++ b/docs/references/mode.loop.uvloop.md @@ -0,0 +1,8 @@ +# mode.loop.uvloop + +::: mode.loop.uvloop + +!!! warning + + Importing this module directly will set the global event loop. + See :mod:`faust.loop` for more information. diff --git a/docs/references/mode.md b/docs/references/mode.md new file mode 100644 index 00000000..342308cf --- /dev/null +++ b/docs/references/mode.md @@ -0,0 +1,3 @@ +# mode + +::: mode diff --git a/docs/references/mode.proxy.md b/docs/references/mode.proxy.md new file mode 100644 index 00000000..254098b5 --- /dev/null +++ b/docs/references/mode.proxy.md @@ -0,0 +1,3 @@ +# mode.proxy + +::: mode.proxy diff --git a/docs/references/mode.services.md b/docs/references/mode.services.md new file mode 100644 index 00000000..fa067837 --- /dev/null +++ b/docs/references/mode.services.md @@ -0,0 +1,3 @@ +# mode.services + +::: mode.services diff --git a/docs/references/mode.signals.md b/docs/references/mode.signals.md new file mode 100644 index 00000000..b9e7f9bb --- /dev/null +++ b/docs/references/mode.signals.md @@ -0,0 +1,3 @@ +# mode.signals + +::: mode.signals diff --git a/docs/references/mode.supervisors.md b/docs/references/mode.supervisors.md new file mode 100644 index 00000000..0628521e --- /dev/null +++ b/docs/references/mode.supervisors.md @@ -0,0 +1,3 @@ +# mode.supervisors + +::: mode.supervisors diff --git a/docs/references/mode.threads.md b/docs/references/mode.threads.md new file mode 100644 index 00000000..cc473d2c --- /dev/null +++ b/docs/references/mode.threads.md @@ -0,0 +1,3 @@ +# mode.threads + +::: mode.threads diff --git a/docs/references/mode.timers.md b/docs/references/mode.timers.md new file mode 100644 index 00000000..5b6e5629 --- /dev/null +++ b/docs/references/mode.timers.md @@ -0,0 +1,3 @@ +# mode.timers + +::: mode.timers diff --git a/docs/references/mode.types.md b/docs/references/mode.types.md new file mode 100644 index 00000000..87647a15 --- /dev/null +++ b/docs/references/mode.types.md @@ -0,0 +1,3 @@ +# mode.types + +::: mode.types diff --git a/docs/references/mode.types.services.md b/docs/references/mode.types.services.md new file mode 100644 index 00000000..510dec0d --- /dev/null +++ b/docs/references/mode.types.services.md @@ -0,0 +1,3 @@ +# mode.types.services + +::: mode.types.services diff --git a/docs/references/mode.types.signals.md b/docs/references/mode.types.signals.md new file mode 100644 index 00000000..0fd70254 --- /dev/null +++ b/docs/references/mode.types.signals.md @@ -0,0 +1,3 @@ +# mode.types.signals + +::: mode.types.signals diff --git a/docs/references/mode.types.supervisors.md b/docs/references/mode.types.supervisors.md new file mode 100644 index 00000000..8b9f8adb --- /dev/null +++ b/docs/references/mode.types.supervisors.md @@ -0,0 +1,3 @@ +# mode.types.supervisors + +::: mode.types.supervisors diff --git a/docs/references/mode.utils.aiter.md b/docs/references/mode.utils.aiter.md new file mode 100644 index 00000000..ace89a8e --- /dev/null +++ b/docs/references/mode.utils.aiter.md @@ -0,0 +1,3 @@ +# mode.utils.aiter + +::: mode.utils.aiter diff --git a/docs/references/mode.utils.collections.md b/docs/references/mode.utils.collections.md new file mode 100644 index 00000000..3579aa74 --- /dev/null +++ b/docs/references/mode.utils.collections.md @@ -0,0 +1,3 @@ +# mode.utils.collections + +::: mode.utils.collections diff --git a/docs/references/mode.utils.compat.md b/docs/references/mode.utils.compat.md new file mode 100644 index 00000000..ae13ced1 --- /dev/null +++ b/docs/references/mode.utils.compat.md @@ -0,0 +1,3 @@ +# mode.utils.compat + +::: mode.utils.compat diff --git a/docs/references/mode.utils.contexts.md b/docs/references/mode.utils.contexts.md new file mode 100644 index 00000000..8d5cce00 --- /dev/null +++ b/docs/references/mode.utils.contexts.md @@ -0,0 +1,3 @@ +# mode.utils.contexts + +::: mode.utils.contexts diff --git a/docs/references/mode.utils.cron.md b/docs/references/mode.utils.cron.md new file mode 100644 index 00000000..870c1364 --- /dev/null +++ b/docs/references/mode.utils.cron.md @@ -0,0 +1,3 @@ +# mode.utils.cron + +::: mode.utils.cron diff --git a/docs/references/mode.utils.futures.md b/docs/references/mode.utils.futures.md new file mode 100644 index 00000000..81f1a594 --- /dev/null +++ b/docs/references/mode.utils.futures.md @@ -0,0 +1,3 @@ +# mode.utils.futures + +::: mode.utils.futures diff --git a/docs/references/mode.utils.graphs.md b/docs/references/mode.utils.graphs.md new file mode 100644 index 00000000..98f0515f --- /dev/null +++ b/docs/references/mode.utils.graphs.md @@ -0,0 +1,3 @@ +# mode.utils.graphs + +::: mode.utils.graphs diff --git a/docs/references/mode.utils.imports.md b/docs/references/mode.utils.imports.md new file mode 100644 index 00000000..aec9e8bf --- /dev/null +++ b/docs/references/mode.utils.imports.md @@ -0,0 +1,3 @@ +# mode.utils.imports + +::: mode.utils.imports diff --git a/docs/references/mode.utils.locals.md b/docs/references/mode.utils.locals.md new file mode 100644 index 00000000..ff5ea55b --- /dev/null +++ b/docs/references/mode.utils.locals.md @@ -0,0 +1,3 @@ +# mode.utils.locals + +::: mode.utils.locals diff --git a/docs/references/mode.utils.locks.md b/docs/references/mode.utils.locks.md new file mode 100644 index 00000000..43cfb520 --- /dev/null +++ b/docs/references/mode.utils.locks.md @@ -0,0 +1,3 @@ +# mode.utils.locks + +::: mode.utils.locks diff --git a/docs/references/mode.utils.logging.md b/docs/references/mode.utils.logging.md new file mode 100644 index 00000000..9ef72eb3 --- /dev/null +++ b/docs/references/mode.utils.logging.md @@ -0,0 +1,3 @@ +# mode.utils.logging + +::: mode.utils.logging diff --git a/docs/references/mode.utils.loops.md b/docs/references/mode.utils.loops.md new file mode 100644 index 00000000..13fc0fe8 --- /dev/null +++ b/docs/references/mode.utils.loops.md @@ -0,0 +1,3 @@ +# mode.utils.loops + +::: mode.utils.loops diff --git a/docs/references/mode.utils.mocks.md b/docs/references/mode.utils.mocks.md new file mode 100644 index 00000000..1322ff3f --- /dev/null +++ b/docs/references/mode.utils.mocks.md @@ -0,0 +1,3 @@ +# mode.utils.mocks + +::: mode.utils.mocks diff --git a/docs/references/mode.utils.objects.md b/docs/references/mode.utils.objects.md new file mode 100644 index 00000000..99d3dfd8 --- /dev/null +++ b/docs/references/mode.utils.objects.md @@ -0,0 +1,3 @@ +# mode.utils.objects + +::: mode.utils.objects diff --git a/docs/references/mode.utils.queues.md b/docs/references/mode.utils.queues.md new file mode 100644 index 00000000..c0914c99 --- /dev/null +++ b/docs/references/mode.utils.queues.md @@ -0,0 +1,3 @@ +# mode.utils.queues + +::: mode.utils.queues diff --git a/docs/references/mode.utils.text.md b/docs/references/mode.utils.text.md new file mode 100644 index 00000000..8b129435 --- /dev/null +++ b/docs/references/mode.utils.text.md @@ -0,0 +1,3 @@ +# mode.utils.text + +::: mode.utils.text diff --git a/docs/references/mode.utils.times.md b/docs/references/mode.utils.times.md new file mode 100644 index 00000000..e2606e04 --- /dev/null +++ b/docs/references/mode.utils.times.md @@ -0,0 +1,3 @@ +# mode.utils.times + +::: mode.utils.times diff --git a/docs/references/mode.utils.tracebacks.md b/docs/references/mode.utils.tracebacks.md new file mode 100644 index 00000000..65dd38d6 --- /dev/null +++ b/docs/references/mode.utils.tracebacks.md @@ -0,0 +1,3 @@ +# mode.utils.tracebacks + +::: mode.utils.tracebacks diff --git a/docs/references/mode.utils.trees.md b/docs/references/mode.utils.trees.md new file mode 100644 index 00000000..e7323d1f --- /dev/null +++ b/docs/references/mode.utils.trees.md @@ -0,0 +1,3 @@ +# mode.utils.trees + +::: mode.utils.trees diff --git a/docs/references/mode.utils.types.graphs.md b/docs/references/mode.utils.types.graphs.md new file mode 100644 index 00000000..4443b8d9 --- /dev/null +++ b/docs/references/mode.utils.types.graphs.md @@ -0,0 +1,3 @@ +# mode.utils.types.graphs + +::: mode.utils.types.graphs diff --git a/docs/references/mode.utils.types.trees.md b/docs/references/mode.utils.types.trees.md new file mode 100644 index 00000000..8e63d32a --- /dev/null +++ b/docs/references/mode.utils.types.trees.md @@ -0,0 +1,3 @@ +# mode.utils.types.trees + +::: mode.utils.types.trees diff --git a/docs/references/mode.worker.md b/docs/references/mode.worker.md new file mode 100644 index 00000000..8c483114 --- /dev/null +++ b/docs/references/mode.worker.md @@ -0,0 +1,3 @@ +# mode.worker + +::: mode.worker diff --git a/docs/templates/readme.txt b/docs/templates/readme.txt deleted file mode 100644 index 7e23aaf3..00000000 --- a/docs/templates/readme.txt +++ /dev/null @@ -1,36 +0,0 @@ -===================================================================== - Mode: AsyncIO Services -===================================================================== - -|build-status| |coverage| |license| |wheel| |pyversion| |pyimp| - -.. include:: ../includes/introduction.txt - -.. include:: ../includes/installation.txt - -.. include:: ../includes/faq.txt - -.. include:: ../includes/code-of-conduct.txt - -.. |build-status| image:: https://travis-ci.com/faust-streaming/mode.png?branch=master - :alt: Build status - :target: https://travis-ci.com/faust-streaming/mode - -.. |coverage| image:: https://codecov.io/github/faust-streaming/mode/coverage.svg?branch=master - :target: https://codecov.io/github/faust-streaming/mode?branch=master - -.. |license| image:: https://img.shields.io/pypi/l/mode-streaming.svg - :alt: BSD License - :target: https://opensource.org/licenses/BSD-3-Clause - -.. |wheel| image:: https://img.shields.io/pypi/wheel/mode-streaming.svg - :alt: Mode can be installed via wheel - :target: http://pypi.org/project/mode-streaming/ - -.. |pyversion| image:: https://img.shields.io/pypi/pyversions/mode-streaming.svg - :alt: Supported Python versions. - :target: http://pypi.org/project/mode-streaming/ - -.. |pyimp| image:: https://img.shields.io/pypi/implementation/mode-streaming.svg - :alt: Supported Python implementations. - :target: http://pypi.org/project/mode-streaming/ diff --git a/docs/userguide/index.rst b/docs/userguide/index.rst deleted file mode 100644 index 688a6200..00000000 --- a/docs/userguide/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -.. _guide: - -=============== - User Guide -=============== - -:Release: |version| -:Date: |today| - -.. toctree:: - :maxdepth: 1 - - services diff --git a/docs/userguide/services.rst b/docs/userguide/services.rst deleted file mode 100644 index 039680e9..00000000 --- a/docs/userguide/services.rst +++ /dev/null @@ -1,249 +0,0 @@ -.. _guide-services: - -================== - Services -================== - -.. module:: mode - :noindex: - -.. currentmodule:: mode - -.. contents:: - :local: - :depth: 1 - -Basics -====== - -The Service class manages the services and background tasks started -by the async program, so that we can implement graceful shutdown -and also helps us visualize the relationships between -services in a dependency graph. - -Anything that can be started/stopped and restarted -should probably be a subclass of the :class:`Service` class. - -The Service API -=============== - -A service can be started, and it may start other services -and background tasks. Most actions in a service are asynchronous, so needs -to be executed from within an async function. - -This first section defines the public service API, as if used by the user, -the next section will define the methods service authors write to define new -services. - -Methods -------- - -.. class:: Service - :noindex: - - .. automethod:: start - :noindex: - - .. automethod:: maybe_start - :noindex: - - .. automethod:: stop - :noindex: - - .. automethod:: restart - :noindex: - - .. automethod:: wait_until_stopped - :noindex: - - .. automethod:: set_shutdown - :noindex: - -Attributes ----------- - -.. class:: Service - :noindex: - - .. autoattribute:: started - :noindex: - - .. autoattribute:: label - :noindex: - - .. autoattribute:: shortlabel - :noindex: - - .. autoattribute:: beacon - :noindex: - -Defining new services -===================== - -Adding child services ---------------------- - -Child services can be added in three ways, - -1) Using ``add_dependency()`` in ``__post_init__``: - - .. sourcecode:: python - - class MyService(Service): - - def __post_init__(self) -> None: - self.add_dependency(OtherService()) - -2) Using ``add_dependency()`` in ``on_start``: - - .. sourcecode:: python - - class MyService(Service): - - async def on_start(self) -> None: - self.add_dependency(OtherService()) - -3) Using ``on_init_dependencies()`` - - This is is a method that if customized should return an iterable - of service instances: - - .. sourcecode:: python - - from typing import Iterable - from mode import Service, ServiceT - - class MyService(Service): - - def on_init_dependencies(self) -> Iterable[ServiceT]: - return [ServiceA(), ServiceB()] - -Ordering --------- - -Knowing exactly what is called, when it's called and in what order -is important, and this table will help you understand that: - -Order at start (``await Service.start()``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -1. The ``on_first_start`` callback is called. -2. Service logs: ``"[Service] Starting..."``. -3. ``on_start`` callback is called. -4. All ``@Service.task`` background tasks are started (in definition order). -5. All child services added by ``add_dependency()``, or - ``on_init_dependencies())`` are started. -6. Service logs: ``"[Service] Started"``. -7. The ``on_started`` callback is called. - -Order when stopping (``await Service.stop()``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -1. Service logs; ``"[Service] Stopping..."``. -2. The ``on_stop()`` callback is called. -3. All child services are stopped, in reverse order. -4. All asyncio futures added by ``add_future()`` are cancelled - in reverse order. -5. Service logs: ``"[Service] Stopped"``. -6. If ``Service.wait_for_shutdown = True``, it will wait for the - ``Service.set_shutdown()`` signal to be called. -7. All futures started by ``add_future()`` will be gathered (awaited). -8. The ``on_shutdown()`` callback is called. -9. The service logs: ``"[Service] Shutdown complete!"``. - -Order when restarting (``await Service.restart()``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -1. The service is stopped (``await service.stop()``). -2. The ``__post_init__()`` callback is called again. -3. The service is started (``await service.start()``). - -Callbacks ---------- - -.. class:: Service - :noindex: - - .. automethod:: on_start - :noindex: - - .. automethod:: on_first_start - :noindex: - - .. automethod:: on_started - :noindex: - - .. automethod:: on_stop - :noindex: - - .. automethod:: on_shutdown - :noindex: - - .. automethod:: on_restart - :noindex: - -Handling Errors ---------------- - -.. class:: Service - :noindex: - - .. automethod:: crash - :noindex: - -Utilities ---------- - -.. class:: Service - :noindex: - - .. automethod:: sleep - :noindex: - - .. automethod:: wait - :noindex: - -Logging -------- - -Your service may add logging to notify the user what is going on, and the -Service class includes some shortcuts to include the service name etc. in -logs. - -The ``self.log`` delegate contains shortcuts for logging: - -.. sourcecode:: python - - # examples/logging.py - - from mode import Service - - - class MyService(Service): - - async def on_start(self) -> None: - self.log.debug('This is a debug message') - self.log.info('This is a info message') - self.log.warn('This is a warning message') - self.log.error('This is a error message') - self.log.exception('This is a error message with traceback') - self.log.critical('This is a critical message') - - self.log.debug('I can also include templates: %r %d %s', - [1, 2, 3], 303, 'string') - -The logs will be emitted by a logger with the same name as the module the -Service class is defined in. It's similar to this setup, that you can do -if you want to manually define the logger used by the service: - -.. sourcecode:: python - - # examples/manual_service_logger.py - - from mode import Service, get_logger - - logger = get_logger(__name__) - - - class MyService(Service): - logger = logger diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..0d14e403 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,98 @@ +site_name: Mode +site_description: avro, kafka, client, faust, schema + +theme: + name: 'material' + palette: + primary: black +# - scheme: default +# primary: blue grey +# accent: indigo +# toggle: +# icon: material/lightbulb +# name: Switch to dark mode +# - scheme: slate +# primary: blue grey +# accent: indigo +# toggle: +# icon: material/lightbulb-outline +# name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - search.suggest + - search.highlight + - content.tabs.link + - content.code.annotate + +repo_name: faust-streaming/mode +repo_url: https://github.com/faust-streaming/mode + +nav: + - Introduction: 'index.md' + - References: + - 'Mode': + - mode: references/mode.md + - mode.debug: references/mode.debug.md + - mode.exceptions: references/mode.exceptions.md + - mode.locals: references/mode.locals.md + - mode.proxy: references/mode.proxy.md + - mode.services: references/mode.services.md + - mode.signals: references/mode.signals.md + - mode.supervisors: references/mode.supervisors.md + - mode.threads: references/mode.threads.md + - mode.timers: references/mode.timers.md + - mode.worker: references/mode.worker.md + - 'Typehints': + - mode.types: references/mode.types.md + - mode.types.services: references/mode.types.services.md + - mode.types.signals: references/mode.types.signals.md + - mode.types.supervisors: references/mode.types.supervisors.md + - 'Event Loops': + - mode.loop: references/mode.loop.md + - mode.loop.eventlet: references/mode.loop.eventlet.md + - mode.loop.gevent: references/mode.loop.gevent.md + - mode.loop.uvloop: references/mode.loop.uvloop.md + - 'Utils': + - mode.utils.aiter: references/mode.utils.aiter.md + - mode.utils.collections: references/mode.utils.collections.md + - mode.utils.compat: references/mode.utils.compat.md + - mode.utils.contexts: references/mode.utils.contexts.md + - mode.utils.cron: references/mode.utils.cron.md + - mode.utils.futures: references/mode.utils.futures.md + - mode.utils.graphs: references/mode.utils.graphs.md + - mode.utils.imports: references/mode.utils.imports.md + - mode.utils.locals: references/mode.utils.locals.md + - mode.utils.locks: references/mode.utils.locks.md + - mode.utils.logging: references/mode.utils.logging.md + - mode.utils.loops: references/mode.utils.loops.md + - mode.utils.mocks: references/mode.utils.mocks.md + - mode.utils.objects: references/mode.utils.objects.md + - mode.utils.queues: references/mode.utils.queues.md + - mode.utils.text: references/mode.utils.text.md + - mode.utils.times: references/mode.utils.times.md + - mode.utils.tracebacks: references/mode.utils.tracebacks.md + - mode.utils.trees: references/mode.utils.trees.md + - mode.utils.types.graphs: references/mode.utils.types.graphs.md + - mode.utils.types.trees: references/mode.utils.types.trees.md +markdown_extensions: + - pymdownx.highlight + - pymdownx.inlinehilite + - pymdownx.superfences + - pymdownx.snippets + - pymdownx.critic + - pymdownx.caret + - pymdownx.keys + - pymdownx.mark + - pymdownx.tilde + - pymdownx.details + - tables + - attr_list + - md_in_html + - admonition + - codehilite + +plugins: + - search + - autorefs + - mkdocstrings diff --git a/mode/__init__.py b/mode/__init__.py index 44afd6ea..13631e48 100644 --- a/mode/__init__.py +++ b/mode/__init__.py @@ -112,10 +112,6 @@ def __dir__(self) -> Sequence[str]: "VERSION", "version_info", "__package__", - "__author__", - "__contact__", - "__homepage__", - "__docformat__", ) ) return result diff --git a/mode/locals.py b/mode/locals.py index 440092ba..07db83a8 100644 --- a/mode/locals.py +++ b/mode/locals.py @@ -12,17 +12,16 @@ For example to create a proxy to a class that both implements the mutable mapping interface and is an async context manager: -.. sourcecode:: python +```python +def create_real(): + print('CREATING X') + return X() +class XProxy(MutableMappingRole, AsyncContextManagerRole): + ... - def create_real(): - print('CREATING X') - return X() - - class XProxy(MutableMappingRole, AsyncContextManagerRole): - ... - - x = XProxy(create_real) +x = XProxy(create_real) +``` Evaluation ========== @@ -31,46 +30,46 @@ class XProxy(MutableMappingRole, AsyncContextManagerRole): every time it is needed, so in the example above a new X will be created every time you access the underlying object: -.. sourcecode:: pycon +```sh +>>> x['foo'] = 'value' +CREATING X - >>> x['foo'] = 'value' - CREATING X +>>> x['foo'] +CREATING X +'value' - >>> x['foo'] - CREATING X - 'value' +>>> X['foo'] +CREATING X +'value' - >>> X['foo'] - CREATING X - 'value' - - >>> # evaluates twice, once for async with then for __getitem__: - >>> async with x: - ... x['foo'] - CREATING X - CREATING X - 'value' +>>> # evaluates twice, once for async with then for __getitem__: +>>> async with x: +... x['foo'] +CREATING X +CREATING X +'value' +``` If you want the creation of the object to be lazy (created when first needed), you can pass the `cache=True` argument to :class:`Proxy`: -.. sourcecode:: pycon - - >>> x = XProxy(create_real, cache=True) +```sh +>>> x = XProxy(create_real, cache=True) - >>> # Now only evaluates the first time it is needed. - >>> x['foo'] = 'value' - CREATING X +>>> # Now only evaluates the first time it is needed. +>>> x['foo'] = 'value' +CREATING X - >>> x['foo'] - 'value' +>>> x['foo'] +'value' - >>> X['foo'] - 'value' +>>> X['foo'] +'value' - >>> async with x: - ... x['foo'] - 'value' +>>> async with x: +... x['foo'] +'value' +``` """ import sys diff --git a/mode/loop/__init__.py b/mode/loop/__init__.py index fe6ae4fe..4ebec3ef 100644 --- a/mode/loop/__init__.py +++ b/mode/loop/__init__.py @@ -8,44 +8,54 @@ aio **default** Normal :mod:`asyncio` event loop policy. -eventlet - Use :pypi:`eventlet` as the event loop. +### eventlet - This uses :pypi:`aioeventlet` and will apply the - :pypi:`eventlet` monkey-patches. +Use :pypi:`eventlet` as the event loop. - To enable execute the following as the first thing that happens - when your program starts (e.g. add it as the top import of your - entrypoint module):: +This uses :pypi:`aioeventlet` and will apply the +:pypi:`eventlet` monkey-patches. - >>> import mode.loop - >>> mode.loop.use('eventlet') +To enable execute the following as the first thing that happens +when your program starts (e.g. add it as the top import of your +entrypoint module): -gevent - Use :pypi:`gevent` as the event loop. +```python +import mode.loop +mode.loop.use('eventlet') +``` - This uses :pypi:`aiogevent` (+modifications) and will apply the - :pypi:`gevent` monkey-patches. +### gevent - This choice enables you to run blocking Python code as if they - have invisible `async/await` syntax around it (NOTE: C extensions are - not usually gevent compatible). +Use :pypi:`gevent` as the event loop. - To enable execute the following as the first thing that happens - when your program starts (e.g. add it as the top import of your - entrypoint module):: +This uses :pypi:`aiogevent` (+modifications) and will apply the +:pypi:`gevent` monkey-patches. - >>> import mode.loop - >>> mode.loop.use('gevent') -uvloop - Event loop using :pypi:`uvloop`. +This choice enables you to run blocking Python code as if they +have invisible `async/await` syntax around it (NOTE: C extensions are +not usually gevent compatible). - To enable execute the following as the first thing that happens - when your program starts (e.g. add it as the top import of your - entrypoint module):: +To enable execute the following as the first thing that happens +when your program starts (e.g. add it as the top import of your +entrypoint module): - >>> import mode.loop - >>> mode.loop.use('uvloop') +```python +import mode.loop +mode.loop.use('gevent') +``` + +### uvloop + +Event loop using :pypi:`uvloop`. + +To enable execute the following as the first thing that happens +when your program starts (e.g. add it as the top import of your +entrypoint module): + +```python +import mode.loop +mode.loop.use('uvloop') +``` """ import importlib diff --git a/mode/proxy.py b/mode/proxy.py index f6c3cc8f..88d97840 100644 --- a/mode/proxy.py +++ b/mode/proxy.py @@ -17,11 +17,14 @@ class ServiceProxy(ServiceBase): """A service proxy delegates ServiceT methods to a composite service. Example: - >>> class MyServiceProxy(ServiceProxy): - ... - ... @cached_property - ... def _service(self) -> ServiceT: - ... return ActualService() + + ```python + class MyServiceProxy(ServiceProxy): + + @cached_property + def _service(self) -> ServiceT: + return ActualService() + ``` Notes: This is used by Faust, and probably useful elsewhere! diff --git a/mode/services.py b/mode/services.py index 33cae6f9..c2fdaf2a 100644 --- a/mode/services.py +++ b/mode/services.py @@ -157,32 +157,36 @@ class Diag(DiagT): This can be used to track what your service is doing. For example if your service is a Kafka consumer with a background thread that commits the offset every 30 seconds, you may want to - see when this happens:: + see when this happens: - DIAG_COMMITTING = 'committing' + ```python + DIAG_COMMITTING = 'committing' - class Consumer(Service): + class Consumer(Service): - @Service.task - async def _background_commit(self) -> None: - while not self.should_stop: - await self.sleep(30.0) - self.diag.set_flag(DIAG_COMMITTING) - try: - await self._consumer.commit() - finally: - self.diag.unset_flag(DIAG_COMMITTING) + @Service.task + async def _background_commit(self) -> None: + while not self.should_stop: + await self.sleep(30.0) + self.diag.set_flag(DIAG_COMMITTING) + try: + await self._consumer.commit() + finally: + self.diag.unset_flag(DIAG_COMMITTING) + ``` The above code is setting the flag manually, but you can also use - a decorator to accomplish the same thing:: + a decorator to accomplish the same thing: - @Service.timer(30.0) - async def _background_commit(self) -> None: - await self.commit() + ```python + @Service.timer(30.0) + async def _background_commit(self) -> None: + await self.commit() - @Service.transitions_with(DIAG_COMMITTING) - async def commit(self) -> None: - await self._consumer.commit() + @Service.transitions_with(DIAG_COMMITTING) + async def commit(self) -> None: + await self._consumer.commit() + ``` """ def __init__(self, service: ServiceT) -> None: @@ -202,15 +206,17 @@ class ServiceTask: """A background task. You don't have to use this class directly, instead - use the ``@Service.task`` decorator:: + use the `@Service.task` decorator: - class MyService(Service): + ```python + class MyService(Service): - @Service.task - def _background_task(self): - while not self.should_stop: - print('Hello') - await self.sleep(1.0) + @Service.task + def _background_task(self): + while not self.should_stop: + print('Hello') + await self.sleep(1.0) + ``` """ def __init__(self, fun: Callable[..., Awaitable]) -> None: @@ -226,95 +232,95 @@ def __repr__(self) -> str: class ServiceCallbacks: """Service callback interface. - When calling ``await service.start()`` this happens: - - .. code-block:: text - - +--------------------+ - | INIT (not started) | - +--------------------+ - V - .-----------------------. - / await service.start() | - `-----------------------' - V - +--------------------+ - | on_first_start | - +--------------------+ - V - +--------------------+ - | on_start | - +--------------------+ - V - +--------------------+ - | on_started | - +--------------------+ - - When stopping and ``wait_for_shutdown`` is unset, this happens: - - .. code-block:: text - - .-----------------------. - / await service.stop() | - `-----------------------' - V - +--------------------+ - | on_stop | - +--------------------+ - V - +--------------------+ - | on_shutdown | - +--------------------+ - - When stopping and ``wait_for_shutdown`` is set, the stop operation + When calling `await service.start()` this happens: + + ```text + +--------------------+ + | INIT (not started) | + +--------------------+ + V + .-----------------------. + / await service.start() | + `-----------------------' + V + +--------------------+ + | on_first_start | + +--------------------+ + V + +--------------------+ + | on_start | + +--------------------+ + V + +--------------------+ + | on_started | + +--------------------+ + ``` + + When stopping and `wait_for_shutdown` is unset, this happens: + + ```text + .-----------------------. + / await service.stop() | + `-----------------------' + V + +--------------------+ + | on_stop | + +--------------------+ + V + +--------------------+ + | on_shutdown | + +--------------------+ + ``` + + When stopping and `wait_for_shutdown` is set, the stop operation will wait for something to set the shutdown flag ``self.set_shutdown()``: - .. code-block:: text - - .-----------------------. - / await service.stop() | - `-----------------------' - V - +--------------------+ - | on_stop | - +--------------------+ - V - .-------------------------. - / service.set_shutdown() | - `-------------------------' - V - +--------------------+ - | on_shutdown | - +--------------------+ + ```text + .-----------------------. + / await service.stop() | + `-----------------------' + V + +--------------------+ + | on_stop | + +--------------------+ + V + .-------------------------. + / service.set_shutdown() | + `-------------------------' + V + +--------------------+ + | on_shutdown | + +--------------------+ + ``` When restarting the order is as follows (assuming - ``wait_for_shutdown`` unset): - - .. code-block:: text - - .-------------------------. - / await service.restart() | - `-------------------------' - V - +--------------------+ - | on_stop | - +--------------------+ - V - +--------------------+ - | on_shutdown | - +--------------------+ - V - +--------------------+ - | on_restart | - +--------------------+ - V - +--------------------+ - | on_start | - +--------------------+ - V - +--------------------+ - | on_started | - +--------------------+ + `wait_for_shutdown` unset): + + ```text + .-------------------------. + / await service.restart() | + `-------------------------' + V + +--------------------+ + | on_stop | + +--------------------+ + V + +--------------------+ + | on_shutdown | + +--------------------+ + V + +--------------------+ + | on_restart | + +--------------------+ + V + +--------------------+ + | on_start | + +--------------------+ + V + +--------------------+ + | on_started | + +--------------------+ + ``` """ async def on_first_start(self) -> None: @@ -412,13 +418,16 @@ def task(cls, fun: Callable[[Any], Awaitable[None]]) -> ServiceTask: """Decorate function to be used as background task. Example: - >>> class S(Service): - ... - ... @Service.task - ... async def background_task(self): - ... while not self.should_stop: - ... await self.sleep(1.0) - ... print('Waking up') + + ```python + class S(Service): + + @Service.task + async def background_task(self): + while not self.should_stop: + await self.sleep(1.0) + print('Waking up') + ``` """ return ServiceTask(fun) @@ -433,12 +442,13 @@ def timer( ) -> Callable[[Callable], ServiceTask]: """Background timer executing every ``n`` seconds. - Example: - >>> class S(Service): - ... - ... @Service.timer(1.0) - ... async def background_timer(self): - ... print('Waking up') + ```python + class S(Service): + + @Service.timer(1.0) + async def background_timer(self): + print('Waking up') + ``` """ _interval = want_seconds(interval) @@ -469,16 +479,19 @@ def crontab( """Background timer executing periodic task based on Crontab description. Example: - >>> class S(Service): - ... - ... @Service.crontab(cron_format='30 18 * * *', - timezone=pytz.timezone('US/Pacific')) - ... async def every_6_30_pm_pacific(self): - ... print('IT IS 6:30pm') - ... - ... @Service.crontab(cron_format='30 18 * * *') - ... async def every_6_30_pm(self): - ... print('6:30pm UTC') + + ```python + class S(Service): + + @Service.crontab(cron_format='30 18 * * *', + timezone=pytz.timezone('US/Pacific')) + async def every_6_30_pm_pacific(self): + print('IT IS 6:30pm') + + @Service.crontab(cron_format='30 18 * * *') + async def every_6_30_pm(self): + print('6:30pm UTC') + ``` """ def _decorate( @@ -1047,13 +1060,13 @@ async def itertimer( clock: ClockArg = perf_counter, name: str = "", ) -> AsyncIterator[float]: - """Sleep ``interval`` seconds for every iteration. + """Sleep `interval` seconds for every iteration. This is an async iterator that takes advantage of :func:`~mode.timers.Timer` to monitor drift and timer - oerlap. + overlap. - Uses ``Service.sleep`` so exits fast when the service is + Uses `Service.sleep` so exits fast when the service is stopped. Note: @@ -1061,9 +1074,12 @@ async def itertimer( from first iteration. Examples: - >>> async for sleep_time in self.itertimer(1.0): - ... print('another second passed, just woke up...') - ... await perform_some_http_request() + + ```python + async for sleep_time in self.itertimer(1.0): + print('another second passed, just woke up...') + await perform_some_http_request() + ``` """ sleepfun = sleep or self.sleep if self.should_stop: diff --git a/mode/signals.py b/mode/signals.py index b3eba7bc..c95bbafc 100644 --- a/mode/signals.py +++ b/mode/signals.py @@ -86,14 +86,18 @@ def _with_default_sender(self, sender: Any = None) -> BaseSignalT: ) def __set_name__(self, owner: Type, name: str) -> None: - # If signal is an attribute of a class, we use __set_name__ - # to show the location of the signal in __repr__. - # E.g.:: - # >>> class X(Service): - # ... starting = Signal() - # - # >>> X.starting - # + """If signal is an attribute of a class, we use __set_name__ to show the location of the signal in __repr__. + + Examples: + + ```python + class X(Service): + starting = Signal() + + >>> X.starting + + ``` + """ if not self.name: self.name = name self.owner = owner diff --git a/mode/utils/aiter.py b/mode/utils/aiter.py index c09fc5ba..b285f143 100644 --- a/mode/utils/aiter.py +++ b/mode/utils/aiter.py @@ -162,15 +162,18 @@ async def chunks(it: AsyncIterable[T], n: int) -> AsyncIterable[List[T]]: """Split an async iterator into chunks with `n` elements each. Example: - # n == 2 - >>> x = chunks(arange(10), 2) - >>> [item async for item in x] - [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10]] - - # n == 3 - >>> x = chunks(arange(10)), 3) - >>> [item async for item in x] - [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]] + + ```sh + # n == 2 + >>> x = chunks(arange(10), 2) + >>> [item async for item in x] + [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10]] + + # n == 3 + >>> x = chunks(arange(10)), 3) + >>> [item async for item in x] + [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]] + ``` """ ait = aiter(it) async for item in ait: diff --git a/mode/utils/collections.py b/mode/utils/collections.py index 87939dcc..992f60d0 100644 --- a/mode/utils/collections.py +++ b/mode/utils/collections.py @@ -116,10 +116,12 @@ def replace(self, item: T) -> T: Note that the value returned may be larger than item! That constrains reasonable uses of this routine unless written as - part of a conditional replacement:: + part of a conditional replacement: - if item > heap[0]: - item = heap.replace(item) + ```python + if item > heap[0]: + item = heap.replace(item) + ``` """ return heapreplace(self.data, item) diff --git a/mode/utils/futures.py b/mode/utils/futures.py index 2e89b900..39c6bfba 100644 --- a/mode/utils/futures.py +++ b/mode/utils/futures.py @@ -80,16 +80,16 @@ class stampede: Examples: Here's an example coroutine method connecting a network client: - .. sourcecode:: python + ```python + class Client: - class Client: + @stampede + async def maybe_connect(self): + await self._connect() - @stampede - async def maybe_connect(self): - await self._connect() - - async def _connect(self): - return Connection() + async def _connect(self): + return Connection() + ``` In the above example, if multiple coroutines call ``maybe_connect`` at the same time, then only one of them will actually perform the diff --git a/mode/utils/imports.py b/mode/utils/imports.py index 61b18225..f03a54f4 100644 --- a/mode/utils/imports.py +++ b/mode/utils/imports.py @@ -66,21 +66,22 @@ class FactoryMapping(FastUserDict, Generic[_T]): qualified Python attribute path, and also supporting the use of these in URLs. - Example: - >>> # Specifying the type enables mypy to know that - >>> # this factory returns Driver subclasses. - >>> drivers: FactoryMapping[Type[Driver]] - >>> drivers = FactoryMapping({ - ... 'rabbitmq': 'my.drivers.rabbitmq:Driver', - ... 'kafka': 'my.drivers.kafka:Driver', - ... 'redis': 'my.drivers.redis:Driver', - ... }) - - >>> drivers.by_url('rabbitmq://localhost:9090') - - - >>> drivers.by_name('redis') - + ```sh + >>> # Specifying the type enables mypy to know that + >>> # this factory returns Driver subclasses. + >>> drivers: FactoryMapping[Type[Driver]] + >>> drivers = FactoryMapping({ + ... 'rabbitmq': 'my.drivers.rabbitmq:Driver', + ... 'kafka': 'my.drivers.kafka:Driver', + ... 'redis': 'my.drivers.redis:Driver', + ... }) + + >>> drivers.by_url('rabbitmq://localhost:9090') + + + >>> drivers.by_name('redis') + + ``` """ aliases: MutableMapping[str, str] @@ -171,17 +172,20 @@ def parse_symbol( no ``package`` argument is specified. Examples: - >>> parse_symbol('mode.services') - ParsedSymbol(module_name='mode.services', attribute_name=None) - >>> parse_symbol('.services', package='mode') - ParsedSymbol(module_name='.services', attribute_name=None) + ```sh + >>> parse_symbol('mode.services') + ParsedSymbol(module_name='mode.services', attribute_name=None) + + >>> parse_symbol('.services', package='mode') + ParsedSymbol(module_name='.services', attribute_name=None) - >>> parse_symbol('mode.services.Service') - ParsedSymbol(module_name='mode.services', attribute_name='Service') + >>> parse_symbol('mode.services.Service') + ParsedSymbol(module_name='mode.services', attribute_name='Service') - >>> parse_symbol('mode.services:Service') - ParsedSymbol(module_name='mode.services', attribute_name='Service') + >>> parse_symbol('mode.services:Service') + ParsedSymbol(module_name='mode.services', attribute_name='Service') + ``` """ module_name: Optional[str] attribute_name: Optional[str] @@ -228,23 +232,29 @@ def symbol_by_name( ) -> _T: """Get symbol by qualified name. - The name should be the full dot-separated path to the class:: + The name should be the full dot-separated path to the class: modulename.ClassName - Example:: + Example: - mazecache.backends.redis.RedisBackend - ^- class name + ```python + mazecache.backends.redis.RedisBackend + ^- class name + ``` - or using ':' to separate module and symbol:: + or using ':' to separate module and symbol: - mazecache.backends.redis:RedisBackend + ```python + mazecache.backends.redis:RedisBackend + ``` If `aliases` is provided, a dict containing short name/long name mappings, the name is looked up in the aliases first. Examples: + + ```sh >>> symbol_by_name('mazecache.backends.redis:RedisBackend') @@ -256,6 +266,7 @@ def symbol_by_name( >>> from mazecache.backends.redis import RedisBackend >>> symbol_by_name(RedisBackend) is RedisBackend True + ``` """ # This code was copied from kombu.utils.symbol_by_name @@ -306,19 +317,23 @@ class RawEntrypointExtension(NamedTuple): def load_extension_classes(namespace: str) -> Iterable[EntrypointExtension]: """Yield extension classes for setuptools entrypoint namespace. - If an entrypoint is defined in ``setup.py``:: + If an entrypoint is defined in `setup.py`: - entry_points={ - 'faust.codecs': [ - 'msgpack = faust_msgpack:msgpack', - ], + ```python + entry_points={ + 'faust.codecs': [ + 'msgpack = faust_msgpack:msgpack', + ], + ``` Iterating over the 'faust.codecs' namespace will yield - the actual attributes specified in the path (``faust_msgpack:msgpack``):: + the actual attributes specified in the path (`faust_msgpack:msgpack`): - >>> from faust_msgpack import msgpack - >>> attrs = list(load_extension_classes('faust.codecs')) - assert msgpack in attrs + ```python + from faust_msgpack import msgpack + attrs = list(load_extension_classes('faust.codecs')) + assert msgpack in attrs + ``` """ for name, cls_name in load_extension_class_names(namespace): try: @@ -337,17 +352,21 @@ def load_extension_class_names( ) -> Iterable[RawEntrypointExtension]: """Get setuptools entrypoint extension class names. - If the entrypoint is defined in ``setup.py`` as:: + If the entrypoint is defined in `setup.py` as: - entry_points={ - 'faust.codecs': [ - 'msgpack = faust_msgpack:msgpack', - ], + ```python + entry_points={ + 'faust.codecs': [ + 'msgpack = faust_msgpack:msgpack', + ], + ``` - Iterating over the 'faust.codecs' namespace will yield the name:: + Iterating over the 'faust.codecs' namespace will yield the name: - >>> list(load_extension_class_names('faust.codecs')) - [('msgpack', 'faust_msgpack:msgpack')] + ```sh + >>> list(load_extension_class_names('faust.codecs')) + [('msgpack', 'faust_msgpack:msgpack')] + ``` """ try: from pkg_resources import iter_entry_points diff --git a/mode/utils/locals.py b/mode/utils/locals.py index 19214517..bc08f60d 100644 --- a/mode/utils/locals.py +++ b/mode/utils/locals.py @@ -5,9 +5,9 @@ - Supports typing: - .. sourcecode:: python - - request_stack: LocalStack[Request] = LocalStack() + ```python + request_stack: LocalStack[Request] = LocalStack() + ``` """ from contextlib import contextmanager diff --git a/mode/utils/logging.py b/mode/utils/logging.py index 668c9d89..64b287ca 100644 --- a/mode/utils/logging.py +++ b/mode/utils/logging.py @@ -170,15 +170,15 @@ class LogSeverityMixin(LogSeverityMixinBase): The class that mixes in this class must define the ``log`` method. Example: - >>> class Foo(LogSeverityMixin): - ... - ... logger = get_logger('foo') - ... - ... def log(self, - ... severity: int, - ... message: str, - ... *args: Any, **kwargs: Any) -> None: - ... return self.logger.log(severity, message, *args, **kwargs) + + ```python + class Foo(LogSeverityMixin): + + logger = get_logger('foo') + + def log(self, severity: int, message: str, *args: Any, **kwargs: Any) -> None: + return self.logger.log(severity, message, *args, **kwargs) + ``` """ def dev(self: HasLog, message: str, *args: Any, **kwargs: Any) -> None: @@ -241,28 +241,28 @@ class CompositeLogger(LogSeverityMixin): Service uses this to add logging methods: - .. sourcecode:: python - - class Service(ServiceT): + ```python + class Service(ServiceT): - log: CompositeLogger + log: CompositeLogger - def __init__(self): - self.log = CompositeLogger( - logger=self.logger, - formatter=self._format_log, - ) + def __init__(self): + self.log = CompositeLogger( + logger=self.logger, + formatter=self._format_log, + ) - def _format_log(self, severity: int, message: str, - *args: Any, **kwargs: Any) -> str: - return (f'[^{"-" * (self.beacon.depth - 1)}' - f'{self.shortlabel}]: {message}') + def _format_log(self, severity: int, message: str, + *args: Any, **kwargs: Any) -> str: + return (f'[^{"-" * (self.beacon.depth - 1)}' + f'{self.shortlabel}]: {message}') + ``` This means those defining a service may also use it to log: - .. sourcecode:: pycon - - >>> service.log.info('Something happened') + ```sh + >>> service.log.info('Something happened') + ``` and when logging additional information about the service is automatically included. @@ -615,15 +615,16 @@ class flight_recorder(ContextManager, LogSeverityMixin): This is a logging utility to log stuff only when something times out. - For example if you have a background thread that is sometimes - hanging:: + For example if you have a background thread that is sometimes hanging: - class RedisCache(mode.Service): + ```python + class RedisCache(mode.Service): - @mode.timer(1.0) - def _background_refresh(self) -> None: - self._users = await self.redis_client.get(USER_KEY) - self._posts = await self.redis_client.get(POSTS_KEY) + @mode.timer(1.0) + def _background_refresh(self) -> None: + self._users = await self.redis_client.get(USER_KEY) + self._posts = await self.redis_client.get(POSTS_KEY) + ``` You want to figure out on what line this is hanging, but logging all the time will provide way too much output, and will even change @@ -632,44 +633,44 @@ def _background_refresh(self) -> None: Use the flight recorder to save the logs and only log when it times out: - .. sourcecode:: python - - logger = mode.get_logger(__name__) + ```python + logger = mode.get_logger(__name__) - class RedisCache(mode.Service): + class RedisCache(mode.Service): - @mode.timer(1.0) - def _background_refresh(self) -> None: - with mode.flight_recorder(logger, timeout=10.0) as on_timeout: - on_timeout.info(f'+redis_client.get({USER_KEY!r})') - await self.redis_client.get(USER_KEY) - on_timeout.info(f'-redis_client.get({USER_KEY!r})') + @mode.timer(1.0) + def _background_refresh(self) -> None: + with mode.flight_recorder(logger, timeout=10.0) as on_timeout: + on_timeout.info(f'+redis_client.get({USER_KEY!r})') + await self.redis_client.get(USER_KEY) + on_timeout.info(f'-redis_client.get({USER_KEY!r})') - on_timeout.info(f'+redis_client.get({POSTS_KEY!r})') - await self.redis_client.get(POSTS_KEY) - on_timeout.info(f'-redis_client.get({POSTS_KEY!r})') + on_timeout.info(f'+redis_client.get({POSTS_KEY!r})') + await self.redis_client.get(POSTS_KEY) + on_timeout.info(f'-redis_client.get({POSTS_KEY!r})') + ``` If the body of this :keyword:`with` statement completes before the timeout, the logs are forgotten about and never emitted -- if it takes more than ten seconds to complete, we will see these messages in the log: - .. sourcecode:: text - - [2018-04-19 09:43:55,877: WARNING]: Warning: Task timed out! - [2018-04-19 09:43:55,878: WARNING]: - Please make sure it is hanging before restarting. - [2018-04-19 09:43:55,878: INFO]: [Flight Recorder-1] - (started at Thu Apr 19 09:43:45 2018) Replaying logs... - [2018-04-19 09:43:55,878: INFO]: [Flight Recorder-1] - (Thu Apr 19 09:43:45 2018) +redis_client.get('user') - [2018-04-19 09:43:55,878: INFO]: [Flight Recorder-1] - (Thu Apr 19 09:43:49 2018) -redis_client.get('user') - [2018-04-19 09:43:55,878: INFO]: [Flight Recorder-1] - (Thu Apr 19 09:43:46 2018) +redis_client.get('posts') - [2018-04-19 09:43:55,878: INFO]: [Flight Recorder-1] -End of log- - - Now we know this ``redis_client.get`` call can take too long to complete, + ```log + [2018-04-19 09:43:55,877: WARNING]: Warning: Task timed out! + [2018-04-19 09:43:55,878: WARNING]: + Please make sure it is hanging before restarting. + [2018-04-19 09:43:55,878: INFO]: [Flight Recorder-1] + (started at Thu Apr 19 09:43:45 2018) Replaying logs... + [2018-04-19 09:43:55,878: INFO]: [Flight Recorder-1] + (Thu Apr 19 09:43:45 2018) +redis_client.get('user') + [2018-04-19 09:43:55,878: INFO]: [Flight Recorder-1] + (Thu Apr 19 09:43:49 2018) -redis_client.get('user') + [2018-04-19 09:43:55,878: INFO]: [Flight Recorder-1] + (Thu Apr 19 09:43:46 2018) +redis_client.get('posts') + [2018-04-19 09:43:55,878: INFO]: [Flight Recorder-1] -End of log- + ``` + + Now we know this `redis_client.get` call can take too long to complete, and should consider adding a timeout to it. """ diff --git a/mode/utils/mocks.py b/mode/utils/mocks.py index bcc06c7b..1ee8d4d4 100644 --- a/mode/utils/mocks.py +++ b/mode/utils/mocks.py @@ -15,10 +15,9 @@ class IN: """Class used to check for multiple alternatives. - .. code-block:: python - - assert foo.value IN(a, b) - + ```python + assert foo.value IN(a, b) + ``` """ def __init__(self, *alternatives: Any) -> None: @@ -70,18 +69,20 @@ def __getattr__(self, attr: str) -> Any: def mask_module(*modnames: str) -> Iterator: """Ban some modules from being importable inside the context. - For example:: + For example: - >>> with mask_module('sys'): - ... try: - ... import sys - ... except ImportError: - ... print('sys not found') - sys not found + ```sh + >>> with mask_module('sys'): + ... try: + ... import sys + ... except ImportError: + ... print('sys not found') + sys not found - >>> import sys # noqa - >>> sys.version - (2, 5, 2, 'final', 0) + >>> import sys # noqa + >>> sys.version + (2, 5, 2, 'final', 0) + ``` Taken from http://bitbucket.org/runeh/snippets/src/tip/missing_modules.py diff --git a/mode/utils/objects.py b/mode/utils/objects.py index d22ad02d..f659b56e 100644 --- a/mode/utils/objects.py +++ b/mode/utils/objects.py @@ -156,32 +156,36 @@ class KeywordReduce: Python objects are made pickleable through defining the ``__reduce__`` method, that returns a tuple of: - ``(restore_function, function_starargs)``:: + `(restore_function, function_starargs)`: - class X: + ```python + class X: - def __init__(self, arg1, kw1=None): - self.arg1 = arg1 - self.kw1 = kw1 + def __init__(self, arg1, kw1=None): + self.arg1 = arg1 + self.kw1 = kw1 - def __reduce__(self) -> Tuple[Callable, Tuple[Any, ...]]: - return type(self), (self.arg1, self.kw1) + def __reduce__(self) -> Tuple[Callable, Tuple[Any, ...]]: + return type(self), (self.arg1, self.kw1) + ``` This is *tedious* since this means you cannot accept ``**kwargs`` in the constructor, so what we do is define a ``__reduce_keywords__`` - argument that returns a dict instead:: + argument that returns a dict instead: - class X: + ```python + class X: - def __init__(self, arg1, kw1=None): - self.arg1 = arg1 - self.kw1 = kw1 + def __init__(self, arg1, kw1=None): + self.arg1 = arg1 + self.kw1 = kw1 - def __reduce_keywords__(self) -> Mapping[str, Any]: - return { - 'arg1': self.arg1, - 'kw1': self.kw1, - } + def __reduce_keywords__(self) -> Mapping[str, Any]: + return { + 'arg1': self.arg1, + 'kw1': self.kw1, + } + ``` """ def __reduce_keywords__(self) -> Mapping: @@ -258,7 +262,7 @@ def annotations( cls: Class to get field information from. stop: Base class to stop at (default is ``object``). invalid_types: Set of types that if encountered should raise - :exc:`InvalidAnnotation` (does not test for subclasses). + :exc:`InvalidAnnotation` (does not test for subclasses). alias_types: Mapping of original type to replacement type. skip_classvar: Skip attributes annotated with :class:`typing.ClassVar`. @@ -279,20 +283,21 @@ def annotations( invalid type is encountered. Examples: - .. sourcecode:: text - >>> class Point: - ... x: float - ... y: float + ```sh + >>> class Point: + ... x: float + ... y: float - >>> class 3DPoint(Point): - ... z: float = 0.0 + >>> class 3DPoint(Point): + ... z: float = 0.0 - >>> fields, defaults = annotations(3DPoint) - >>> fields - {'x': float, 'y': float, 'z': 'float'} - >>> defaults - {'z': 0.0} + >>> fields, defaults = annotations(3DPoint) + >>> fields + {'x': float, 'y': float, 'z': 'float'} + >>> defaults + {'z': 0.0} + ``` """ fields: Dict[str, Type] = {} defaults: Dict[str, Any] = {} @@ -359,7 +364,10 @@ def eval_type( """Convert (possible) string annotation to actual type. Examples: - >>> eval_type('List[int]') == typing.List[int] + + ```sh + >>> eval_type('List[int]') == typing.List[int] + ``` """ invalid_types = invalid_types or set() alias_types = alias_types or {} @@ -395,15 +403,18 @@ def iter_mro_reversed(cls: Type, stop: Type) -> Iterable[Type]: The last item produced will be the class itself (`cls`). Examples: - >>> class A: ... - >>> class B(A): ... - >>> class C(B): ... - >>> list(iter_mro_reverse(C, object)) - [A, B, C] + ```sh + >>> class A: ... + >>> class B(A): ... + >>> class C(B): ... + + >>> list(iter_mro_reverse(C, object)) + [A, B, C] - >>> list(iter_mro_reverse(C, A)) - [B, C] + >>> list(iter_mro_reverse(C, A)) + [B, C] + ``` Yields: Iterable[Type]: every class. @@ -488,17 +499,20 @@ def guess_polymorphic_type( ) -> Tuple[Type, Type]: """Try to find the polymorphic and concrete type of an abstract type. - Returns tuple of ``(polymorphic_type, concrete_type)``. + Returns tuple of `(polymorphic_type, concrete_type)`. Examples: - >>> guess_polymorphic_type(List[int]) - (list, int) - >>> guess_polymorphic_type(Optional[List[int]]) - (list, int) + ```sh + >>> guess_polymorphic_type(List[int]) + (list, int) - >>> guess_polymorphic_type(MutableMapping[int, str]) - (dict, str) + >>> guess_polymorphic_type(Optional[List[int]]) + (list, int) + + >>> guess_polymorphic_type(MutableMapping[int, str]) + (dict, str) + ``` """ args, typ = _remove_optional(typ, find_origin=True) if typ is not str and typ is not bytes: @@ -561,23 +575,24 @@ class cached_property(Generic[RT]): of the get function. Examples: - .. sourcecode:: python - - @cached_property - def connection(self): - return Connection() - - @connection.setter # Prepares stored value - def connection(self, value): - if value is None: - raise TypeError('Connection must be a connection') - return value - - @connection.deleter - def connection(self, value): - # Additional action to do at del(self.attr) - if value is not None: - print(f'Connection {value!r} deleted') + + ```python + @cached_property + def connection(self): + return Connection() + + @connection.setter # Prepares stored value + def connection(self, value): + if value is None: + raise TypeError('Connection must be a connection') + return value + + @connection.deleter + def connection(self, value): + # Additional action to do at del(self.attr) + if value is not None: + print(f'Connection {value!r} deleted') + ``` """ def __init__( diff --git a/mode/utils/queues.py b/mode/utils/queues.py index 6948d726..2f18aa74 100644 --- a/mode/utils/queues.py +++ b/mode/utils/queues.py @@ -29,31 +29,41 @@ class FlowControlEvent: The FlowControlEvent manages flow in one or many queue instances at the same time. - To flow control queues, first create the shared event:: + To flow control queues, first create the shared event: - >>> flow_control = FlowControlEvent() + ```sh + >>> flow_control = FlowControlEvent() + ``` - Then pass that shared event to the queues that should be managed by it:: + Then pass that shared event to the queues that should be managed by it: - >>> q1 = FlowControlQueue(maxsize=1, flow_control=flow_control) - >>> q2 = FlowControlQueue(flow_control=flow_control) + ```sh + >>> q1 = FlowControlQueue(maxsize=1, flow_control=flow_control) + >>> q2 = FlowControlQueue(flow_control=flow_control) + ``` If you want the contents of the queue to be cleared when flow is resumed, - then specify that by using the ``clear_on_resume`` flag:: + then specify that by using the `clear_on_resume` flag: - >>> q3 = FlowControlQueue(clear_on_resume=True, - ... flow_control=flow_control) + ```sh + >>> q3 = FlowControlQueue(clear_on_resume=True, + ... flow_control=flow_control) + ``` - To suspend production into queues, use ``flow_control.suspend``:: + To suspend production into queues, use `flow_control.suspend`: - >>> flow_control.suspend() + ```sh + >>> flow_control.suspend() + ``` While the queues are suspend, any producer attempting to send something to the queue will hang until flow is resumed. - To resume production into queues, use ``flow_control.resume``:: + To resume production into queues, use `flow_control.resume`: - >>> flow_control.resume() + ```sh + >>> flow_control.resume() + ``` Notes: In Faust queues are managed by the ``app.flow_control`` event. diff --git a/mode/utils/text.py b/mode/utils/text.py index d11df8ca..a109722a 100644 --- a/mode/utils/text.py +++ b/mode/utils/text.py @@ -121,11 +121,14 @@ def enumeration( sep: str = "\n", template: str = "{index}) {item}", ) -> str: - r"""Enumerate list of strings. + """Enumerate list of strings. Example: - >>> enumeration(['x', 'y', '...']) - "1) x\n2) y\n3) ..." + + ```sh + >>> enumeration(['x', 'y', '...']) + "1) x\n2) y\n3) ..." + ``` """ return sep.join( template.format(index=index, item=item) @@ -208,16 +211,18 @@ def _abbr_abrupt(s: str, max: int, suffix: str = "...") -> str: def abbr_fqdn(origin: str, name: str, *, prefix: str = "") -> str: """Abbreviate fully-qualified Python name, by removing origin. - ``app.origin`` is the package where the app is defined, - so if this is ``examples.simple``:: + `app.origin` is the package where the app is defined, + so if this is `examples.simple`: - >>> app.origin - 'examples.simple' - >>> abbr_fqdn(app.origin, 'examples.simple.Withdrawal', prefix='[...]') - '[...]Withdrawal' + ```sh + >>> app.origin + 'examples.simple' + >>> abbr_fqdn(app.origin, 'examples.simple.Withdrawal', prefix='[...]') + '[...]Withdrawal' - >>> abbr_fqdn(app.origin, 'examples.other.Foo', prefix='[...]') - 'examples.other.foo' + >>> abbr_fqdn(app.origin, 'examples.other.Foo', prefix='[...]') + 'examples.other.foo' + ``` :func:`shorten_fqdn` is similar, but will always shorten a too long name, abbr_fqdn will only remove the origin portion of the name. diff --git a/mode/utils/times.py b/mode/utils/times.py index f7fc1f48..b47ea855 100644 --- a/mode/utils/times.py +++ b/mode/utils/times.py @@ -74,38 +74,45 @@ class Bucket(AsyncContextManager): caller has to wait. If this returns :const:`False`, it's prudent to either sleep or raise - an exception:: + an exception: - if not bucket.pour(): - await asyncio.sleep(bucket.expected_time()) + ```python + if not bucket.pour(): + await asyncio.sleep(bucket.expected_time()) + ``` - If you want to consume multiple tokens in one go then specify the number:: + If you want to consume multiple tokens in one go then specify the number: - if not bucket.pour(10): - await asyncio.sleep(bucket.expected_time(10)) + ```python + if not bucket.pour(10): + await asyncio.sleep(bucket.expected_time(10)) + ``` This class can also be used as an async. context manager, but in that case - can only consume one tokens at a time:: + can only consume one tokens at a time: - async with bucket: - # do something + ```python + async with bucket: + # do something + ``` By default the async. context manager will suspend the current coroutine and sleep until as soon as the time that a token can be consumed. If you wish you can also raise an exception, instead of sleeping, by - providing the ``raises`` keyword argument:: + providing the `raises` keyword argument: - # hundred tokens in one second, and async with: raises TimeoutError + ```python + # hundred tokens in one second, and async with: raises TimeoutError - class MyError(Exception): - pass + class MyError(Exception): + pass - bucket = Bucket(100, over=1.0, raises=MyError) - - async with bucket: - # do something + bucket = Bucket(100, over=1.0, raises=MyError) + async with bucket: + # do something + ``` """ rate: float diff --git a/pyproject.toml b/pyproject.toml index 0ce72e94..07266c45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,7 +198,7 @@ ignore = [ "tests/**" = ["D", "S"] [tool.ruff.lint.isort] -known-first-party = ["mode", "t"] +known-first-party = ["mode", "tests"] split-on-trailing-comma = false [tool.ruff.lint.pydocstyle] From f28c5cb0427fb903bc653520adfe4839541c9c71 Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Thu, 7 Mar 2024 22:52:52 +0100 Subject: [PATCH 04/16] style(documentation): switch to mkdocs from sphinx --- Makefile | 40 +--- docs/creating-service.md | 102 ++++++++++ docs/index.md | 270 ++++++++------------------ docs/references/mode.loop.eventlet.md | 2 +- docs/references/mode.loop.gevent.md | 2 +- docs/references/mode.loop.uvloop.md | 2 +- docs/references/mode.md | 3 - mkdocs.yml | 36 ++-- mode/debug.py | 7 +- mode/locals.py | 60 +++--- mode/loop/__init__.py | 16 +- mode/loop/_gevent_loop.py | 2 +- mode/loop/eventlet.py | 2 +- mode/loop/gevent.py | 2 +- mode/loop/uvloop.py | 2 +- mode/services.py | 12 +- mode/supervisors.py | 2 +- mode/threads.py | 5 +- mode/timers.py | 6 +- mode/types/services.py | 4 +- mode/types/signals.py | 2 +- mode/types/supervisors.py | 2 +- mode/utils/aiter.py | 2 +- mode/utils/collections.py | 22 ++- mode/utils/compat.py | 2 +- mode/utils/futures.py | 4 +- mode/utils/imports.py | 29 ++- mode/utils/locals.py | 2 +- mode/utils/locks.py | 2 +- mode/utils/logging.py | 14 +- mode/utils/mocks.py | 2 +- mode/utils/objects.py | 10 +- mode/utils/queues.py | 10 +- mode/utils/text.py | 24 +-- mode/utils/times.py | 6 +- mode/utils/tracebacks.py | 4 +- mode/utils/trees.py | 2 +- mode/utils/types/graphs.py | 2 +- mode/utils/types/trees.py | 2 +- requirements-docs.txt | 11 +- 40 files changed, 359 insertions(+), 372 deletions(-) create mode 100644 docs/creating-service.md delete mode 100644 docs/references/mode.md diff --git a/Makefile b/Makefile index 15309713..58801857 100644 --- a/Makefile +++ b/Makefile @@ -8,18 +8,14 @@ TOX ?= tox NOSETESTS ?= nosetests ICONV ?= iconv MYPY ?= mypy -SPHINX2RST ?= sphinx2rst TESTDIR ?= t -SPHINX_DIR ?= docs/ -SPHINX_BUILDDIR ?= "${SPHINX_DIR}/_build" README ?= README.rst README_SRC ?= "docs/templates/readme.txt" CONTRIBUTING ?= CONTRIBUTING.rst CONTRIBUTING_SRC ?= "docs/contributing.rst" COC ?= CODE_OF_CONDUCT.rst COC_SRC ?= "docs/includes/code-of-conduct.txt" -SPHINX_HTMLDIR="${SPHINX_BUILDDIR}/html" DOCUMENTATION=Documentation all: help @@ -55,19 +51,15 @@ release: . PHONY: deps-default deps-default: - $(PIP) install -U -r requirements/default.txt - -. PHONY: deps-dist -deps-dist: - $(PIP) install -U -r requirements/dist.txt + $(PIP) install -U -e "." . PHONY: deps-docs deps-docs: - $(PIP) install -U -r requirements/docs.txt + $(PIP) install -U -r requirements-docs.txt . PHONY: deps-test deps-test: - $(PIP) install -U -r requirements/test.txt + $(PIP) install -U -r requirements-test.txt . PHONY: deps-extras deps-extras: @@ -80,12 +72,15 @@ develop: deps-default deps-dist deps-docs deps-test deps-extras . PHONY: Documentation Documentation: - (cd "$(SPHINX_DIR)"; $(MAKE) html) - mv "$(SPHINX_HTMLDIR)" $(DOCUMENTATION) + mkdocs build . PHONY: docs docs: Documentation +. PHONE: serve-docs +serve-docs: + mkdocs serve + clean-docs: -rm -rf "$(SPHINX_BUILDDIR)" @@ -94,34 +89,22 @@ ruff: lint: ruff apicheck readmecheck -apicheck: - (cd "$(SPHINX_DIR)"; $(MAKE) apicheck) - clean-readme: -rm -f $(README) readmecheck: $(ICONV) -f ascii -t ascii $(README) >/dev/null -$(README): - $(SPHINX2RST) "$(README_SRC)" --ascii > $@ - readme: clean-readme $(README) readmecheck clean-contrib: -rm -f "$(CONTRIBUTING)" -$(CONTRIBUTING): - $(SPHINX2RST) "$(CONTRIBUTING_SRC)" > $@ - contrib: clean-contrib $(CONTRIBUTING) clean-coc: -rm -f "$(COC)" -$(COC): - $(SPHINX2RST) "$(COC_SRC)" > $@ - coc: clean-coc $(COC) clean-pyc: @@ -161,9 +144,4 @@ typecheck: .PHONY: requirements requirements: $(PIP) install --upgrade pip;\ - for f in `ls requirements/` ; do $(PIP) install -r requirements/$$f ; done - -.PHONY: clean-requirements -clean-requirements: - pip freeze | xargs pip uninstall -y - $(MAKE) requirements + $(PIP) install -r requirements.txt diff --git a/docs/creating-service.md b/docs/creating-service.md new file mode 100644 index 00000000..1991f178 --- /dev/null +++ b/docs/creating-service.md @@ -0,0 +1,102 @@ + +# Creating a Service + +To define a service, simply subclass and fill in the methods +to do stuff as the service is started/stopped etc.: + +```python +class MyService(Service): + + async def on_start(self) -> None: + print('Im starting now') + + async def on_started(self) -> None: + print('Im ready') + + async def on_stop(self) -> None: + print('Im stopping now') +``` + +To start the service, call ``await service.start()``: + +```python +await service.start() +``` + +Or you can use ``mode.Worker`` (or a subclass of this) to start your +services-based asyncio program from the console: + +```python +if __name__ == '__main__': + import mode + worker = mode.Worker( + MyService(), + loglevel='INFO', + logfile=None, + daemon=False, + ) + worker.execute_from_commandline() +``` + +## It's a Graph! + +Services can start other services, coroutines, and background tasks. + +1) Starting other services using ``add_dependency``: + +```python + class MyService(Service): + + def __post_init__(self) -> None: + self.add_dependency(OtherService(loop=self.loop)) +``` + +1) Start a list of services using ``on_init_dependencies``: + +```python +class MyService(Service): + + def on_init_dependencies(self) -> None: + return [ + ServiceA(loop=self.loop), + ServiceB(loop=self.loop), + ServiceC(loop=self.loop), + ] +``` + +1) Start a future/coroutine (that will be waited on to complete on stop): + +```python + class MyService(Service): + + async def on_start(self) -> None: + self.add_future(self.my_coro()) + + async def my_coro(self) -> None: + print('Executing coroutine') +``` + +1) Start a background task: + +```python +class MyService(Service): + + @Service.task + async def _my_coro(self) -> None: + print('Executing coroutine') +``` + + +1) Start a background task that keeps running: + +```python +class MyService(Service): + + @Service.task + async def _my_coro(self) -> None: + while not self.should_stop: + # NOTE: self.sleep will wait for one second, or + # until service stopped/crashed. + await self.sleep(1.0) + print('Background thread waking up') +``` diff --git a/docs/index.md b/docs/index.md index f3bcb74f..cd84c730 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,232 +21,132 @@ restart and supervise. A service is just a class: ```python - class PageViewCache(Service): - redis: Redis = None +class PageViewCache(Service): + redis: Redis = None - async def on_start(self) -> None: - self.redis = connect_to_redis() + async def on_start(self) -> None: + self.redis = connect_to_redis() - async def update(self, url: str, n: int = 1) -> int: - return await self.redis.incr(url, n) + async def update(self, url: str, n: int = 1) -> int: + return await self.redis.incr(url, n) - async def get(self, url: str) -> int: - return await self.redis.get(url) + async def get(self, url: str) -> int: + return await self.redis.get(url) ``` - Services are started, stopped and restarted and have callbacks for those actions. It can start another service: ```python - class App(Service): - page_view_cache: PageViewCache = None +class App(Service): + page_view_cache: PageViewCache = None - async def on_start(self) -> None: - await self.add_runtime_dependency(self.page_view_cache) + async def on_start(self) -> None: + await self.add_runtime_dependency(self.page_view_cache) - @cached_property - def page_view_cache(self) -> PageViewCache: - return PageViewCache() + @cached_property + def page_view_cache(self) -> PageViewCache: + return PageViewCache() ``` It can include background tasks: ```python - class PageViewCache(Service): +class PageViewCache(Service): - @Service.timer(1.0) - async def _update_cache(self) -> None: - self.data = await cache.get('key') + @Service.timer(1.0) + async def _update_cache(self) -> None: + self.data = await cache.get('key') ``` Services that depends on other services actually form a graph that you can visualize. -Worker - Mode optionally provides a worker that you can use to start the program, - with support for logging, blocking detection, remote debugging and more. - - To start a worker add this to your program: - - if __name__ == '__main__': - from mode import Worker - Worker(Service(), loglevel="info").execute_from_commandline() - - Then execute your program to start the worker: - - ```sh - $ python examples/tutorial.py - [2018-03-27 15:47:12,159: INFO]: [^Worker]: Starting... - [2018-03-27 15:47:12,160: INFO]: [^-AppService]: Starting... - [2018-03-27 15:47:12,160: INFO]: [^--Websockets]: Starting... - STARTING WEBSOCKET SERVER - [2018-03-27 15:47:12,161: INFO]: [^--UserCache]: Starting... - [2018-03-27 15:47:12,161: INFO]: [^--Webserver]: Starting... - [2018-03-27 15:47:12,164: INFO]: [^--Webserver]: Serving on port 8000 - REMOVING EXPIRED USERS - REMOVING EXPIRED USERS - ``` - - To stop it hit :kbd:`Control-c`: - - ```sh - [2018-03-27 15:55:08,084: INFO]: [^Worker]: Stopping on signal received... - [2018-03-27 15:55:08,084: INFO]: [^Worker]: Stopping... - [2018-03-27 15:55:08,084: INFO]: [^-AppService]: Stopping... - [2018-03-27 15:55:08,084: INFO]: [^--UserCache]: Stopping... - REMOVING EXPIRED USERS - [2018-03-27 15:55:08,085: INFO]: [^Worker]: Gathering service tasks... - [2018-03-27 15:55:08,085: INFO]: [^--UserCache]: -Stopped! - [2018-03-27 15:55:08,085: INFO]: [^--Webserver]: Stopping... - [2018-03-27 15:55:08,085: INFO]: [^Worker]: Gathering all futures... - [2018-03-27 15:55:08,085: INFO]: [^--Webserver]: Closing server - [2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Waiting for server to close handle - [2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Shutting down web application - [2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Waiting for handler to shut down - [2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Cleanup - [2018-03-27 15:55:08,086: INFO]: [^--Webserver]: -Stopped! - [2018-03-27 15:55:08,086: INFO]: [^--Websockets]: Stopping... - [2018-03-27 15:55:08,086: INFO]: [^--Websockets]: -Stopped! - [2018-03-27 15:55:08,087: INFO]: [^-AppService]: -Stopped! - [2018-03-27 15:55:08,087: INFO]: [^Worker]: -Stopped! - ``` - -Beacons - The ``beacon`` object that we pass to services keeps track of the services - in a graph. - - They are not strictly required, but can be used to visualize a running - system, for example we can render it as a pretty graph. - - This requires you to have the `pydot` library and GraphViz - installed: - - ```sh - $ pip install pydot - ``` - - Let's change the app service class to dump the graph to an image - at startup: - - - ```python - class AppService(Service): - - async def on_start(self) -> None: - print('APP STARTING') - import pydot - import io - o = io.StringIO() - beacon = self.app.beacon.root or self.app.beacon - beacon.as_graph().to_dot(o) - graph, = pydot.graph_from_dot_data(o.getvalue()) - print('WRITING GRAPH TO image.png') - with open('image.png', 'wb') as fh: - fh.write(graph.create_png()) - ``` - - -## Creating a Service - -To define a service, simply subclass and fill in the methods -to do stuff as the service is started/stopped etc.: - -```python - class MyService(Service): - - async def on_start(self) -> None: - print('Im starting now') - - async def on_started(self) -> None: - print('Im ready') +### Worker - async def on_stop(self) -> None: - print('Im stopping now') -``` +Mode optionally provides a worker that you can use to start the program, +with support for logging, blocking detection, remote debugging and more. -To start the service, call ``await service.start()``: +To start a worker add this to your program: ```python - await service.start() +if __name__ == '__main__': + from mode import Worker + Worker(Service(), loglevel="info").execute_from_commandline() ``` -Or you can use ``mode.Worker`` (or a subclass of this) to start your -services-based asyncio program from the console: - -```python - if __name__ == '__main__': - import mode - worker = mode.Worker( - MyService(), - loglevel='INFO', - logfile=None, - daemon=False, - ) - worker.execute_from_commandline() -``` - -## It's a Graph! - -Services can start other services, coroutines, and background tasks. - -1) Starting other services using ``add_dependency``: - -```python - class MyService(Service): - - def __post_init__(self) -> None: - self.add_dependency(OtherService(loop=self.loop)) +Then execute your program to start the worker: + +```sh +$ python examples/tutorial.py +[2018-03-27 15:47:12,159: INFO]: [^Worker]: Starting... +[2018-03-27 15:47:12,160: INFO]: [^-AppService]: Starting... +[2018-03-27 15:47:12,160: INFO]: [^--Websockets]: Starting... +STARTING WEBSOCKET SERVER +[2018-03-27 15:47:12,161: INFO]: [^--UserCache]: Starting... +[2018-03-27 15:47:12,161: INFO]: [^--Webserver]: Starting... +[2018-03-27 15:47:12,164: INFO]: [^--Webserver]: Serving on port 8000 +REMOVING EXPIRED USERS +REMOVING EXPIRED USERS ``` -2) Start a list of services using ``on_init_dependencies``: - -```python - class MyService(Service): - - def on_init_dependencies(self) -> None: - return [ - ServiceA(loop=self.loop), - ServiceB(loop=self.loop), - ServiceC(loop=self.loop), - ] +To stop it hit `Control-c`: + +```sh +[2018-03-27 15:55:08,084: INFO]: [^Worker]: Stopping on signal received... +[2018-03-27 15:55:08,084: INFO]: [^Worker]: Stopping... +[2018-03-27 15:55:08,084: INFO]: [^-AppService]: Stopping... +[2018-03-27 15:55:08,084: INFO]: [^--UserCache]: Stopping... +REMOVING EXPIRED USERS +[2018-03-27 15:55:08,085: INFO]: [^Worker]: Gathering service tasks... +[2018-03-27 15:55:08,085: INFO]: [^--UserCache]: -Stopped! +[2018-03-27 15:55:08,085: INFO]: [^--Webserver]: Stopping... +[2018-03-27 15:55:08,085: INFO]: [^Worker]: Gathering all futures... +[2018-03-27 15:55:08,085: INFO]: [^--Webserver]: Closing server +[2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Waiting for server to close handle +[2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Shutting down web application +[2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Waiting for handler to shut down +[2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Cleanup +[2018-03-27 15:55:08,086: INFO]: [^--Webserver]: -Stopped! +[2018-03-27 15:55:08,086: INFO]: [^--Websockets]: Stopping... +[2018-03-27 15:55:08,086: INFO]: [^--Websockets]: -Stopped! +[2018-03-27 15:55:08,087: INFO]: [^-AppService]: -Stopped! +[2018-03-27 15:55:08,087: INFO]: [^Worker]: -Stopped! ``` -3) Start a future/coroutine (that will be waited on to complete on stop): +### Beacons -```python - class MyService(Service): - - async def on_start(self) -> None: - self.add_future(self.my_coro()) +The `beacon` object that we pass to services keeps track of the services +in a graph. - async def my_coro(self) -> None: - print('Executing coroutine') -``` +They are not strictly required, but can be used to visualize a running +system, for example we can render it as a pretty graph. -4) Start a background task: - -```python - class MyService(Service): +This requires you to have the `pydot` library and GraphViz +installed: - @Service.task - async def _my_coro(self) -> None: - print('Executing coroutine') +```sh +$ pip install pydot ``` +Let's change the app service class to dump the graph to an image +at startup: -5) Start a background task that keeps running: ```python - class MyService(Service): - - @Service.task - async def _my_coro(self) -> None: - while not self.should_stop: - # NOTE: self.sleep will wait for one second, or - # until service stopped/crashed. - await self.sleep(1.0) - print('Background thread waking up') +class AppService(Service): + + async def on_start(self) -> None: + print('APP STARTING') + import pydot + import io + o = io.StringIO() + beacon = self.app.beacon.root or self.app.beacon + beacon.as_graph().to_dot(o) + graph, = pydot.graph_from_dot_data(o.getvalue()) + print('WRITING GRAPH TO image.png') + with open('image.png', 'wb') as fh: + fh.write(graph.create_png()) ``` diff --git a/docs/references/mode.loop.eventlet.md b/docs/references/mode.loop.eventlet.md index 072f34a1..49887afa 100644 --- a/docs/references/mode.loop.eventlet.md +++ b/docs/references/mode.loop.eventlet.md @@ -5,4 +5,4 @@ !!! warning Importing this module directly will set the global event loop. - See :mod:`faust.loop` for more information. + See `faust.loop` for more information. diff --git a/docs/references/mode.loop.gevent.md b/docs/references/mode.loop.gevent.md index 8f0a229f..f23c4cb9 100644 --- a/docs/references/mode.loop.gevent.md +++ b/docs/references/mode.loop.gevent.md @@ -5,4 +5,4 @@ !!! warning Importing this module directly will set the global event loop. - See :mod:`faust.loop` for more information. + See `faust.loop` for more information. diff --git a/docs/references/mode.loop.uvloop.md b/docs/references/mode.loop.uvloop.md index c1f5044a..6cdf6e72 100644 --- a/docs/references/mode.loop.uvloop.md +++ b/docs/references/mode.loop.uvloop.md @@ -5,4 +5,4 @@ !!! warning Importing this module directly will set the global event loop. - See :mod:`faust.loop` for more information. + See `faust.loop` for more information. diff --git a/docs/references/mode.md b/docs/references/mode.md deleted file mode 100644 index 342308cf..00000000 --- a/docs/references/mode.md +++ /dev/null @@ -1,3 +0,0 @@ -# mode - -::: mode diff --git a/mkdocs.yml b/mkdocs.yml index 0d14e403..28c34726 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,19 +4,21 @@ site_description: avro, kafka, client, faust, schema theme: name: 'material' palette: - primary: black -# - scheme: default -# primary: blue grey -# accent: indigo -# toggle: -# icon: material/lightbulb -# name: Switch to dark mode -# - scheme: slate -# primary: blue grey -# accent: indigo -# toggle: -# icon: material/lightbulb-outline -# name: Switch to light mode + # Palette toggle for light mode + - scheme: default + primary: deep orange + accent: deep orange + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - scheme: slate + primary: deep orange + accent: deep orange + toggle: + icon: material/brightness-4 + name: Switch to light mode features: - navigation.tabs - navigation.sections @@ -29,15 +31,16 @@ repo_name: faust-streaming/mode repo_url: https://github.com/faust-streaming/mode nav: - - Introduction: 'index.md' + - Mode: + - Introduction: 'index.md' + - Creating a Service: 'creating-service.md' - References: - 'Mode': - - mode: references/mode.md + - mode.services: references/mode.services.md - mode.debug: references/mode.debug.md - mode.exceptions: references/mode.exceptions.md - mode.locals: references/mode.locals.md - mode.proxy: references/mode.proxy.md - - mode.services: references/mode.services.md - mode.signals: references/mode.signals.md - mode.supervisors: references/mode.supervisors.md - mode.threads: references/mode.threads.md @@ -86,6 +89,7 @@ markdown_extensions: - pymdownx.mark - pymdownx.tilde - pymdownx.details + - pymdownx.magiclink - tables - attr_list - md_in_html diff --git a/mode/debug.py b/mode/debug.py index 7334ca9c..21f4b0e6 100644 --- a/mode/debug.py +++ b/mode/debug.py @@ -41,8 +41,11 @@ class BlockingDetector(Service): """Service that detects blocking code using alarm/itimer. Examples: - blockdetect = BlockingDetector(timeout=10.0) - await blockdetect.start() + + ```python + blockdetect = BlockingDetector(timeout=10.0) + await blockdetect.start() + ``` Keyword Arguments: timeout (Seconds): number of seconds that the event loop can diff --git a/mode/locals.py b/mode/locals.py index 07db83a8..30fe87ad 100644 --- a/mode/locals.py +++ b/mode/locals.py @@ -26,7 +26,7 @@ class XProxy(MutableMappingRole, AsyncContextManagerRole): Evaluation ========== -By default the callable passed to :class:`Proxy` will be evaluated +By default the callable passed to `Proxy` will be evaluated every time it is needed, so in the example above a new X will be created every time you access the underlying object: @@ -51,7 +51,7 @@ class XProxy(MutableMappingRole, AsyncContextManagerRole): ``` If you want the creation of the object to be lazy (created -when first needed), you can pass the `cache=True` argument to :class:`Proxy`: +when first needed), you can pass the `cache=True` argument to `Proxy`: ```sh >>> x = XProxy(create_real, cache=True) @@ -397,7 +397,7 @@ def __reduce__(self) -> Tuple: class AwaitableRole(Awaitable[T]): - """Role/Mixin for :class:`typing.Awaitable` proxy methods.""" + """Role/Mixin for `typing.Awaitable` proxy methods.""" def _get_awaitable(self) -> Awaitable[T]: obj = self._get_current_object() # type: ignore @@ -408,11 +408,11 @@ def __await__(self) -> Generator[Any, None, T]: class AwaitableProxy(Proxy[T], AwaitableRole[T]): - """Proxy to :class:`typing.Awaitable` object.""" + """Proxy to `typing.Awaitable` object.""" class CoroutineRole(Coroutine[T_co, T_contra, V_co]): - """Role/Mixin for :class:`typing.Coroutine` proxy methods.""" + """Role/Mixin for `typing.Coroutine` proxy methods.""" def _get_coroutine(self) -> Coroutine[T_co, T_contra, V_co]: obj = self._get_current_object() # type: ignore @@ -439,11 +439,11 @@ def close(self) -> None: class CoroutineProxy( Proxy[Coroutine[T_co, T_contra, V_co]], CoroutineRole[T_co, T_contra, V_co] ): - """Proxy to :class:`typing.Coroutine` object.""" + """Proxy to `typing.Coroutine` object.""" class AsyncIterableRole(AsyncIterable[T_co]): - """Role/Mixin for :class:`typing.AsyncIterable` proxy methods.""" + """Role/Mixin for `typing.AsyncIterable` proxy methods.""" def _get_iterable(self) -> AsyncIterable[T_co]: obj = self._get_current_object() # type: ignore @@ -454,11 +454,11 @@ def __aiter__(self) -> AsyncIterator[T_co]: class AsyncIterableProxy(Proxy[AsyncIterable[T_co]], AsyncIterableRole[T_co]): - """Proxy to :class:`typing.AsyncIterable` object.""" + """Proxy to `typing.AsyncIterable` object.""" class AsyncIteratorRole(AsyncIterator[T_co]): - """Role/Mixin for :class:`typing.AsyncIterator` proxy methods.""" + """Role/Mixin for `typing.AsyncIterator` proxy methods.""" def _get_iterator(self) -> AsyncIterator[T_co]: obj = self._get_current_object() # type: ignore @@ -472,11 +472,11 @@ def __anext__(self) -> Awaitable[T_co]: class AsyncIteratorProxy(Proxy[AsyncIterator[T_co]], AsyncIteratorRole[T_co]): - """Proxy to :class:`typing.AsyncIterator` object.""" + """Proxy to `typing.AsyncIterator` object.""" class AsyncGeneratorRole(AsyncGenerator[T_co, T_contra]): - """Role/Mixin for :class:`typing.AsyncGenerator` proxy methods.""" + """Role/Mixin for `typing.AsyncGenerator` proxy methods.""" def _get_generator(self) -> AsyncGenerator[T_co, T_contra]: obj = self._get_current_object() # type: ignore @@ -506,11 +506,11 @@ def __aiter__(self) -> AsyncGenerator[T_co, T_contra]: class AsyncGeneratorProxy( Proxy[AsyncGenerator[T_co, T_contra]], AsyncGeneratorRole[T_co, T_contra] ): - """Proxy to :class:`typing.AsyncGenerator` object.""" + """Proxy to `typing.AsyncGenerator` object.""" class SequenceRole(Sequence[T_co]): - """Role/Mixin for :class:`typing.Sequence` proxy methods.""" + """Role/Mixin for `typing.Sequence` proxy methods.""" def _get_sequence(self) -> Sequence[T_co]: obj = self._get_current_object() # type: ignore @@ -545,11 +545,11 @@ def __len__(self) -> int: class SequenceProxy(Proxy[Sequence[T_co]], SequenceRole[T_co]): - """Proxy to :class:`typing.Sequence` object.""" + """Proxy to `typing.Sequence` object.""" class MutableSequenceRole(SequenceRole[T], MutableSequence[T]): - """Role/Mixin for :class:`typing.MutableSequence` proxy methods.""" + """Role/Mixin for `typing.MutableSequence` proxy methods.""" def _get_sequence(self) -> MutableSequence[T]: obj = self._get_current_object() # type: ignore @@ -598,11 +598,11 @@ def __iadd__(self, x: Iterable[T]) -> MutableSequence[T]: class MutableSequenceProxy( Proxy[MutableSequence[T_co]], MutableSequenceRole[T_co] ): - """Proxy to :class:`typing.MutableSequence` object.""" + """Proxy to `typing.MutableSequence` object.""" class SetRole(AbstractSet[T_co]): - """Role/Mixin for :class:`typing.AbstractSet` proxy methods.""" + """Role/Mixin for `typing.AbstractSet` proxy methods.""" def _get_set(self) -> AbstractSet[T_co]: obj = self._get_current_object() # type: ignore @@ -646,11 +646,11 @@ def __len__(self) -> int: class SetProxy(Proxy[AbstractSet[T_co]], SetRole[T_co]): - """Proxy to :class:`typing.AbstractSet` object.""" + """Proxy to `typing.AbstractSet` object.""" class MutableSetRole(SetRole[T], MutableSet[T]): - """Role/Mixin for :class:`typing.MutableSet` proxy methods.""" + """Role/Mixin for `typing.MutableSet` proxy methods.""" def _get_set(self) -> MutableSet[T]: obj = self._get_current_object() # type: ignore @@ -685,11 +685,11 @@ def __isub__(self, s: AbstractSet[Any]) -> MutableSet[T]: class MutableSetProxy(Proxy[MutableSet[T_co]], MutableSetRole[T_co]): - """Proxy to :class:`typing.MutableSet` object.""" + """Proxy to `typing.MutableSet` object.""" class ContextManagerRole(ContextManager[T]): - """Role/Mixin for :class:`typing.ContextManager` proxy methods.""" + """Role/Mixin for `typing.ContextManager` proxy methods.""" def _get_context(self) -> ContextManager[T]: obj = self._get_current_object() # type: ignore @@ -703,11 +703,11 @@ def __exit__(self, *exc_info: Any) -> Any: class ContextManagerProxy(Proxy[ContextManager[T]], ContextManagerRole[T]): - """Proxy to :class:`typing.ContextManager` object.""" + """Proxy to `typing.ContextManager` object.""" class AsyncContextManagerRole(AsyncContextManager[T_co]): - """Role/Mixin for :class:`typing.AsyncContextManager` proxy methods.""" + """Role/Mixin for `typing.AsyncContextManager` proxy methods.""" def __aenter__(self) -> Awaitable[T_co]: obj = self._get_current_object() # type: ignore @@ -727,11 +727,11 @@ def __aexit__( class AsyncContextManagerProxy( Proxy[AsyncContextManager[T_co]], AsyncContextManagerRole[T_co] ): - """Proxy to :class:`typing.AsyncContextManager` object.""" + """Proxy to `typing.AsyncContextManager` object.""" class MappingRole(Mapping[KT, VT_co]): - """Role/Mixin for :class:`typing.Mapping` proxy methods.""" + """Role/Mixin for `typing.Mapping` proxy methods.""" def _get_mapping(self) -> Mapping[KT, VT_co]: obj = self._get_current_object() # type: ignore @@ -769,11 +769,11 @@ def __len__(self) -> int: class MappingProxy(Proxy[Mapping[KT, VT_co]], MappingRole[KT, VT_co]): - """Proxy to :class:`typing.Mapping` object.""" + """Proxy to `typing.Mapping` object.""" class MutableMappingRole(MappingRole[KT, VT], MutableMapping[KT, VT]): - """Role/Mixin for :class:`typing.MutableMapping` proxy methods.""" + """Role/Mixin for `typing.MutableMapping` proxy methods.""" def _get_mapping(self) -> MutableMapping[KT, VT]: obj = self._get_current_object() # type: ignore @@ -819,11 +819,11 @@ def update(self, *args: Any, **kwargs: Any) -> None: class MutableMappingProxy( Proxy[MutableMapping[KT, VT]], MutableMappingRole[KT, VT] ): - """Proxy to :class:`typing.MutableMapping` object.""" + """Proxy to `typing.MutableMapping` object.""" class CallableRole: - """Role/Mixin for :class:`typing.Callable` proxy methods.""" + """Role/Mixin for `typing.Callable` proxy methods.""" def _get_callable(self) -> Callable: obj = self._get_current_object() # type: ignore @@ -834,7 +834,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: class CallableProxy(Proxy[Callable], CallableRole): - """Proxy to :class:`typing.Callable` object.""" + """Proxy to `typing.Callable` object.""" def maybe_evaluate(obj: Any) -> Any: diff --git a/mode/loop/__init__.py b/mode/loop/__init__.py index 4ebec3ef..d964cbdf 100644 --- a/mode/loop/__init__.py +++ b/mode/loop/__init__.py @@ -6,14 +6,14 @@ The choices available are: aio **default** - Normal :mod:`asyncio` event loop policy. + Normal `asyncio` event loop policy. ### eventlet -Use :pypi:`eventlet` as the event loop. +Use [`eventlet`](https://pypi.org/project/eventlet) as the event loop. -This uses :pypi:`aioeventlet` and will apply the -:pypi:`eventlet` monkey-patches. +This uses [`aioeventlet`](https://pypi.org/project/aioeventlet) and will apply the +[`eventlet`](https://pypi.org/project/eventlet) monkey-patches. To enable execute the following as the first thing that happens when your program starts (e.g. add it as the top import of your @@ -26,10 +26,10 @@ ### gevent -Use :pypi:`gevent` as the event loop. +Use [`gevent`](https://pypi.org/project/gevent) as the event loop. -This uses :pypi:`aiogevent` (+modifications) and will apply the -:pypi:`gevent` monkey-patches. +This uses [`aiogevent`](https://pypi.org/project/aiogevent) (+modifications) and will apply the +[`gevent`](https://pypi.org/project/gevent) monkey-patches. This choice enables you to run blocking Python code as if they have invisible `async/await` syntax around it (NOTE: C extensions are @@ -46,7 +46,7 @@ ### uvloop -Event loop using :pypi:`uvloop`. +Event loop using [`uvloop`](https://pypi.org/project/uvloop). To enable execute the following as the first thing that happens when your program starts (e.g. add it as the top import of your diff --git a/mode/loop/_gevent_loop.py b/mode/loop/_gevent_loop.py index 133fc71c..8dc86e5d 100644 --- a/mode/loop/_gevent_loop.py +++ b/mode/loop/_gevent_loop.py @@ -7,7 +7,7 @@ class Loop(gevent.core.loop): # type: ignore - """Gevent core event loop modifed to support :mod:`asyncio`.""" + """Gevent core event loop modifed to support `asyncio`.""" _aioloop_loop = None diff --git a/mode/loop/eventlet.py b/mode/loop/eventlet.py index 112f1bbf..38575a81 100644 --- a/mode/loop/eventlet.py +++ b/mode/loop/eventlet.py @@ -1,4 +1,4 @@ -"""Enable :pypi:`eventlet` support for :mod:`asyncio`.""" +"""Enable [`eventlet`](https://pypi.org/project/eventlet) support for `asyncio`.""" import asyncio import os diff --git a/mode/loop/gevent.py b/mode/loop/gevent.py index 02a9652b..fce643ae 100644 --- a/mode/loop/gevent.py +++ b/mode/loop/gevent.py @@ -1,4 +1,4 @@ -"""Enable :pypi:`gevent` support for :mod:`asyncio`.""" +"""Enable [`gevent`](https://pypi.org/project/gevent) support for `asyncio`.""" import asyncio import os diff --git a/mode/loop/uvloop.py b/mode/loop/uvloop.py index 3304aac2..42f3e046 100644 --- a/mode/loop/uvloop.py +++ b/mode/loop/uvloop.py @@ -1,4 +1,4 @@ -"""Enable :pypi:`uvloop` as the event loop for :mod:`asyncio`.""" +"""Enable [`uvloop`](https://pypi.org/project/uvloop) as the event loop for `asyncio`.""" import asyncio diff --git a/mode/services.py b/mode/services.py index c2fdaf2a..e8850f11 100644 --- a/mode/services.py +++ b/mode/services.py @@ -62,7 +62,7 @@ class WaitResults(NamedTuple): class WaitResult(NamedTuple): - """Return value of :meth:`Service.wait`.""" + """Return value of `Service.wait`.""" #: Return value of the future we were waiting for. result: Any @@ -443,11 +443,11 @@ def timer( """Background timer executing every ``n`` seconds. ```python - class S(Service): + class S(Service): - @Service.timer(1.0) - async def background_timer(self): - print('Waking up') + @Service.timer(1.0) + async def background_timer(self): + print('Waking up') ``` """ _interval = want_seconds(interval) @@ -1063,7 +1063,7 @@ async def itertimer( """Sleep `interval` seconds for every iteration. This is an async iterator that takes advantage - of :func:`~mode.timers.Timer` to monitor drift and timer + of `~mode.timers.Timer` to monitor drift and timer overlap. Uses `Service.sleep` so exits fast when the service is diff --git a/mode/supervisors.py b/mode/supervisors.py index c85a84c9..89a8846b 100644 --- a/mode/supervisors.py +++ b/mode/supervisors.py @@ -90,7 +90,7 @@ def _contribute_to_service(self, service: ServiceT) -> None: # Setting the service.supervisor attribute here means calling # `await service.crash(exc)` won't traverse the tree, crash # every parent of the service, until it hits Worker terminating - # the running program abruptly. See :class:`CrashingSupervisor`. + # the running program abruptly. See `CrashingSupervisor`. service.supervisor = self def discard(self, *services: ServiceT) -> None: diff --git a/mode/threads.py b/mode/threads.py index 8a5c5653..2dd0aeb7 100644 --- a/mode/threads.py +++ b/mode/threads.py @@ -3,8 +3,9 @@ Will use the default thread pool executor (``loop.set_default_executor()``), unless you specify a specific executor instance. -Note: To stop something using the thread's loop, you have to -use the ``on_thread_stop`` callback instead of the on_stop callback. +!!! note + To stop something using the thread's loop, you have to + use the ``on_thread_stop`` callback instead of the on_stop callback. """ import asyncio diff --git a/mode/timers.py b/mode/timers.py index 01997a7d..4d7a6138 100644 --- a/mode/timers.py +++ b/mode/timers.py @@ -174,9 +174,9 @@ def timer_intervals( # XXX deprecated ) -> Iterator[float]: """Generate timer sleep times. - Note: This function is deprecated, please use :func:`itertimer` - instead (this function also sleeps and calculates sleep time correctly.) - + !!! note + This function is deprecated, please use `itertimer` + instead (this function also sleeps and calculates sleep time correctly.) """ state = Timer( interval, diff --git a/mode/types/services.py b/mode/types/services.py index 32f6f269..7f69a5f4 100644 --- a/mode/types/services.py +++ b/mode/types/services.py @@ -1,4 +1,4 @@ -"""Type classes for :mod:`mode.services`.""" +"""Type classes for `mode.services`.""" import abc import asyncio @@ -48,7 +48,7 @@ class ServiceT(AsyncContextManager): """Abstract type for an asynchronous service that can be started/stopped. See Also: - :class:`mode.Service`. + `mode.Service`. """ Diag: Type[DiagT] diff --git a/mode/types/signals.py b/mode/types/signals.py index 9ba009f6..0fc43cd3 100644 --- a/mode/types/signals.py +++ b/mode/types/signals.py @@ -1,4 +1,4 @@ -"""Type classes for :mod:`mode.signals`.""" +"""Type classes for `mode.signals`.""" import abc import asyncio diff --git a/mode/types/supervisors.py b/mode/types/supervisors.py index 0fbe2bd3..f1c00996 100644 --- a/mode/types/supervisors.py +++ b/mode/types/supervisors.py @@ -1,4 +1,4 @@ -"""Type classes for :mod:`mode.supervisors`.""" +"""Type classes for `mode.supervisors`.""" import abc import typing diff --git a/mode/utils/aiter.py b/mode/utils/aiter.py index b285f143..f7cf0dd5 100644 --- a/mode/utils/aiter.py +++ b/mode/utils/aiter.py @@ -112,7 +112,7 @@ async def __anext__(self) -> int: class arange(AsyncIterable[int]): - """Async generator that counts like :class:`range`.""" + """Async generator that counts like `range`.""" def __init__( self, *slice_args: Optional[int], **slice_kwargs: Optional[int] diff --git a/mode/utils/collections.py b/mode/utils/collections.py index 992f60d0..2ebfc909 100644 --- a/mode/utils/collections.py +++ b/mode/utils/collections.py @@ -78,7 +78,7 @@ class LazySettings: ... class Heap(MutableSequence[T]): - """Generic interface to :mod:`heapq`.""" + """Generic interface to `heapq`.""" def __init__(self, data: Optional[Sequence[T]] = None) -> None: self.data = list(data or []) @@ -104,14 +104,14 @@ def pushpop(self, item: T) -> T: """Push item on the heap, then pop and return from the heap. The combined action runs more efficiently than - :meth:`push` followed by a separate call to :meth:`pop`. + `push` followed by a separate call to `pop`. """ return heappushpop(self.data, item) def replace(self, item: T) -> T: """Pop and return the current smallest value, and add the new item. - This is more efficient than :meth`pop` followed by :meth:`push`, + This is more efficient than :meth`pop` followed by `push`, and can be more appropriate when using a fixed-size heap. Note that the value returned may be larger than item! @@ -182,7 +182,7 @@ def __len__(self) -> int: class FastUserDict(MutableMapping[KT, VT]): """Proxy to dict. - Like :class:`collection.UserDict` but reimplements some methods + Like `collection.UserDict` but reimplements some methods for better performance when the underlying dictionary is a real dict. """ @@ -650,7 +650,9 @@ def raw_update(self, *args: Any, **kwargs: Any) -> None: class AttributeDictMixin: """Mixin for Mapping interface that adds attribute access. - I.e., `d.key -> d[key]`). + Example: + + `d.key` -> `d[key]` """ def __getattr__(self, k: str) -> Any: @@ -663,7 +665,11 @@ def __getattr__(self, k: str) -> Any: ) from err def __setattr__(self, key: str, value: Any) -> None: - """`d[key] = value -> d.key = value`.""" + """ + ```python + d[key] = value -> d.key = value + ``` + """ self[key] = value @@ -674,8 +680,8 @@ class AttributeDict(dict, AttributeDictMixin): class DictAttribute(MutableMapping[str, VT], MappingViewProxy): """Dict interface to attributes. - `obj[k] -> obj.k` - `obj[k] = val -> obj.k = val` + - `obj[k]` -> `obj.k` + - `obj[k] = val` -> `obj.k = val` """ obj: Any = None diff --git a/mode/utils/compat.py b/mode/utils/compat.py index bf3d7222..80252774 100644 --- a/mode/utils/compat.py +++ b/mode/utils/compat.py @@ -23,7 +23,7 @@ def isatty(fh: IO) -> bool: """Return True if fh has a controlling terminal. Notes: - Use with e.g. :data:`sys.stdin`. + Use with e.g. `sys.stdin`. """ try: return fh.isatty() diff --git a/mode/utils/futures.py b/mode/utils/futures.py index 39c6bfba..0776c14c 100644 --- a/mode/utils/futures.py +++ b/mode/utils/futures.py @@ -121,7 +121,7 @@ def __get__(self, obj: Any, type: Optional[Type] = None) -> Any: def done_future( result: Any = None, *, loop: Optional[asyncio.AbstractEventLoop] = None ) -> asyncio.Future: - """Return :class:`asyncio.Future` that is already evaluated.""" + """Return `asyncio.Future` that is already evaluated.""" f = ( loop or asyncio.get_event_loop_policy().get_event_loop() ).create_future() @@ -167,7 +167,7 @@ def maybe_set_result(fut: Optional[asyncio.Future], result: Any) -> bool: def notify(fut: Optional[asyncio.Future], result: Any = None) -> None: - """Set :class:`asyncio.Future` result if future exists and is not done.""" + """Set `asyncio.Future` result if future exists and is not done.""" # can be used to turn a Future into a lockless, single-consumer condition, # for multi-consumer use asyncio.Condition if fut is not None and not fut.done(): diff --git a/mode/utils/imports.py b/mode/utils/imports.py index f03a54f4..afb00681 100644 --- a/mode/utils/imports.py +++ b/mode/utils/imports.py @@ -149,7 +149,7 @@ def _ensure_identifier(path: str, full: str) -> None: class ParsedSymbol(NamedTuple): - """Tuple returned by :func:`parse_symbol`.""" + """Tuple returned by `parse_symbol`.""" module_name: Optional[str] attribute_name: Optional[str] @@ -162,7 +162,7 @@ def parse_symbol( strict_separator: str = ":", relative_separator: str = ".", ) -> ParsedSymbol: - """Parse :func:`symbol_by_name` argument into components. + """Parse `symbol_by_name` argument into components. Returns: ParsedSymbol: Tuple of ``(module_name, attribute_name)`` @@ -254,20 +254,19 @@ def symbol_by_name( Examples: - ```sh - >>> symbol_by_name('mazecache.backends.redis:RedisBackend') - - - >>> symbol_by_name('default', { - ... 'default': 'mazecache.backends.redis:RedisBackend'}) - + ```sh + >>> symbol_by_name('mazecache.backends.redis:RedisBackend') + - # Does not try to look up non-string names. - >>> from mazecache.backends.redis import RedisBackend - >>> symbol_by_name(RedisBackend) is RedisBackend - True - ``` + >>> symbol_by_name('default', { + ... 'default': 'mazecache.backends.redis:RedisBackend'}) + + # Does not try to look up non-string names. + >>> from mazecache.backends.redis import RedisBackend + >>> symbol_by_name(RedisBackend) is RedisBackend + True + ``` """ # This code was copied from kombu.utils.symbol_by_name imp = importlib.import_module if imp is None else imp @@ -412,7 +411,7 @@ def import_from_cwd( def smart_import(path: str, imp: Any = None) -> Any: - """Import module if module, otherwise same as :func:`symbol_by_name`.""" + """Import module if module, otherwise same as `symbol_by_name`.""" imp = importlib.import_module if imp is None else imp if ":" in path: # Path includes attribute so can just jump diff --git a/mode/utils/locals.py b/mode/utils/locals.py index bc08f60d..955b1f1a 100644 --- a/mode/utils/locals.py +++ b/mode/utils/locals.py @@ -1,4 +1,4 @@ -"""Implements thread-local stack using :class:`ContextVar` (:pep:`567`). +"""Implements thread-local stack using `ContextVar` (:pep:`567`). This is a reimplementation of the local stack as used by Flask, Werkzeug, Celery, and other libraries to keep a thread-local stack of objects. diff --git a/mode/utils/locks.py b/mode/utils/locks.py index 9a331aea..be89882c 100644 --- a/mode/utils/locks.py +++ b/mode/utils/locks.py @@ -1,6 +1,6 @@ """Modern versions of asyncio.locks. -asyncio primitives call get_event_loop() in __init__, +asyncio primitives call `get_event_loop()` in __init__, which makes them unsuitable for use in programs that don't want to pass the loop around. """ diff --git a/mode/utils/logging.py b/mode/utils/logging.py index 64b287ca..bf8a4c38 100644 --- a/mode/utils/logging.py +++ b/mode/utils/logging.py @@ -305,7 +305,7 @@ def formatter(fun: FormatterHandler) -> FormatterHandler: def formatter2(fun: FormatterHandler2) -> FormatterHandler2: """Register formatter for logging positional args. - Like :func:`formatter` but the handler accepts + Like `formatter` but the handler accepts two arguments instead of one: ``(arg, logrecord)``. Passing the log record as additional argument expands @@ -333,7 +333,7 @@ def format(self, record: logging.LogRecord) -> str: class ExtensionFormatter(colorlog.TTYColoredFormatter): """Formatter that can register callbacks to format args. - Extends :pypi:`colorlog`. + Extends [`colorlog`](https://pypi.org/project/colorlog). """ def __init__(self, stream: Optional[IO] = None, **kwargs: Any) -> None: @@ -585,7 +585,7 @@ def cry( def print_task_name(task: asyncio.Task, file: IO) -> None: - """Print name of :class:`asyncio.Task` in tracebacks.""" + """Print name of `asyncio.Task` in tracebacks.""" coro = task._coro # type: ignore wrapped = getattr(task, "__wrapped__", None) coro_name = getattr(coro, "__name__", None) @@ -610,7 +610,7 @@ class LogMessage(NamedTuple): class flight_recorder(ContextManager, LogSeverityMixin): - """Flight Recorder context for use with :keyword:`with` statement. + """Flight Recorder context for use with `with` statement. This is a logging utility to log stuff only when something times out. @@ -650,7 +650,7 @@ def _background_refresh(self) -> None: on_timeout.info(f'-redis_client.get({POSTS_KEY!r})') ``` - If the body of this :keyword:`with` statement completes before the + If the body of this `with` statement completes before the timeout, the logs are forgotten about and never emitted -- if it takes more than ten seconds to complete, we will see these messages in the log: @@ -863,7 +863,7 @@ def _safewrap_handlers(self) -> None: def _safewrap_handler(self, handler: logging.Handler) -> None: # Make the logger handlers dump internal errors to - # :data:`sys.__stderr__` instead of :data:`sys.stderr` to circumvent + # `sys.__stderr__` instead of `sys.stderr` to circumvent # infinite loops. class WithSafeHandleError(logging.Handler): def handleError(self, record: logging.LogRecord) -> None: @@ -983,7 +983,7 @@ def redirect_stdouts( stdout: bool = True, stderr: bool = True, ) -> Iterator[FileLogProxy]: - """Redirect :data:`sys.stdout` and :data:`sys.stdout` to logger.""" + """Redirect `sys.stdout` and `sys.stdout` to logger.""" proxy = FileLogProxy(logger, severity=severity) if stdout: sys.stdout = proxy diff --git a/mode/utils/mocks.py b/mode/utils/mocks.py index 1ee8d4d4..5cd4eb32 100644 --- a/mode/utils/mocks.py +++ b/mode/utils/mocks.py @@ -36,7 +36,7 @@ def __repr__(self) -> str: @contextmanager def patch_module(*names: str, new_callable: Any = MagicMock) -> Iterator: - """Mock one or modules such that every attribute is a :class:`Mock`.""" + """Mock one or modules such that every attribute is a `Mock`.""" prev = {} class MockModule(types.ModuleType): diff --git a/mode/utils/objects.py b/mode/utils/objects.py index f659b56e..8a515809 100644 --- a/mode/utils/objects.py +++ b/mode/utils/objects.py @@ -123,7 +123,7 @@ class _UsingKwargsInNew(_InitSubclassCheck, ident=909): ... class InvalidAnnotation(Exception): - """Raised by :func:`annotations` when encountering an invalid type.""" + """Raised by `annotations` when encountering an invalid type.""" @total_ordering @@ -152,7 +152,7 @@ class KeywordReduce: """Mixin class for objects that can be "pickled". "Pickled" means the object can be serialized using the Python binary - serializer -- the :mod:`pickle` module. + serializer -- the `pickle` module. Python objects are made pickleable through defining the ``__reduce__`` method, that returns a tuple of: @@ -265,11 +265,11 @@ def annotations( :exc:`InvalidAnnotation` (does not test for subclasses). alias_types: Mapping of original type to replacement type. skip_classvar: Skip attributes annotated with - :class:`typing.ClassVar`. + `typing.ClassVar`. globalns: Global namespace to use when evaluating forward - references (see :class:`typing.ForwardRef`). + references (see `typing.ForwardRef`). localns: Local namespace to use when evaluating forward - references (see :class:`typing.ForwardRef`). + references (see `typing.ForwardRef`). Returns: Tuple[FieldMapping, DefaultsMapping]: Tuple with two dictionaries, diff --git a/mode/utils/queues.py b/mode/utils/queues.py index 2f18aa74..c09bca75 100644 --- a/mode/utils/queues.py +++ b/mode/utils/queues.py @@ -1,4 +1,4 @@ -"""Queue utilities - variations of :class:`asyncio.Queue`.""" +"""Queue utilities - variations of `asyncio.Queue`.""" import asyncio import math @@ -24,7 +24,7 @@ class FlowControlEvent: - """Manage flow control :class:`FlowControlQueue` instances. + """Manage flow control `FlowControlQueue` instances. The FlowControlEvent manages flow in one or many queue instances at the same time. @@ -87,7 +87,7 @@ def __init__( self._queues = WeakSet() def manage_queue(self, queue: "FlowControlQueue") -> None: - """Add :class:`FlowControlQueue` to be cleared on resume.""" + """Add `FlowControlQueue` to be cleared on resume.""" self._queues.add(queue) def suspend(self) -> None: @@ -115,10 +115,10 @@ async def acquire(self) -> None: class FlowControlQueue(asyncio.Queue): - """:class:`asyncio.Queue` managed by :class:`FlowControlEvent`. + """`asyncio.Queue` managed by `FlowControlEvent`. See Also: - :class:`FlowControlEvent`. + `FlowControlEvent`. """ pressure_high_ratio = 1.25 # divided by diff --git a/mode/utils/text.py b/mode/utils/text.py index a109722a..3605c3c1 100644 --- a/mode/utils/text.py +++ b/mode/utils/text.py @@ -47,7 +47,7 @@ def isatty(fh: IO) -> bool: """Return True if fh has a controlling terminal. Notes: - Use with e.g. :data:`sys.stdin`. + Use with e.g. `sys.stdin`. """ try: return fh.isatty() @@ -79,21 +79,23 @@ def didyoumean( """Generate message with helpful list of alternatives. Examples: - >>> raise Exception(f'Unknown mode: {mode}! {didyoumean(modes, mode)}') - >>> didyoumean(['foo', 'bar', 'baz'], 'boo') - 'Did you mean foo?' + ```sh + >>> raise Exception(f'Unknown mode: {mode}! {didyoumean(modes, mode)}') + + >>> didyoumean(['foo', 'bar', 'baz'], 'boo') + 'Did you mean foo?' - >>> didyoumean(['foo', 'moo', 'bar'], 'boo') - 'Did you mean one of foo, moo?' + >>> didyoumean(['foo', 'moo', 'bar'], 'boo') + 'Did you mean one of foo, moo?' - >>> didyoumean(['foo', 'moo', 'bar'], 'xxx') - '' + >>> didyoumean(['foo', 'moo', 'bar'], 'xxx') + '' + ``` Arguments: haystack: List of all available choices. needle: What the user provided. - fmt_many: String format returned when there are more than one alternative. Default is: ``"Did you mean one of {alt}?"``. fmt_one: String format returned when there's a single fuzzy match. @@ -127,7 +129,7 @@ def enumeration( ```sh >>> enumeration(['x', 'y', '...']) - "1) x\n2) y\n3) ..." + "1) x\\n2) y\\n3) ..." ``` """ return sep.join( @@ -224,7 +226,7 @@ def abbr_fqdn(origin: str, name: str, *, prefix: str = "") -> str: 'examples.other.foo' ``` - :func:`shorten_fqdn` is similar, but will always shorten a too long name, + `shorten_fqdn` is similar, but will always shorten a too long name, abbr_fqdn will only remove the origin portion of the name. """ if name.startswith(origin): diff --git a/mode/utils/times.py b/mode/utils/times.py index b47ea855..d22341a4 100644 --- a/mode/utils/times.py +++ b/mode/utils/times.py @@ -37,7 +37,7 @@ else: TIME_MONOTONIC = time.monotonic -#: Seconds can be expressed as float or :class:`~datetime.timedelta`, +#: Seconds can be expressed as float or `~datetime.timedelta`, Seconds = Union[timedelta, float, str] @@ -243,7 +243,7 @@ def rate_limit( @singledispatch def want_seconds(s: float) -> float: - """Convert :data:`Seconds` to float.""" + """Convert `Seconds` to float.""" return s @@ -271,7 +271,7 @@ def humanize_seconds( For example, 60 becomes "1 minute", and 7200 becomes "2 hours". Arguments: - secs: Seconds to format (as :class:`float` or :class:`int`). + secs: Seconds to format (as `float` or `int`). prefix (str): can be used to add a preposition to the output (e.g., 'in' will give 'in 1 second', but add nothing to 'now'). suffix (str): same as prefix, adds suffix unless 'now'. diff --git a/mode/utils/tracebacks.py b/mode/utils/tracebacks.py index 74a77fd9..b12067b3 100644 --- a/mode/utils/tracebacks.py +++ b/mode/utils/tracebacks.py @@ -30,7 +30,7 @@ def print_task_stack( limit: int = DEFAULT_MAX_FRAMES, capture_locals: bool = False, ) -> None: - """Print the stack trace for an :class:`asyncio.Task`.""" + """Print the stack trace for an `asyncio.Task`.""" print(f"Stack for {task!r} (most recent call last):", file=file) tb = Traceback.from_task(task, limit=limit) print_list( @@ -89,7 +89,7 @@ def format_task_stack( limit: int = DEFAULT_MAX_FRAMES, capture_locals: bool = False, ) -> str: - """Format :class:`asyncio.Task` stack trace as a string.""" + """Format `asyncio.Task` stack trace as a string.""" f = io.StringIO() print_task_stack(task, file=f, limit=limit, capture_locals=capture_locals) return f.getvalue() diff --git a/mode/utils/trees.py b/mode/utils/trees.py index 53ab4975..b2a8645f 100644 --- a/mode/utils/trees.py +++ b/mode/utils/trees.py @@ -119,7 +119,7 @@ def walk(self) -> Iterator[NodeT[T]]: node = node.parent def as_graph(self) -> DependencyGraphT: - """Convert to :class:`~mode.utils.graphs.DependencyGraph`.""" + """Convert to `~mode.utils.graphs.DependencyGraph`.""" graph = DependencyGraph() stack: Deque[NodeT] = Deque([self]) while stack: diff --git a/mode/utils/types/graphs.py b/mode/utils/types/graphs.py index d5de3830..a1aec760 100644 --- a/mode/utils/types/graphs.py +++ b/mode/utils/types/graphs.py @@ -1,4 +1,4 @@ -"""Type classes for :mod:`mode.utils.graphs`.""" +"""Type classes for `mode.utils.graphs`.""" import abc from typing import ( diff --git a/mode/utils/types/trees.py b/mode/utils/types/trees.py index c2fc0f3b..5d38672d 100644 --- a/mode/utils/types/trees.py +++ b/mode/utils/types/trees.py @@ -1,4 +1,4 @@ -"""Type classes for :mod:`mode.utils.trees`.""" +"""Type classes for `mode.utils.trees`.""" import abc from typing import Any, Generic, Iterator, List, Optional, TypeVar, Union diff --git a/requirements-docs.txt b/requirements-docs.txt index f67f9879..2a5a7446 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,8 +1,3 @@ -six -sphinx<6.0.0 -sphinx_celery>=1.4.8 -sphinx-autodoc-annotation -alabaster -babel -sphinx-autobuild # TODO: check need -sphinx2rst>=1.0 # TODO: check need +mkdocs>=1.5.3 +mkdocs-material>=9.5.13 +mkdocstrings>=0.24.1 From c235cfef44f237945b4370491d1bbf0cd03afcca Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Sat, 9 Mar 2024 10:26:29 +0100 Subject: [PATCH 05/16] fix: working on fixing github actions pipeline --- .editorconfig | 4 + .../{gh-pages.yml => deploy-docs.yml} | 32 +- .github/workflows/dist.yml | 47 -- .github/workflows/publish.yml | 49 ++ .../{python-package.yml => tests.yml} | 38 +- .pre-commit-config.yaml | 5 +- CHANGELOG.md | 4 + CODE_OF_CONDUCT.rst => CODE_OF_CONDUCT.md | 3 +- CONTRIBUTING.md | 131 +++++ Changelog | 35 -- Makefile | 147 ----- README.md | 492 ++++++++++++++++ README.rst | 534 ------------------ mode/loop/gevent.py | 6 +- mode/utils/times.py | 4 +- mode/utils/tracebacks.py | 4 +- pyproject.toml | 7 +- requirements-docs.txt | 2 +- requirements-tests.txt | 5 +- requirements.txt | 2 +- scripts/README.md | 9 - scripts/build | 13 - scripts/build-docs.sh | 5 + scripts/build.sh | 6 + scripts/bump.sh | 5 + scripts/check | 13 - scripts/{clean => clean.sh} | 0 scripts/docs | 10 - scripts/format | 11 - scripts/format.sh | 6 + scripts/lint.sh | 8 + scripts/publish | 19 - scripts/test | 14 - scripts/tests.sh | 6 + 34 files changed, 778 insertions(+), 898 deletions(-) rename .github/workflows/{gh-pages.yml => deploy-docs.yml} (66%) delete mode 100644 .github/workflows/dist.yml create mode 100644 .github/workflows/publish.yml rename .github/workflows/{python-package.yml => tests.yml} (63%) create mode 100644 CHANGELOG.md rename CODE_OF_CONDUCT.rst => CODE_OF_CONDUCT.md (98%) create mode 100644 CONTRIBUTING.md delete mode 100644 Changelog delete mode 100644 Makefile create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 scripts/README.md delete mode 100755 scripts/build create mode 100755 scripts/build-docs.sh create mode 100755 scripts/build.sh create mode 100755 scripts/bump.sh delete mode 100755 scripts/check rename scripts/{clean => clean.sh} (100%) delete mode 100755 scripts/docs delete mode 100755 scripts/format create mode 100755 scripts/format.sh create mode 100755 scripts/lint.sh delete mode 100755 scripts/publish delete mode 100755 scripts/test create mode 100755 scripts/tests.sh diff --git a/.editorconfig b/.editorconfig index 22fb1f90..77f690c9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,5 +10,9 @@ insert_final_newline = true charset = utf-8 end_of_line = lf +[{*.yml,*.yaml}] +indent_style = space +indent_size = 2 + [Makefile] indent_style = tab diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/deploy-docs.yml similarity index 66% rename from .github/workflows/gh-pages.yml rename to .github/workflows/deploy-docs.yml index 68f1475b..7876651e 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/deploy-docs.yml @@ -8,28 +8,34 @@ on: release: types: [created] branches: - - 'master' + - master jobs: build: - name: "Build docs" + name: Build docs runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v4 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # otherwise, you will failed to push refs to dest repo - - name: "Install runtime dependencies in order to get package metadata" - run: "scripts/install" - - name: "Install deps and build with Sphinx" - run: make docs - - name: "Upload artifacts" - uses: actions/upload-pages-artifact@v1 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Build docs + run: scripts/build-docs.sh + + - name: Upload artifacts + uses: actions/upload-pages-artifact@v3 with: # Upload built docs - path: "./Documentation" + path: "./site" deploy: - name: "Deploy docs" + name: Deploy docs if: github.event_name == 'release' && github.event.action == 'published' needs: build runs-on: ubuntu-latest @@ -42,6 +48,6 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - - uses: actions/deploy-pages@v1 + - uses: actions/deploy-pages@v4 id: deployment name: "Deploy to GitHub Pages" diff --git a/.github/workflows/dist.yml b/.github/workflows/dist.yml deleted file mode 100644 index 52fe3ba1..00000000 --- a/.github/workflows/dist.yml +++ /dev/null @@ -1,47 +0,0 @@ -# vim:ts=2:sw=2:et:ai:sts=2 -name: 'Build distribution' - -on: - # Only run when release is created in the master branch - release: - types: [created] - branches: - - 'master' - -jobs: - build: - name: 'Build distributable files' - runs-on: 'ubuntu-latest' - steps: - - uses: actions/checkout@v3 - name: 'Checkout source repository' - with: - fetch-depth: 0 - - - uses: actions/setup-python@v4 - - - name: 'Build sdist and wheel' - run: python3 setup.py sdist bdist_wheel - - - uses: actions/upload-artifact@v2 - name: 'Upload build artifacts' - with: - path: 'dist/*' - - upload_pypi: - name: 'Upload packages' - needs: ['build'] - runs-on: 'ubuntu-latest' - if: github.event_name == 'release' && github.event.action == 'created' - steps: - - uses: actions/download-artifact@v3 - name: 'Download artifacts' - with: - name: 'artifact' - path: 'dist' - - - uses: pypa/gh-action-pypi-publish@release/v1 - name: "Publish package to PyPI" - with: - user: '__token__' - password: '${{ secrets.PYPI_API_TOKEN }}' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..54021005 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Publish Package + +on: + # Only run when release is created in the master branch + release: + types: [created] + branches: + - 'master' + +jobs: + build: + name: Build distributable files + runs-on: 'ubuntu-latest' + steps: + - name: 'Checkout source repository' + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + + - name: Install build dependencies + run: pip install build twine + + - name: 'Build package' + run: scripts/build.sh + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + path: 'dist/*' + + upload_pypi: + name: Upload packages + needs: ['build'] + runs-on: 'ubuntu-latest' + if: github.event_name == 'release' && github.event.action == 'created' + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifact + path: dist + + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: '__token__' + password: '${{ secrets.PYPI_API_TOKEN }}' diff --git a/.github/workflows/python-package.yml b/.github/workflows/tests.yml similarity index 63% rename from .github/workflows/python-package.yml rename to .github/workflows/tests.yml index fbeb126d..0a5bda48 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/tests.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python -name: Run Python tests +name: Python package on: push: @@ -31,24 +31,30 @@ jobs: experimental: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: "actions/setup-python@v4" + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: - python-version: "${{ matrix.python-version }}" - cache: "pip" + python-version: ${{ matrix.python-version }} + cache: pip cache-dependency-path: | - requirements/*.txt - requirements/**/*.txt - - name: "Install dependencies" - run: "scripts/install" - - name: "Run linting checks" - run: "scripts/check" - - name: "Run tests" - run: "scripts/tests" - - name: "Enforce coverage" - uses: codecov/codecov-action@v3 + requirements-*.txt + pyproject.toml + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run linting checks + run: scripts/lint.sh + + - name: Run tests + run: scripts/tests.sh + + - name: Enforce coverage + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 15d6d113..42c8326d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,10 @@ repos: - id: check-yaml - id: check-toml - id: check-added-large-files - + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.18.0 + hooks: + - id: commitizen - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.3.0 hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b80da178 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ + +## v0.2.0 (2024-03-02) + +## 0.1.0 (2023-01-10) diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.md similarity index 98% rename from CODE_OF_CONDUCT.rst rename to CODE_OF_CONDUCT.md index 7141c712..b5d27c3a 100644 --- a/CODE_OF_CONDUCT.rst +++ b/CODE_OF_CONDUCT.md @@ -1,5 +1,4 @@ -Code of Conduct -=============== +# Code of Conduct Everyone interacting in the project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Mode Code of Conduct. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f1b7210c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,131 @@ +# Contributing + +Contributions to mode are very welcome, feel free to open an issue to propose your ideas. + +To make a contribution please create a pull request. + +## Developing + +First it is recommended to fork this repository into your personal Github account then clone your freshly generated fork locally. + +### Setup environment + +Here are some guidelines to set up your environment: + +```sh +$ cd mode/ +$ python -m venv env # Create python virtual environment, a `env/` has been created +$ source env/bin/active # Activate the environment +(venv) $ which pip # Ensure everything is well configured +/some/directory/mode/env/bin/pip +``` + +### Install project dependencies + +```sh +(venv) $ pip install -r requirements.txt +Obtaining file:///some/directory/mode + Installing build dependencies ... done + Checking if build backend supports build_editable ... done + Getting requirements to build editable ... done + Installing backend dependencies ... done + Preparing editable metadata (pyproject.toml) ... done +Ignoring pre-commit: markers 'python_version < "3.9"' don't match your environment +... +``` + +This project apply some quality rules on code and commit, to enforce them at commit time you should install the [pre-commit](https://pre-commit.com/) hook: + +```sh +(venv) $ pre-commit install +pre-commit installed at .git/hooks/pre-commit +``` + +### Format & lint the code + +You can run the format script to make your change compliant: + +```sh +(venv) $ ./script/format.sh ++ ruff format mode tests +79 files left unchanged ++ ruff check mode tests --fix +``` + +_The script uses [ruff](https://github.com/astral-sh/ruff) & [mypy](https://mypy-lang.org/)._ + +### Run tests + +A script is also available to run them: + +``` +(venv) $ ./scripts/tests.sh ++ pytest tests --cov=mode +Test session starts (platform: linux, Python 3.12.2, pytest 8.1.1, pytest-sugar 1.0.0) +... +``` + +_The script uses [pytest](https://docs.pytest.org/en/8.0.x/contents.html)._ + +### Commit format + +Commit should be formatted following [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). + +You can commit manually and respect the convention or you can use the cli to help you formatting correctly: + +```sh +(venv) $ cz commit +? Select the type of change you are committing docs: Documentation only changes +? What is the scope of this change? (class or file name): (press [enter] to skip) + README +? Write a short and imperative summary of the code changes: (lower case and no period) + correct spelling of README +? Provide additional contextual information about the code changes: (press [enter] to skip) + +? Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer No +? Footer. Information about Breaking Changes and reference issues that this commit closes: (press [enter] to skip) + + +docs(README): correct spelling of README +``` + +## Documentation + +To be able to run the documentation locally you should setup your environment and install dependencies, if not already you can read the two first part of the Developing section. + +```sh +(venv) $ mkdocs serve +INFO - Building documentation... +INFO - Cleaning site directory +INFO - Documentation built in 1.78 seconds +INFO - [19:38:48] Watching paths for changes: 'docs', 'mkdocs.yml' +INFO - [19:38:48] Serving on http://127.0.0.1:8000/ +``` + +Then, you can browse the documentation on http://127.0.0.1:8000. + +## Maintainers + +### Publish a new release + +1. First create a new tag and update changelog + +```sh +(venv) $ ./scripts/bump.sh ++ cz bump --changelog +bump: version 0.2.0 → 0.2.1 +tag to create: 0.2.1 +increment detected: PATCH + +[master b35722f] bump: version 0.2.0 → 0.2.1 + 2 files changed, 2 insertions(+), 1 deletion(-) + +... + +Done! +``` + +!!! note + If this pass it will automatically push the commit and tags to the remote server, you may want to use `cz bump --changelog --dry-run` to check generated changes. + +2. Then after ensuring github pass you could publish the release via the Github interface. (An action will triggered and will publish the package to pypi and documentation to the Github pages). diff --git a/Changelog b/Changelog deleted file mode 100644 index 234e0da0..00000000 --- a/Changelog +++ /dev/null @@ -1,35 +0,0 @@ -.. _changelog: - -================ - Change history -================ - -.. version-0.2.0: - -0.2.0 -===== -:release-date: 2021-10-14 -:release-by: Taybin Rutkin (:github_user:`taybin`) - -- Support python-3.10 - -- format with black and isort - -- add crontab timer from Faust (:github_user:`lqhuang`) - -.. version-0.1.0: - -0.1.0 -===== -:release-date: 2020-12-17 14:00 P.M CET -:release-by: Thomas Sarboni (:github_user:`max-k`) - -- Friendly fork of ask/mode : Initial release - -- Move to new travis-ci.com domain - -- Add tests on Python 3.8.1-3.8.6 - -- Fix broken tests - -- Add Python 3.9 support diff --git a/Makefile b/Makefile deleted file mode 100644 index 58801857..00000000 --- a/Makefile +++ /dev/null @@ -1,147 +0,0 @@ -PROJ ?= mode -PGPIDENT ?= "Faust Security Team" -PYTHON ?= python -PYTEST ?= py.test -PIP ?= pip -GIT ?= git -TOX ?= tox -NOSETESTS ?= nosetests -ICONV ?= iconv -MYPY ?= mypy - -TESTDIR ?= t -README ?= README.rst -README_SRC ?= "docs/templates/readme.txt" -CONTRIBUTING ?= CONTRIBUTING.rst -CONTRIBUTING_SRC ?= "docs/contributing.rst" -COC ?= CODE_OF_CONDUCT.rst -COC_SRC ?= "docs/includes/code-of-conduct.txt" -DOCUMENTATION=Documentation - -all: help - -help: - @echo "docs - Build documentation." - @echo "test-all - Run tests for all supported python versions." - @echo "develop - Install all dependencies into current virtualenv." - @echo "distcheck ---------- - Check distribution for problems." - @echo " test - Run unittests using current python." - @echo " lint ------------ - Check codebase for problems." - @echo " apicheck - Check API reference coverage." - @echo " readmecheck - Check README.rst encoding." - @echo " contribcheck - Check CONTRIBUTING.rst encoding" - @echo " ruff - Check code for syntax and style errors." - @echo "readme - Regenerate README.rst file." - @echo "contrib - Regenerate CONTRIBUTING.rst file" - @echo "coc - Regenerate CODE_OF_CONDUCT.rst file" - @echo "clean-dist --------- - Clean all distribution build artifacts." - @echo " clean-git-force - Remove all uncomitted files." - @echo " clean ------------ - Non-destructive clean" - @echo " clean-pyc - Remove .pyc/__pycache__ files" - @echo " clean-docs - Remove documentation build artifacts." - @echo " clean-build - Remove setup artifacts." - @echo "release - Make PyPI release." - -clean: clean-docs clean-pyc clean-build - -clean-dist: clean clean-git-force - -release: - $(PYTHON) register sdist bdist_wheel upload --sign --identity="$(PGPIDENT)" - -. PHONY: deps-default -deps-default: - $(PIP) install -U -e "." - -. PHONY: deps-docs -deps-docs: - $(PIP) install -U -r requirements-docs.txt - -. PHONY: deps-test -deps-test: - $(PIP) install -U -r requirements-test.txt - -. PHONY: deps-extras -deps-extras: - $(PIP) install -U -r requirements/extras/eventlet.txt - $(PIP) install -U -r requirements/extras/uvloop.txt - -. PHONY: develop -develop: deps-default deps-dist deps-docs deps-test deps-extras - $(PYTHON) develop - -. PHONY: Documentation -Documentation: - mkdocs build - -. PHONY: docs -docs: Documentation - -. PHONE: serve-docs -serve-docs: - mkdocs serve - -clean-docs: - -rm -rf "$(SPHINX_BUILDDIR)" - -ruff: - ruff check . --fix - -lint: ruff apicheck readmecheck - -clean-readme: - -rm -f $(README) - -readmecheck: - $(ICONV) -f ascii -t ascii $(README) >/dev/null - -readme: clean-readme $(README) readmecheck - -clean-contrib: - -rm -f "$(CONTRIBUTING)" - -contrib: clean-contrib $(CONTRIBUTING) - -clean-coc: - -rm -f "$(COC)" - -coc: clean-coc $(COC) - -clean-pyc: - -find . -type f -a \( -name "*.pyc" -o -name "*$$py.class" \) | xargs rm - -find . -type d -name "__pycache__" | xargs rm -r - -removepyc: clean-pyc - -clean-build: - rm -rf build/ dist/ .eggs/ *.egg-info/ .tox/ .coverage cover/ - -clean-git: - $(GIT) clean -xdn - -clean-git-force: - $(GIT) clean -xdf - -test-all: clean-pyc - $(TOX) - -test: - $(PYTEST) . - -cov: - $(PYTEST) -x --cov="$(PROJ)" --cov-report=html - -build: - $(PYTHON) sdist bdist_wheel - -distcheck: lint test clean - -dist: readme contrib clean-dist build - -typecheck: - $(PYTHON) -m $(MYPY) -p $(PROJ) - -.PHONY: requirements -requirements: - $(PIP) install --upgrade pip;\ - $(PIP) install -r requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 00000000..f50b41a7 --- /dev/null +++ b/README.md @@ -0,0 +1,492 @@ +# AsyncIO Services Fork + +

+ + Latest release + + + Coverage + + + BSD License + + + Supported Python versions + +

+ +--- + +**Documentation**: https://faust-streaming.github.io/mode/ + +**Source Code**: https://github.com/faust-streaming/mode + +--- + +## Why the fork + +We have decided to fork the original *Mode* project because there is a critical process of releasing new versions which causes uncertainty in the community. Everybody is welcome to contribute to this *fork*, and you can be added as a maintainer. + +We want to: + +- Ensure continues release +- Code quality +- Support latest Python versions +- Update the documentation + +and more... + +## What is Mode? + +Mode is a very minimal Python library built-on top of AsyncIO that makes +it much easier to use. + +In Mode your program is built out of services that you can start, stop, +restart and supervise. + +A service is just a class: + +```python +class PageViewCache(Service): + redis: Redis = None + + async def on_start(self) -> None: + self.redis = connect_to_redis() + + async def update(self, url: str, n: int = 1) -> int: + return await self.redis.incr(url, n) + + async def get(self, url: str) -> int: + return await self.redis.get(url) +``` + +Services are started, stopped and restarted and have +callbacks for those actions. + +It can start another service: + +```python +class App(Service): + page_view_cache: PageViewCache = None + + async def on_start(self) -> None: + await self.add_runtime_dependency(self.page_view_cache) + + @cached_property + def page_view_cache(self) -> PageViewCache: + return PageViewCache() +``` + +It can include background tasks: + +```python +class PageViewCache(Service): + + @Service.timer(1.0) + async def _update_cache(self) -> None: + self.data = await cache.get('key') +``` + +Services that depends on other services actually form a graph +that you can visualize. + +### Worker + +Mode optionally provides a worker that you can use to start the program, +with support for logging, blocking detection, remote debugging and more. + +To start a worker add this to your program: + + +```python +if __name__ == '__main__': + from mode import Worker + Worker(Service(), loglevel="info").execute_from_commandline() +``` + +Then execute your program to start the worker: + +```log +$ python examples/tutorial.py +[2018-03-27 15:47:12,159: INFO]: [^Worker]: Starting... +[2018-03-27 15:47:12,160: INFO]: [^-AppService]: Starting... +[2018-03-27 15:47:12,160: INFO]: [^--Websockets]: Starting... +STARTING WEBSOCKET SERVER +[2018-03-27 15:47:12,161: INFO]: [^--UserCache]: Starting... +[2018-03-27 15:47:12,161: INFO]: [^--Webserver]: Starting... +[2018-03-27 15:47:12,164: INFO]: [^--Webserver]: Serving on port 8000 +REMOVING EXPIRED USERS +REMOVING EXPIRED USERS +``` + +To stop it hit `Control-c`: + +```log +[2018-03-27 15:55:08,084: INFO]: [^Worker]: Stopping on signal received... +[2018-03-27 15:55:08,084: INFO]: [^Worker]: Stopping... +[2018-03-27 15:55:08,084: INFO]: [^-AppService]: Stopping... +[2018-03-27 15:55:08,084: INFO]: [^--UserCache]: Stopping... +REMOVING EXPIRED USERS +[2018-03-27 15:55:08,085: INFO]: [^Worker]: Gathering service tasks... +[2018-03-27 15:55:08,085: INFO]: [^--UserCache]: -Stopped! +[2018-03-27 15:55:08,085: INFO]: [^--Webserver]: Stopping... +[2018-03-27 15:55:08,085: INFO]: [^Worker]: Gathering all futures... +[2018-03-27 15:55:08,085: INFO]: [^--Webserver]: Closing server +[2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Waiting for server to close handle +[2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Shutting down web application +[2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Waiting for handler to shut down +[2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Cleanup +[2018-03-27 15:55:08,086: INFO]: [^--Webserver]: -Stopped! +[2018-03-27 15:55:08,086: INFO]: [^--Websockets]: Stopping... +[2018-03-27 15:55:08,086: INFO]: [^--Websockets]: -Stopped! +[2018-03-27 15:55:08,087: INFO]: [^-AppService]: -Stopped! +[2018-03-27 15:55:08,087: INFO]: [^Worker]: -Stopped! +``` + +### Beacons + +The `beacon` object that we pass to services keeps track of the services +in a graph. + +They are not strictly required, but can be used to visualize a running +system, for example we can render it as a pretty graph. + +This requires you to have the `pydot` library and GraphViz +installed: + +```sh +$ pip install pydot +``` + +Let's change the app service class to dump the graph to an image +at startup: + +```python +class AppService(Service): + + async def on_start(self) -> None: + print('APP STARTING') + import pydot + import io + o = io.StringIO() + beacon = self.app.beacon.root or self.app.beacon + beacon.as_graph().to_dot(o) + graph, = pydot.graph_from_dot_data(o.getvalue()) + print('WRITING GRAPH TO image.png') + with open('image.png', 'wb') as fh: + fh.write(graph.create_png()) +``` + +## Creating a Service + +To define a service, simply subclass and fill in the methods +to do stuff as the service is started/stopped etc.: + + +```python +class MyService(Service): + + async def on_start(self) -> None: + print('Im starting now') + + async def on_started(self) -> None: + print('Im ready') + + async def on_stop(self) -> None: + print('Im stopping now') +``` + +To start the service, call `await service.start()`: + +```python +await service.start() +``` + +Or you can use `mode.Worker` (or a subclass of this) to start your +services-based asyncio program from the console: + +```python +if __name__ == '__main__': + import mode + worker = mode.Worker( + MyService(), + loglevel='INFO', + logfile=None, + daemon=False, + ) + worker.execute_from_commandline() +``` + +## It's a Graph! + +Services can start other services, coroutines, and background tasks. + +1) Starting other services using `add_dependency`: + +```python +class MyService(Service): + def __post_init__(self) -> None: + self.add_dependency(OtherService(loop=self.loop)) +``` + +1) Start a list of services using `on_init_dependencies`: + +```python +class MyService(Service): + + def on_init_dependencies(self) -> None: + return [ + ServiceA(loop=self.loop), + ServiceB(loop=self.loop), + ServiceC(loop=self.loop), + ] +``` + +1) Start a future/coroutine (that will be waited on to complete on stop): + +```python +class MyService(Service): + + async def on_start(self) -> None: + self.add_future(self.my_coro()) + + async def my_coro(self) -> None: + print('Executing coroutine') +``` + +1) Start a background task: + +```python +class MyService(Service): + + @Service.task + async def _my_coro(self) -> None: + print('Executing coroutine') +``` + +1) Start a background task that keeps running: + +```python +class MyService(Service): + + @Service.task + async def _my_coro(self) -> None: + while not self.should_stop: + # NOTE: self.sleep will wait for one second, or + # until service stopped/crashed. + await self.sleep(1.0) + print('Background thread waking up') +``` + +## Installation + +You can install Mode either via the Python Package Index (PyPI) +or from source. + +To install using `pip`: + +```sh +$ pip install -U mode-streaming +``` + +Downloading and installing from source: http://pypi.org/project/mode-streaming + +You can install it by doing the following: + +```sh +$ tar xvfz mode-streaming-0.2.1.tar.gz +$ cd mode-0.2.1 +$ python -m build . +# python install +``` + +The last command must be executed as a privileged user if +you are not currently using a virtualenv. + + +Using the development version: + +With pip: + +You can install the latest snapshot of Mode using the following +pip command: + +```sh +$ pip install mode-streaming +``` + +## Developing + +The guideline and associated information are stored in [CONTRIBUTING.md](./CONTRIBUTING.md) + +## FAQ + +#### Can I use Mode with Django/Flask/etc.? + +Yes! Use gevent/eventlet as a bridge to integrate with asyncio. + +Using `gevent`: + +This works with any blocking Python library that can work with gevent. + +Using gevent requires you to install the `aiogevent` module, +and you can install this as a bundle with Mode: + +```sh +$ pip install -U mode-streaming[gevent] +``` + +Then to actually use gevent as the event loop you have to +execute the following in your entrypoint module (usually where you +start the worker), before any other third party libraries are imported: + + +```python +#!/usr/bin/env python3 +import mode.loop +mode.loop.use('gevent') +# execute program +``` + +REMEMBER: This must be located at the very top of the module, +in such a way that it executes before you import other libraries. + + +Using `eventlet`: + +This works with any blocking Python library that can work with eventlet. + +Using eventlet requires you to install the `aioeventlet` module, +and you can install this as a bundle with Mode: + +```sh +$ pip install -U mode-streaming[eventlet] +``` + +Then to actually use eventlet as the event loop you have to +execute the following in your entrypoint module (usually where you +start the worker), before any other third party libraries are imported: + +```python +#!/usr/bin/env python3 +import mode.loop +mode.loop.use('eventlet') +# execute program +``` + +REMEMBER: It's very important this is at the very top of the module, +and that it executes before you import libraries. + +#### Can I use Mode with Tornado? + +Yes! Use the `tornado.platform.asyncio` bridge: http://www.tornadoweb.org/en/stable/asyncio.html + +#### Can I use Mode with Twisted? + +Yes! Use the asyncio reactor implementation: +https://twistedmatrix.com/documents/17.1.0/api/twisted.internet.asyncioreactor.html + +#### Will you support Python 3.5 or earlier? + +There are no immediate plans to support Python 3.5, but you are welcome to +contribute to the project. + +Here are some of the steps required to accomplish this: + +- Source code transformation to rewrite variable annotations to comments for example, the code: + +```python +class Point: + x: int = 0 + y: int = 0 +``` +must be rewritten into: + +```python +class Point: + x = 0 # type: int + y = 0 # type: int +``` + +- Source code transformation to rewrite async functions for example, the code: + +```python +async def foo(): + await asyncio.sleep(1.0) +``` + +must be rewritten into: + +```python +@coroutine +def foo(): + yield from asyncio.sleep(1.0) +``` + +#### Will you support Python 2? + +There are no plans to support Python 2, but you are welcome to contribute to +the project (details in question above is relevant also for Python 2). + + +### At Shutdown I get lots of warnings, what is this about? + +If you get warnings such as this at shutdown: + +```log +Task was destroyed but it is pending! +task: wait_for=()]>> +Task was destroyed but it is pending! +task: wait_for=()]>> +Task was destroyed but it is pending! +task: wait_for=()]>> +Task was destroyed but it is pending! +task: cb=[_release_waiter(()]>)() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:316]> +Task was destroyed but it is pending! + task: cb=[_release_waiter(()]>)() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:316]> +``` + +It usually means you forgot to stop a service before the process exited. + +## Code of Conduct + +Everyone interacting in the project's codebases, issue trackers, chat rooms, +and mailing lists is expected to follow the Mode Code of Conduct. + +As contributors and maintainers of these projects, and in the interest of fostering +an open and welcoming community, we pledge to respect all people who contribute +through reporting issues, posting feature requests, updating documentation, +submitting pull requests or patches, and other activities. + +We are committed to making participation in these projects a harassment-free +experience for everyone, regardless of level of experience, gender, +gender identity and expression, sexual orientation, disability, +personal appearance, body size, race, ethnicity, age, +religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical + or electronic addresses, without explicit permission +* Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct. By adopting this Code of Conduct, +project maintainers commit themselves to fairly and consistently applying +these principles to every aspect of managing this project. Project maintainers +who do not follow or enforce the Code of Conduct may be permanently removed from +the project team. + +This code of conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by opening an issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the Contributor Covenant, +version 1.2.0 available at http://contributor-covenant.org/version/1/2/0/. diff --git a/README.rst b/README.rst deleted file mode 100644 index b7ec20c5..00000000 --- a/README.rst +++ /dev/null @@ -1,534 +0,0 @@ -===================================================================== - AsyncIO Services Fork -===================================================================== - -|release| |coverage| |license| |wheel| |pyversion| - -:Web: https://faust-streaming.github.io/mode/ -:Download: https://pypi.org/project/mode-streaming -:Source: https://github.com/faust-streaming/mode -:Keywords: async, service, framework, actors, bootsteps, graph - - -Why the fork -============ - -We have decided to fork the original *Mode* project because there is a critical process of releasing new versions which causes uncertainty in the community. Everybody is welcome to contribute to this *fork*, and you can be added as a maintainer. - -We want to: - -- Ensure continues release -- Code quality -- Support latest Python versions -- Update the documentation - -and more... - -What is Mode? -============= - -Mode is a very minimal Python library built-on top of AsyncIO that makes -it much easier to use. - -In Mode your program is built out of services that you can start, stop, -restart and supervise. - -A service is just a class: - -.. code-block:: python - - class PageViewCache(Service): - redis: Redis = None - - async def on_start(self) -> None: - self.redis = connect_to_redis() - - async def update(self, url: str, n: int = 1) -> int: - return await self.redis.incr(url, n) - - async def get(self, url: str) -> int: - return await self.redis.get(url) - - -Services are started, stopped and restarted and have -callbacks for those actions. - -It can start another service: - -.. code-block:: python - - class App(Service): - page_view_cache: PageViewCache = None - - async def on_start(self) -> None: - await self.add_runtime_dependency(self.page_view_cache) - - @cached_property - def page_view_cache(self) -> PageViewCache: - return PageViewCache() - -It can include background tasks: - -.. code-block:: python - - class PageViewCache(Service): - - @Service.timer(1.0) - async def _update_cache(self) -> None: - self.data = await cache.get('key') - -Services that depends on other services actually form a graph -that you can visualize. - -Worker - Mode optionally provides a worker that you can use to start the program, - with support for logging, blocking detection, remote debugging and more. - - To start a worker add this to your program: - - .. code-block:: python - - if __name__ == '__main__': - from mode import Worker - Worker(Service(), loglevel="info").execute_from_commandline() - - Then execute your program to start the worker: - - .. code-block:: console - - $ python examples/tutorial.py - [2018-03-27 15:47:12,159: INFO]: [^Worker]: Starting... - [2018-03-27 15:47:12,160: INFO]: [^-AppService]: Starting... - [2018-03-27 15:47:12,160: INFO]: [^--Websockets]: Starting... - STARTING WEBSOCKET SERVER - [2018-03-27 15:47:12,161: INFO]: [^--UserCache]: Starting... - [2018-03-27 15:47:12,161: INFO]: [^--Webserver]: Starting... - [2018-03-27 15:47:12,164: INFO]: [^--Webserver]: Serving on port 8000 - REMOVING EXPIRED USERS - REMOVING EXPIRED USERS - - To stop it hit ``Control-c``: - - .. code-block:: console - - [2018-03-27 15:55:08,084: INFO]: [^Worker]: Stopping on signal received... - [2018-03-27 15:55:08,084: INFO]: [^Worker]: Stopping... - [2018-03-27 15:55:08,084: INFO]: [^-AppService]: Stopping... - [2018-03-27 15:55:08,084: INFO]: [^--UserCache]: Stopping... - REMOVING EXPIRED USERS - [2018-03-27 15:55:08,085: INFO]: [^Worker]: Gathering service tasks... - [2018-03-27 15:55:08,085: INFO]: [^--UserCache]: -Stopped! - [2018-03-27 15:55:08,085: INFO]: [^--Webserver]: Stopping... - [2018-03-27 15:55:08,085: INFO]: [^Worker]: Gathering all futures... - [2018-03-27 15:55:08,085: INFO]: [^--Webserver]: Closing server - [2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Waiting for server to close handle - [2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Shutting down web application - [2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Waiting for handler to shut down - [2018-03-27 15:55:08,086: INFO]: [^--Webserver]: Cleanup - [2018-03-27 15:55:08,086: INFO]: [^--Webserver]: -Stopped! - [2018-03-27 15:55:08,086: INFO]: [^--Websockets]: Stopping... - [2018-03-27 15:55:08,086: INFO]: [^--Websockets]: -Stopped! - [2018-03-27 15:55:08,087: INFO]: [^-AppService]: -Stopped! - [2018-03-27 15:55:08,087: INFO]: [^Worker]: -Stopped! - -Beacons - The ``beacon`` object that we pass to services keeps track of the services - in a graph. - - They are not stricly required, but can be used to visualize a running - system, for example we can render it as a pretty graph. - - This requires you to have the ``pydot`` library and GraphViz - installed: - - .. code-block:: console - - $ pip install pydot - - Let's change the app service class to dump the graph to an image - at startup: - - .. code-block:: python - - class AppService(Service): - - async def on_start(self) -> None: - print('APP STARTING') - import pydot - import io - o = io.StringIO() - beacon = self.app.beacon.root or self.app.beacon - beacon.as_graph().to_dot(o) - graph, = pydot.graph_from_dot_data(o.getvalue()) - print('WRITING GRAPH TO image.png') - with open('image.png', 'wb') as fh: - fh.write(graph.create_png()) - - -Creating a Service -================== - -To define a service, simply subclass and fill in the methods -to do stuff as the service is started/stopped etc.: - - -.. code-block:: python - - class MyService(Service): - - async def on_start(self) -> None: - print('Im starting now') - - async def on_started(self) -> None: - print('Im ready') - - async def on_stop(self) -> None: - print('Im stopping now') - -To start the service, call ``await service.start()``: - -.. code-block:: python - - await service.start() - -Or you can use ``mode.Worker`` (or a subclass of this) to start your -services-based asyncio program from the console: - -.. code-block:: python - - if __name__ == '__main__': - import mode - worker = mode.Worker( - MyService(), - loglevel='INFO', - logfile=None, - daemon=False, - ) - worker.execute_from_commandline() - -It's a Graph! -============= - -Services can start other services, coroutines, and background tasks. - -1) Starting other services using ``add_depenency``: - -.. code-block:: python - - class MyService(Service): - - def __post_init__(self) -> None: - self.add_dependency(OtherService(loop=self.loop)) - -2) Start a list of services using ``on_init_dependencies``: - -.. code-block:: python - - class MyService(Service): - - def on_init_dependencies(self) -> None: - return [ - ServiceA(loop=self.loop), - ServiceB(loop=self.loop), - ServiceC(loop=self.loop), - ] - -3) Start a future/coroutine (that will be waited on to complete on stop): - -.. code-block:: python - - class MyService(Service): - - async def on_start(self) -> None: - self.add_future(self.my_coro()) - - async def my_coro(self) -> None: - print('Executing coroutine') - -4) Start a background task: - -.. code-block:: python - - class MyService(Service): - - @Service.task - async def _my_coro(self) -> None: - print('Executing coroutine') - - -5) Start a background task that keeps running: - -.. code-block:: python - - class MyService(Service): - - @Service.task - async def _my_coro(self) -> None: - while not self.should_stop: - # NOTE: self.sleep will wait for one second, or - # until service stopped/crashed. - await self.sleep(1.0) - print('Background thread waking up') - -.. _installation: - -Installation -============ - -You can install Mode either via the Python Package Index (PyPI) -or from source. - -To install using `pip`: - -.. code-block:: console - - $ pip install -U mode-streaming - -.. _installing-from-source: - -Downloading and installing from source --------------------------------------- - -Download the latest version of Mode from -http://pypi.org/project/mode-streaming - -You can install it by doing the following: - - .. code-block:: console - - $ tar xvfz mode-streaming-0.2.1.tar.gz - $ cd mode-0.2.1 - $ python build - # python install - -The last command must be executed as a privileged user if -you are not currently using a virtualenv. - -.. _installing-from-git: - -Using the development version ------------------------------ - -With pip -~~~~~~~~ - -You can install the latest snapshot of Mode using the following -pip command: - -.. code-block:: console - - $ pip install mode-streaming - -FAQ -=== - -Can I use Mode with Django/Flask/etc.? --------------------------------------- - -Yes! Use gevent/eventlet as a bridge to integrate with asyncio. - -Using ``gevent`` -~~~~~~~~~~~~~~~~ - -This works with any blocking Python library that can work with gevent. - -Using gevent requires you to install the ``aiogevent`` module, -and you can install this as a bundle with Mode: - -.. code-block:: console - - $ pip install -U mode-streaming[gevent] - -Then to actually use gevent as the event loop you have to -execute the following in your entrypoint module (usually where you -start the worker), before any other third party libraries are imported: - -.. code-block:: python - - #!/usr/bin/env python3 - import mode.loop - mode.loop.use('gevent') - # execute program - -REMEMBER: This must be located at the very top of the module, -in such a way that it executes before you import other libraries. - - -Using ``eventlet`` -~~~~~~~~~~~~~~~~~~ - -This works with any blocking Python library that can work with eventlet. - -Using eventlet requires you to install the ``aioeventlet`` module, -and you can install this as a bundle with Mode: - -.. code-block:: console - - $ pip install -U mode-streaming[eventlet] - -Then to actually use eventlet as the event loop you have to -execute the following in your entrypoint module (usually where you -start the worker), before any other third party libraries are imported: - -.. code-block:: python - - #!/usr/bin/env python3 - import mode.loop - mode.loop.use('eventlet') - # execute program - -REMEMBER: It's very important this is at the very top of the module, -and that it executes before you import libraries. - -Can I use Mode with Tornado? ----------------------------- - -Yes! Use the ``tornado.platform.asyncio`` bridge: -http://www.tornadoweb.org/en/stable/asyncio.html - -Can I use Mode with Twisted? ------------------------------ - -Yes! Use the asyncio reactor implementation: -https://twistedmatrix.com/documents/17.1.0/api/twisted.internet.asyncioreactor.html - -Will you support Python 3.5 or earlier? ---------------------------------------- - -There are no immediate plans to support Python 3.5, but you are welcome to -contribute to the project. - -Here are some of the steps required to accomplish this: - -- Source code transformation to rewrite variable annotations to comments - - for example, the code: - - .. code-block:: python - - class Point: - x: int = 0 - y: int = 0 - - must be rewritten into: - - .. code-block:: python - - class Point: - x = 0 # type: int - y = 0 # type: int - -- Source code transformation to rewrite async functions - - for example, the code: - - .. code-block:: python - - async def foo(): - await asyncio.sleep(1.0) - - must be rewritten into: - - .. code-block:: python - - @coroutine - def foo(): - yield from asyncio.sleep(1.0) - -Will you support Python 2? --------------------------- - -There are no plans to support Python 2, but you are welcome to contribute to -the project (details in question above is relevant also for Python 2). - - -At Shutdown I get lots of warnings, what is this about? -------------------------------------------------------- - -If you get warnings such as this at shutdown: - -.. code-block:: text - - Task was destroyed but it is pending! - task: wait_for=()]>> - Task was destroyed but it is pending! - task: wait_for=()]>> - Task was destroyed but it is pending! - task: wait_for=()]>> - Task was destroyed but it is pending! - task: cb=[_release_waiter(()]>)() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:316]> - Task was destroyed but it is pending! - task: cb=[_release_waiter(()]>)() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:316]> - -It usually means you forgot to stop a service before the process exited. - -Code of Conduct -=============== - -Everyone interacting in the project's codebases, issue trackers, chat rooms, -and mailing lists is expected to follow the Mode Code of Conduct. - -As contributors and maintainers of these projects, and in the interest of fostering -an open and welcoming community, we pledge to respect all people who contribute -through reporting issues, posting feature requests, updating documentation, -submitting pull requests or patches, and other activities. - -We are committed to making participation in these projects a harassment-free -experience for everyone, regardless of level of experience, gender, -gender identity and expression, sexual orientation, disability, -personal appearance, body size, race, ethnicity, age, -religion, or nationality. - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing other's private information, such as physical - or electronic addresses, without explicit permission -* Other unethical or unprofessional conduct. - -Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct. By adopting this Code of Conduct, -project maintainers commit themselves to fairly and consistently applying -these principles to every aspect of managing this project. Project maintainers -who do not follow or enforce the Code of Conduct may be permanently removed from -the project team. - -This code of conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by opening an issue or contacting one or more of the project maintainers. - -This Code of Conduct is adapted from the Contributor Covenant, -version 1.2.0 available at http://contributor-covenant.org/version/1/2/0/. - -.. |build-status| image:: https://travis-ci.com/faust-streaming/mode.png?branch=master - :alt: Build status - :target: https://travis-ci.com/faust-streaming/mode - -.. |coverage| image:: https://codecov.io/github/faust-streaming/mode/coverage.svg?branch=master - :target: https://codecov.io/github/faust-streaming/mode?branch=master - -.. |license| image:: https://img.shields.io/pypi/l/mode-streaming.svg - :alt: BSD License - :target: https://opensource.org/licenses/BSD-3-Clause - -.. |wheel| image:: https://img.shields.io/pypi/wheel/mode-streaming.svg - :alt: Mode can be installed via wheel - :target: http://pypi.org/project/mode-streaming/ - -.. |pyversion| image:: https://img.shields.io/pypi/pyversions/mode-streaming.svg - :alt: Supported Python versions. - :target: http://pypi.org/project/mode-streaming/ - -.. |pyimp| image:: https://img.shields.io/pypi/implementation/mode-streaming.svg - :alt: Supported Python implementations. - :target: http://pypi.org/project/mode-streaming/ - -.. |release| image:: https://img.shields.io/pypi/v/mode-streaming.svg - :alt: Latest release - :target: https://pypi.python.org/pypi/mode-streaming/ diff --git a/mode/loop/gevent.py b/mode/loop/gevent.py index fce643ae..057a4da6 100644 --- a/mode/loop/gevent.py +++ b/mode/loop/gevent.py @@ -31,7 +31,7 @@ psycogreen.gevent.patch_psycopg() try: - import aiogevent + import asyncio_gevent except ImportError: raise raise ImportError( @@ -43,13 +43,13 @@ raise RuntimeError("Event loop created before importing gevent loop!") -class Policy(aiogevent.EventLoopPolicy): # type: ignore +class Policy(asyncio_gevent.EventLoopPolicy): # type: ignore """Custom gevent event loop policy.""" _loop: Optional[asyncio.AbstractEventLoop] = None def get_event_loop(self) -> asyncio.AbstractEventLoop: - # aiogevent raises an error here current_thread() is not MainThread, + # asyncio_gevent raises an error here current_thread() is not MainThread, # but gevent monkey patches current_thread, so it's not a good check. loop = self._loop if loop is None: diff --git a/mode/utils/times.py b/mode/utils/times.py index d22341a4..fce228f9 100644 --- a/mode/utils/times.py +++ b/mode/utils/times.py @@ -283,9 +283,7 @@ def humanize_seconds( for unit, divider, formatter in TIME_UNITS: if secs >= divider: w = secs / float(divider) - return "{}{}{} {}{}".format( - prefix, sep, formatter(w), pluralize(int(w), unit), suffix - ) + return f"{prefix}{sep}{formatter(w)} {pluralize(int(w), unit)}{suffix}" if microseconds and secs > 0.0: return f"{prefix}{sep}{secs:.2f} seconds{suffix}" return now diff --git a/mode/utils/tracebacks.py b/mode/utils/tracebacks.py index b12067b3..eccaa3e7 100644 --- a/mode/utils/tracebacks.py +++ b/mode/utils/tracebacks.py @@ -273,9 +273,7 @@ def _get_coroutine_frame( @classmethod def _what_is_this(cls, obj: Any) -> AttributeError: return AttributeError( - "WHAT IS THIS? str={} repr={!r} typ={!r} dir={}".format( - obj, obj, type(obj), dir(obj) - ) + f"WHAT IS THIS? str={obj} repr={obj!r} typ={type(obj)!r} dir={dir(obj)}" ) @classmethod diff --git a/pyproject.toml b/pyproject.toml index 07266c45..a205260c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "mode-streaming" +version = "0.3.5" description = "AsyncIO Service-based programming" readme = "README.rst" requires-python = ">=3.8" @@ -37,7 +38,6 @@ dependencies = [ "croniter>=2.0.0,<3.0.0", "mypy_extensions", ] -dynamic = ["version"] [project.optional-dependencies] eventlet = [ @@ -45,7 +45,7 @@ eventlet = [ "dnspython", ] gevent = [ - "aiogevent~=0.2", + "asyncio-gevent~=0.2", ] uvloop = [ "uvloop>=0.19.0", @@ -211,3 +211,6 @@ skip-magic-trailing-comma = true quote-style = "double" indent-style = "space" line-ending = "auto" + +[tool.commitizen] +version_provider = "pep621" diff --git a/requirements-docs.txt b/requirements-docs.txt index 2a5a7446..a3cca4e4 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,3 +1,3 @@ mkdocs>=1.5.3 mkdocs-material>=9.5.13 -mkdocstrings>=0.24.1 +mkdocstrings[python]>=0.24.1 diff --git a/requirements-tests.txt b/requirements-tests.txt index ac161e9c..370fe4d2 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,7 +1,6 @@ freezegun>=0.3.11 hypothesis>=3.31 mypy>=1.8.0 -pre-commit>=3.6.2 pytest-aiofiles>=0.2.0 pytest-asyncio==0.21.1 pytest-base-url>=2.1.0 @@ -15,3 +14,7 @@ pytz ruff>=0.3.0 vulture yarl + +# Conditional +pre-commit>=3.6.2; python_version >= '3.9' +pre-commit>=3.5.0; python_version < '3.9' diff --git a/requirements.txt b/requirements.txt index 39b931ef..9b5b17ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ --e .[eventlet] +-e . -r requirements-tests.txt -r requirements-docs.txt diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 2ba18d56..00000000 --- a/scripts/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Development Scripts - -* `scripts/test` - Run the test suite. -* `scripts/format` - Run the automated code linting/formatting tools. -* `scripts/check` - Run the code linting, checking that it passes. -* `scripts/build` - Build source and wheel packages. -* `scripts/publish` - Publish the latest version to PyPI. - -Styled after GitHub's ["Scripts to Rule Them All"](https://github.com/github/scripts-to-rule-them-all). diff --git a/scripts/build b/scripts/build deleted file mode 100755 index 5d5e71a1..00000000 --- a/scripts/build +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -e - -if [ -d 'venv' ] ; then - PREFIX="venv/bin/" -else - PREFIX="" -fi - -set -x - -${PREFIX}python -m build -${PREFIX}twine check dist/* -# ${PREFIX}mkdocs build diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh new file mode 100755 index 00000000..f2a8f681 --- /dev/null +++ b/scripts/build-docs.sh @@ -0,0 +1,5 @@ +#!/bin/sh -e + +set -x + +mkdocs build diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 00000000..43c74ec5 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,6 @@ +#!/bin/sh -e + +set -x + +python3 -m build . --wheel +twine check dist/* diff --git a/scripts/bump.sh b/scripts/bump.sh new file mode 100755 index 00000000..dedda4e8 --- /dev/null +++ b/scripts/bump.sh @@ -0,0 +1,5 @@ +#!/bin/sh -e + +set -x + +cz bump --changelog diff --git a/scripts/check b/scripts/check deleted file mode 100755 index 67409901..00000000 --- a/scripts/check +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -e - -export PREFIX="" -if [ -d 'venv' ] ; then - export PREFIX="venv/bin/" -fi -export SOURCE_FILES="mode tests" - -set -x - -${PREFIX}ruff check $SOURCE_FILES --diff -${PREFIX}ruff format $SOURCE_FILES --check --diff -# ${PREFIX}mypy $SOURCE_FILES diff --git a/scripts/clean b/scripts/clean.sh similarity index 100% rename from scripts/clean rename to scripts/clean.sh diff --git a/scripts/docs b/scripts/docs deleted file mode 100755 index 4ac3beb7..00000000 --- a/scripts/docs +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -e - -export PREFIX="" -if [ -d 'venv' ] ; then - export PREFIX="venv/bin/" -fi - -set -x - -${PREFIX}mkdocs serve diff --git a/scripts/format b/scripts/format deleted file mode 100755 index f305fc43..00000000 --- a/scripts/format +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -e - -export PREFIX="" -if [ -d 'venv' ] ; then - export PREFIX="venv/bin/" -fi - -set -x - -${PREFIX}ruff format mode tests -${PREFIX}ruff check mode tests --fix diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100755 index 00000000..d0a3f36f --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,6 @@ +#!/bin/sh -e + +set -x + +ruff format mode tests +ruff check mode tests --fix diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 00000000..58b5dc05 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +set -x + +# mypy mode +ruff check mode tests +ruff format mode tests --check diff --git a/scripts/publish b/scripts/publish deleted file mode 100755 index 0cf25fb8..00000000 --- a/scripts/publish +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -e - -VERSION_FILE="mode/__init__.py" - -if [ -d 'venv' ] ; then - PREFIX="venv/bin/" -else - PREFIX="" -fi - -VERSION=`grep __version__ ${VERSION_FILE} | grep -o '[0-9][^"]*'` - -set -x - -${PREFIX}twine upload dist/* -# ${PREFIX}mkdocs gh-deploy --force - -git tag -a v${VERSION} -m "release v${VERSION}" -git push origin v${VERSION} diff --git a/scripts/test b/scripts/test deleted file mode 100755 index e2fdcb29..00000000 --- a/scripts/test +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -export PREFIX="" -if [ -d 'venv' ] ; then - export PREFIX="venv/bin/" -fi - -set -ex - -if [ -z $GITHUB_ACTIONS ]; then - scripts/check -fi - -${PREFIX}pytest tests/unit tests/functional --cov=mode diff --git a/scripts/tests.sh b/scripts/tests.sh new file mode 100755 index 00000000..5adcdec5 --- /dev/null +++ b/scripts/tests.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e +set -x + +pytest tests --cov=mode From 57e06f47c9e50ac2f1fadc1dcb62a1462e541003 Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Tue, 12 Mar 2024 13:39:18 +0100 Subject: [PATCH 06/16] build: bump pypy to 3.10 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0a5bda48..5bff407c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: python-version: - - "pypy3.9" + - "pypy3.10" - "3.8" - "3.9" - "3.10" From 0a84305444ac338e143677c836cea81847bc1730 Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Tue, 12 Mar 2024 14:08:58 +0100 Subject: [PATCH 07/16] docs(mkdocs): add file import via snippet --- docs/changelog.md | 1 + docs/contributing.md | 1 + docs/example-service.md | 3 +++ docs/example-webapp.md | 3 +++ examples/{tutorial.py => webapp.py} | 0 mkdocs.yml | 10 ++++++++-- 6 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 docs/changelog.md create mode 100644 docs/contributing.md create mode 100644 docs/example-service.md create mode 100644 docs/example-webapp.md rename examples/{tutorial.py => webapp.py} (100%) diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..786b75d5 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +--8<-- "CHANGELOG.md" diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..ea38c9bf --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1 @@ +--8<-- "CONTRIBUTING.md" diff --git a/docs/example-service.md b/docs/example-service.md new file mode 100644 index 00000000..876b4044 --- /dev/null +++ b/docs/example-service.md @@ -0,0 +1,3 @@ +```python title="Example Service" +--8<-- "examples/service.py" +``` diff --git a/docs/example-webapp.md b/docs/example-webapp.md new file mode 100644 index 00000000..694e64d0 --- /dev/null +++ b/docs/example-webapp.md @@ -0,0 +1,3 @@ +```python title="Example Webapp" +--8<-- "examples/webapp.py" +``` diff --git a/examples/tutorial.py b/examples/webapp.py similarity index 100% rename from examples/tutorial.py rename to examples/webapp.py diff --git a/mkdocs.yml b/mkdocs.yml index 28c34726..84d2b1f6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,8 +32,13 @@ repo_url: https://github.com/faust-streaming/mode nav: - Mode: - - Introduction: 'index.md' - - Creating a Service: 'creating-service.md' + - Introduction: index.md + - Creating a Service: creating-service.md + - Examples: + - Service: example-service.md + - Web app: example-webapp.md + - Developing: + - Contributing Guide: contributing.md - References: - 'Mode': - mode.services: references/mode.services.md @@ -78,6 +83,7 @@ nav: - mode.utils.trees: references/mode.utils.trees.md - mode.utils.types.graphs: references/mode.utils.types.graphs.md - mode.utils.types.trees: references/mode.utils.types.trees.md + - Changelog: changelog.md markdown_extensions: - pymdownx.highlight - pymdownx.inlinehilite From 5b0364d1d91f61ea020c1d27d75b3a4a450d5617 Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Wed, 13 Mar 2024 20:31:10 +0100 Subject: [PATCH 08/16] fix: restore install.sh and see if github pipeline pass --- .github/workflows/tests.yml | 5 +++-- scripts/install.sh | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100755 scripts/install.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5bff407c..1112b00e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,11 +41,12 @@ jobs: python-version: ${{ matrix.python-version }} cache: pip cache-dependency-path: | - requirements-*.txt + requirements-docs.txt + requirements-tests.txt pyproject.toml - name: Install dependencies - run: pip install -r requirements.txt + run: scripts/install.sh - name: Run linting checks run: scripts/lint.sh diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 00000000..880d6802 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,17 @@ +#!/bin/sh -e + +# Use the Python executable provided from the `-p` option, or a default. +[ "$1" = "-p" ] && PYTHON=$2 || PYTHON="python3" + +VENV="venv" + +set -x + +if [ -z "$GITHUB_ACTIONS" ]; then + "$PYTHON" -m venv "$VENV" + PIP="$VENV/bin/pip" +else + PIP="pip" +fi + +"$PIP" install -r requirements.txt From 8cfc8759468852dc8db7dce985b9d4815adc7bc0 Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Wed, 13 Mar 2024 20:43:50 +0100 Subject: [PATCH 09/16] fix: rollback install as it changed nothing --- .github/workflows/tests.yml | 2 +- examples/webapp.py | 2 +- pyproject.toml | 7 +++++-- scripts/install.sh | 17 ----------------- 4 files changed, 7 insertions(+), 21 deletions(-) delete mode 100755 scripts/install.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1112b00e..198764f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,7 +46,7 @@ jobs: pyproject.toml - name: Install dependencies - run: scripts/install.sh + run: pip install -r requirements.txt - name: Run linting checks run: scripts/lint.sh diff --git a/examples/webapp.py b/examples/webapp.py index d9d4803f..889e088c 100644 --- a/examples/webapp.py +++ b/examples/webapp.py @@ -1,4 +1,4 @@ -# This is code for the tutorial in README.rst +# This is code for the tutorial in README.md from typing import Any, List, MutableMapping from aiohttp.web import Application diff --git a/pyproject.toml b/pyproject.toml index a205260c..7d2f0ce8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,18 @@ [build-system] -requires = ["setuptools>=61.0"] +requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools.package-data] "modeStreaming" = ["py.typed"] +[tool.distutils.bdist_wheel] +universal = true + [project] name = "mode-streaming" version = "0.3.5" description = "AsyncIO Service-based programming" -readme = "README.rst" +readme = "README.md" requires-python = ">=3.8" keywords = ["asyncio", "service", "bootsteps", "graph", "coroutine"] authors = [ diff --git a/scripts/install.sh b/scripts/install.sh deleted file mode 100755 index 880d6802..00000000 --- a/scripts/install.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -e - -# Use the Python executable provided from the `-p` option, or a default. -[ "$1" = "-p" ] && PYTHON=$2 || PYTHON="python3" - -VENV="venv" - -set -x - -if [ -z "$GITHUB_ACTIONS" ]; then - "$PYTHON" -m venv "$VENV" - PIP="$VENV/bin/pip" -else - PIP="pip" -fi - -"$PIP" install -r requirements.txt From d45500d9a01e5e150ad119e34d87a6bc7e888db7 Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Wed, 13 Mar 2024 20:51:52 +0100 Subject: [PATCH 10/16] fix: rollback github action --- .github/workflows/tests.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 198764f5..1ea21397 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,15 +31,13 @@ jobs: experimental: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - uses: "actions/setup-python@v4" with: - python-version: ${{ matrix.python-version }} - cache: pip + python-version: "${{ matrix.python-version }}" + cache: "pip" cache-dependency-path: | requirements-docs.txt requirements-tests.txt From 9319c71e4a6103a1b4a519edfef49e68811b0a34 Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Thu, 14 Mar 2024 12:39:09 -0400 Subject: [PATCH 11/16] Install self in tests.yml and test PyPy 3.9 as well --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1ea21397..6db8cd16 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,6 +19,7 @@ jobs: fail-fast: false matrix: python-version: + - "pypy3.9" - "pypy3.10" - "3.8" - "3.9" @@ -46,6 +47,9 @@ jobs: - name: Install dependencies run: pip install -r requirements.txt + - name: Install for testing + run: pip install -e . + - name: Run linting checks run: scripts/lint.sh From d721b129a7f86cdd61a9e5bb0a3bbb998c134f50 Mon Sep 17 00:00:00 2001 From: William Barnhart Date: Thu, 14 Mar 2024 12:43:07 -0400 Subject: [PATCH 12/16] Revert pip -e invocation in tests.yml lol --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6db8cd16..5e917eb4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,9 +47,6 @@ jobs: - name: Install dependencies run: pip install -r requirements.txt - - name: Install for testing - run: pip install -e . - - name: Run linting checks run: scripts/lint.sh From 6e653dc027630daad21285b232775e64d3846b07 Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Sun, 17 Mar 2024 11:30:24 +0100 Subject: [PATCH 13/16] fix: restore coveragerc --- .coveragerc | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..8dab3b2e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,35 @@ +[run] +branch = 1 +cover_pylib = 0 +include=*mode/* +omit = tests.* + +[report] +omit = + */python?.?/* + */site-packages/* + */pypy/* + + # tested by functional tests + */mode/loop/* + + # not needed + */mode/types/* + */mode/utils/types/* + */mode/utils/mocks.py + + # been in celery since forever + */mode/utils/graphs/* +exclude_lines = + # Have to re-enable the standard pragma + if\ typing\.TYPE_CHECKING\: + + pragma: no cover + + if sys.platform == 'win32': + + \@abc\.abstractmethod + + \# Py3\.6 + + \@overload From 0f97c819d0224b53040794372ec1c5fcccf83d0c Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Sun, 17 Mar 2024 11:59:52 +0100 Subject: [PATCH 14/16] fix: pytest declare specific dir instead of mean directory --- .coveragerc | 35 ----------------------------------- scripts/tests.sh | 2 +- 2 files changed, 1 insertion(+), 36 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 8dab3b2e..00000000 --- a/.coveragerc +++ /dev/null @@ -1,35 +0,0 @@ -[run] -branch = 1 -cover_pylib = 0 -include=*mode/* -omit = tests.* - -[report] -omit = - */python?.?/* - */site-packages/* - */pypy/* - - # tested by functional tests - */mode/loop/* - - # not needed - */mode/types/* - */mode/utils/types/* - */mode/utils/mocks.py - - # been in celery since forever - */mode/utils/graphs/* -exclude_lines = - # Have to re-enable the standard pragma - if\ typing\.TYPE_CHECKING\: - - pragma: no cover - - if sys.platform == 'win32': - - \@abc\.abstractmethod - - \# Py3\.6 - - \@overload diff --git a/scripts/tests.sh b/scripts/tests.sh index 5adcdec5..751fc771 100755 --- a/scripts/tests.sh +++ b/scripts/tests.sh @@ -3,4 +3,4 @@ set -e set -x -pytest tests --cov=mode +pytest tests/unit tests/functional --cov=mode From 22735672a5837d5bc38c2e74157f77e710060714 Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Sun, 17 Mar 2024 12:02:17 +0100 Subject: [PATCH 15/16] fix: pytest declare specific dir instead of mean directory --- scripts/tests.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/tests.sh b/scripts/tests.sh index 751fc771..7fa7e9b5 100755 --- a/scripts/tests.sh +++ b/scripts/tests.sh @@ -1,6 +1,10 @@ -#!/usr/bin/env bash +#!/bin/sh -set -e -set -x +export PREFIX="" +if [ -d 'venv' ] ; then + export PREFIX="venv/bin/" +fi -pytest tests/unit tests/functional --cov=mode +set -ex + +${PREFIX}pytest tests/unit tests/functional --cov=mode From 5e4bb4cad04434d08787881610032b8c344d9eca Mon Sep 17 00:00:00 2001 From: Clovis Kyndt Date: Sun, 17 Mar 2024 12:05:11 +0100 Subject: [PATCH 16/16] fix: pytest declare specific dir instead of mean directory --- scripts/tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tests.sh b/scripts/tests.sh index 7fa7e9b5..c8cfba2c 100755 --- a/scripts/tests.sh +++ b/scripts/tests.sh @@ -7,4 +7,4 @@ fi set -ex -${PREFIX}pytest tests/unit tests/functional --cov=mode +${PREFIX}pytest tests/unit tests/functional