├── requirements.txt ├── .coveragerc ├── docs ├── source │ ├── modules.rst │ ├── octobot_channels.util.rst │ ├── index.rst │ ├── octobot_channels.channels.rst │ ├── octobot_channels.rst │ └── conf.py ├── Makefile └── make.bat ├── MANIFEST.in ├── dev_requirements.txt ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .readthedocs.yml ├── async_channel ├── constants.py ├── enums.py ├── util │ ├── __init__.py │ ├── logging_util.py │ └── channel_creator.py ├── channels │ ├── __init__.py │ ├── channel_instances.py │ └── channel.py ├── __init__.py ├── producer.py └── consumer.py ├── .gitignore ├── setup.py ├── tests ├── __init__.py ├── test_channel_creator.py ├── test_consumer.py ├── test_channel_instances.py ├── test_producer.py ├── test_synchronized.py └── test_channel.py ├── README.md ├── LICENSE ├── CHANGELOG.md └── standard.rc /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | tests/* 4 | setup.py 5 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | octobot_channels 2 | ================ 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | octobot_channels 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include async_channel *.pxd 2 | 3 | include README.md 4 | include LICENSE 5 | include CHANGELOG.md 6 | include requirements.txt 7 | 8 | global-exclude *.c 9 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=7.1 2 | pytest-asyncio>=0.19 3 | pytest-pep8 4 | pytest-cov 5 | pytest-asyncio 6 | pytest-xdist 7 | 8 | mock>=4.0.2 9 | 10 | coverage 11 | coveralls 12 | 13 | twine 14 | pip 15 | setuptools 16 | wheel 17 | 18 | pur 19 | 20 | sphinx==3.2.1 21 | sphinx_rtd_theme 22 | 23 | pylint 24 | black==23.3.0 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 10 8 | allow: 9 | - dependency-name: "OctoBot*" 10 | - dependency-name: "cython" 11 | reviewers: 12 | - Herklos 13 | - GuillaumeDSM 14 | assignees: 15 | - Herklos 16 | - GuillaumeDSM 17 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | # Build documentation in the docs/ directory with Sphinx 4 | sphinx: 5 | configuration: docs/source/conf.py 6 | 7 | # Optionally build your docs in additional formats such as PDF and ePub 8 | formats: all 9 | 10 | # Optionally set the version of Python and requirements required to build your docs 11 | python: 12 | version: 3.8 13 | install: 14 | - requirements: requirements.txt 15 | - requirements: dev_requirements.txt 16 | -------------------------------------------------------------------------------- /docs/source/octobot_channels.util.rst: -------------------------------------------------------------------------------- 1 | octobot\_channels.util package 2 | ============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | octobot\_channels.util.channel\_creator module 8 | ---------------------------------------------- 9 | 10 | .. automodule:: octobot_channels.util.channel_creator 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: octobot_channels.util 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. OctoBot-Channels documentation master file, created by 2 | sphinx-quickstart on Tue Mar 24 00:38:18 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to OctoBot-Channels's documentation! 7 | ============================================ 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/source/octobot_channels.channels.rst: -------------------------------------------------------------------------------- 1 | octobot\_channels.channels package 2 | ================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | octobot\_channels.channels.channel module 8 | ----------------------------------------- 9 | 10 | .. automodule:: octobot_channels.channels.channel 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | octobot\_channels.channels.channel\_instances module 16 | ---------------------------------------------------- 17 | 18 | .. automodule:: octobot_channels.channels.channel_instances 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: octobot_channels.channels 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /async_channel/constants.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | """ 17 | Define async_channel global constants 18 | """ 19 | CHANNEL_WILDCARD = "*" 20 | 21 | DEFAULT_QUEUE_SIZE = 0 # unlimited 22 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/octobot_channels.rst: -------------------------------------------------------------------------------- 1 | octobot\_channels package 2 | ========================= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | octobot_channels.channels 10 | octobot_channels.util 11 | 12 | Submodules 13 | ---------- 14 | 15 | octobot\_channels.constants module 16 | ---------------------------------- 17 | 18 | .. automodule:: octobot_channels.constants 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | octobot\_channels.consumer module 24 | --------------------------------- 25 | 26 | .. automodule:: octobot_channels.consumer 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | octobot\_channels.producer module 32 | --------------------------------- 33 | 34 | .. automodule:: octobot_channels.producer 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | Module contents 41 | --------------- 42 | 43 | .. automodule:: octobot_channels 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /async_channel/enums.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | """ 17 | Define async_channel global enums 18 | """ 19 | 20 | import enum 21 | 22 | 23 | class ChannelConsumerPriorityLevels(enum.Enum): 24 | """ 25 | Channel consumer priority levels 26 | """ 27 | 28 | HIGH = 0 29 | MEDIUM = 1 30 | # LOW = 2 not necessary for now 31 | OPTIONAL = 2 32 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Async-Channel-CI 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | tags: 7 | - '*' 8 | pull_request: 9 | 10 | jobs: 11 | lint: 12 | uses: Drakkar-Software/.github/.github/workflows/python3_lint_workflow.yml@master 13 | with: 14 | project_main_package: async_channel 15 | use_black: true 16 | 17 | tests: 18 | needs: lint 19 | uses: Drakkar-Software/.github/.github/workflows/python3_tests_workflow.yml@master 20 | secrets: 21 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 22 | 23 | publish: 24 | needs: tests 25 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 26 | uses: Drakkar-Software/.github/.github/workflows/python3_sdist_workflow.yml@master 27 | secrets: 28 | PYPI_OFFICIAL_UPLOAD_URL: ${{ secrets.PYPI_OFFICIAL_UPLOAD_URL }} 29 | PYPI_USERNAME: __token__ 30 | PYPI_PASSWORD: ${{ secrets.PYPI_TOKEN }} 31 | 32 | notify: 33 | if: ${{ failure() }} 34 | needs: 35 | - lint 36 | - tests 37 | - publish 38 | uses: Drakkar-Software/.github/.github/workflows/failure_notify_workflow.yml@master 39 | secrets: 40 | DISCORD_GITHUB_WEBHOOK: ${{ secrets.DISCORD_GITHUB_WEBHOOK }} 41 | -------------------------------------------------------------------------------- /async_channel/util/__init__.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | """ 17 | Define Channel helping methods 18 | """ 19 | from async_channel.util import channel_creator 20 | from async_channel.util import logging_util 21 | 22 | from async_channel.util.channel_creator import ( 23 | create_all_subclasses_channel, 24 | create_channel_instance, 25 | ) 26 | 27 | from async_channel.util.logging_util import ( 28 | get_logger, 29 | ) 30 | 31 | __all__ = [ 32 | "create_all_subclasses_channel", 33 | "create_channel_instance", 34 | "get_logger", 35 | ] 36 | -------------------------------------------------------------------------------- /async_channel/util/logging_util.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | """ 17 | Define async_channel logger implementation 18 | """ 19 | import logging 20 | 21 | 22 | # pylint: disable=no-member, import-outside-toplevel 23 | def get_logger(name: str = "") -> logging.Logger: 24 | """ 25 | :param name: the logger name 26 | :return: the logger implementation, can be octobot_commons one or default python logging 27 | """ 28 | try: 29 | import octobot_commons.logging as common_logging 30 | 31 | return common_logging.get_logger(logger_name=name) 32 | except ImportError: 33 | return logging.getLogger(name) 34 | -------------------------------------------------------------------------------- /async_channel/channels/__init__.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | """ 17 | Define async_channel implementation and usage 18 | """ 19 | from async_channel.channels import channel_instances 20 | from async_channel.channels import channel 21 | 22 | from async_channel.channels.channel_instances import ( 23 | ChannelInstances, 24 | set_chan_at_id, 25 | get_channels, 26 | del_channel_container, 27 | get_chan_at_id, 28 | del_chan_at_id, 29 | ) 30 | from async_channel.channels.channel import ( 31 | Channel, 32 | set_chan, 33 | del_chan, 34 | get_chan, 35 | ) 36 | 37 | __all__ = [ 38 | "ChannelInstances", 39 | "set_chan_at_id", 40 | "get_channels", 41 | "del_channel_container", 42 | "get_chan_at_id", 43 | "del_chan_at_id", 44 | "Channel", 45 | "set_chan", 46 | "del_chan", 47 | "get_chan", 48 | ] 49 | -------------------------------------------------------------------------------- /async_channel/__init__.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | """ 17 | Define async_channel project 18 | """ 19 | 20 | from async_channel import constants 21 | from async_channel.constants import ( 22 | CHANNEL_WILDCARD, 23 | DEFAULT_QUEUE_SIZE, 24 | ) 25 | 26 | from async_channel import enums 27 | from async_channel.enums import ChannelConsumerPriorityLevels 28 | 29 | from async_channel import producer 30 | from async_channel.producer import Producer 31 | 32 | from async_channel import consumer 33 | from async_channel.consumer import ( 34 | Consumer, 35 | InternalConsumer, 36 | SupervisedConsumer, 37 | ) 38 | 39 | PROJECT_NAME = "async-channel" 40 | VERSION = "2.2.1" # major.minor.revision 41 | 42 | __all__ = [ 43 | "CHANNEL_WILDCARD", 44 | "DEFAULT_QUEUE_SIZE", 45 | "ChannelConsumerPriorityLevels", 46 | "Producer", 47 | "Consumer", 48 | "InternalConsumer", 49 | "SupervisedConsumer", 50 | "PROJECT_NAME", 51 | "VERSION", 52 | ] 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # ide 107 | .idea 108 | 109 | # cython 110 | *.html 111 | *.c 112 | cython_debug/ 113 | 114 | # doc 115 | docs/build 116 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | # from distutils.extension import Extension 17 | from setuptools import find_packages 18 | from setuptools import setup 19 | 20 | from async_channel import PROJECT_NAME, VERSION 21 | 22 | PACKAGES = find_packages(exclude=["tests"]) 23 | 24 | 25 | REQUIRED = open('requirements.txt').readlines() 26 | REQUIRES_PYTHON = '>=3.8' 27 | 28 | setup( 29 | name=PROJECT_NAME.lower().replace("-", "_"), 30 | version=VERSION, 31 | url='https://github.com/Drakkar-Software/Async-Channel', 32 | license='LGPL-3.0', 33 | author='Drakkar-Software', 34 | author_email='contact@drakkar.software', 35 | description='Python channel based communication library', 36 | packages=PACKAGES, 37 | include_package_data=True, 38 | tests_require=["pytest"], 39 | test_suite="tests", 40 | zip_safe=False, 41 | data_files=[], 42 | setup_requires=REQUIRED, 43 | install_requires=REQUIRED, 44 | python_requires=REQUIRES_PYTHON, 45 | classifiers=[ 46 | 'Development Status :: 5 - Production/Stable', 47 | 'Operating System :: OS Independent', 48 | 'Operating System :: MacOS :: MacOS X', 49 | 'Operating System :: Microsoft :: Windows', 50 | 'Operating System :: POSIX', 51 | 'Programming Language :: Python :: 3.8', 52 | 'Programming Language :: Python :: 3.9', 53 | 'Programming Language :: Cython', 54 | 'Environment :: Console' 55 | ], 56 | ) 57 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | import asyncio 17 | 18 | import async_channel.channels as channels 19 | import async_channel.consumer as channel_consumer 20 | import async_channel.producer as producer 21 | 22 | TEST_CHANNEL = "Test" 23 | EMPTY_TEST_CHANNEL = "EmptyTest" 24 | EMPTY_TEST_WITH_ID_CHANNEL = "EmptyTestWithId" 25 | CONSUMER_KEY = "test" 26 | 27 | 28 | class EmptyTestConsumer(channel_consumer.Consumer): 29 | pass 30 | 31 | 32 | class EmptyTestSupervisedConsumer(channel_consumer.SupervisedConsumer): 33 | pass 34 | 35 | 36 | class EmptyTestProducer(producer.Producer): 37 | async def start(self): 38 | await asyncio.sleep(100000) 39 | 40 | async def pause(self): 41 | pass 42 | 43 | async def resume(self): 44 | pass 45 | 46 | 47 | class EmptyTestChannel(channels.Channel): 48 | CONSUMER_CLASS = EmptyTestConsumer 49 | PRODUCER_CLASS = EmptyTestProducer 50 | 51 | 52 | async def empty_test_callback(): 53 | pass 54 | 55 | 56 | async def mock_was_called_once(mocked_method): 57 | await wait_asyncio_next_cycle() 58 | mocked_method.assert_called_once() 59 | 60 | 61 | async def mock_was_not_called(mocked_method): 62 | await wait_asyncio_next_cycle() 63 | mocked_method.assert_not_called() 64 | 65 | 66 | class EmptyTestWithIdChannel(channels.Channel): 67 | CONSUMER_CLASS = EmptyTestConsumer 68 | PRODUCER_CLASS = EmptyTestProducer 69 | 70 | def __init__(self, test_id): 71 | super().__init__() 72 | self.chan_id = test_id 73 | 74 | 75 | async def wait_asyncio_next_cycle(): 76 | async def do_nothing(): 77 | pass 78 | await asyncio.create_task(do_nothing()) 79 | -------------------------------------------------------------------------------- /tests/test_channel_creator.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | import copy 17 | 18 | import pytest 19 | import async_channel.channels as channels 20 | import async_channel.util as util 21 | 22 | import tests 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_create_channel_instance(): 27 | class TestChannel(channels.Channel): 28 | pass 29 | 30 | channels.del_chan(tests.TEST_CHANNEL) 31 | await util.create_channel_instance(TestChannel, channels.set_chan) 32 | await channels.get_chan(tests.TEST_CHANNEL).stop() 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_create_synchronized_channel_instance(): 37 | class TestChannel(channels.Channel): 38 | pass 39 | 40 | channels.del_chan(tests.TEST_CHANNEL) 41 | await util.create_channel_instance(TestChannel, channels.set_chan, is_synchronized=True) 42 | assert channels.get_chan(tests.TEST_CHANNEL).is_synchronized 43 | await channels.get_chan(tests.TEST_CHANNEL).stop() 44 | 45 | 46 | @pytest.mark.asyncio 47 | async def test_create_all_subclasses_channel(): 48 | class TestChannelClass(channels.Channel): 49 | pass 50 | 51 | class Test1Channel(TestChannelClass): 52 | pass 53 | 54 | class Test2Channel(TestChannelClass): 55 | pass 56 | 57 | def clean_channels(): 58 | for channel in copy.deepcopy(channels.ChannelInstances.instance().channels): 59 | channels.del_chan(channel) 60 | 61 | channels.del_chan(tests.TEST_CHANNEL) 62 | await util.create_all_subclasses_channel(TestChannelClass, channels.set_chan) 63 | assert len(channels.ChannelInstances.instance().channels) == 3 # (EmptyTestChannel, Test1Channel, Test2Channel) 64 | clean_channels() 65 | await util.create_all_subclasses_channel(TestChannelClass, channels.set_chan, is_synchronized=True) 66 | assert all(channels.get_chan(channel).is_synchronized for channel in channels.ChannelInstances.instance().channels) 67 | clean_channels() 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Async-Channel [2.2.1](https://github.com/Drakkar-Software/Async-Channel/blob/master/CHANGELOG.md) 2 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/523d43c62f1d4de08395752367f5fddc)](https://www.codacy.com/gh/Drakkar-Software/Async-Channel/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Drakkar-Software/Async-Channel&utm_campaign=Badge_Grade) 3 | [![PyPI](https://img.shields.io/pypi/v/async-channel.svg)](https://pypi.python.org/pypi/async-channel/) 4 | [![Github-Action-CI](https://github.com/Drakkar-Software/Async-Channel/workflows/Async-Channel-Default-CI/badge.svg)](https://github.com/Drakkar-Software/Async-Channel/actions) 5 | [![Build Status](https://cloud.drone.io/api/badges/Drakkar-Software/Async-Channel/status.svg)](https://cloud.drone.io/Drakkar-Software/Async-Channel) 6 | [![Coverage Status](https://coveralls.io/repos/github/Drakkar-Software/OctoBot-Channels/badge.svg?branch=master)](https://coveralls.io/github/Drakkar-Software/OctoBot-Channels?branch=master) 7 | [![Doc Status](https://readthedocs.org/projects/octobot-channels/badge/?version=stable)](https://octobot-channels.readthedocs.io/en/stable/?badge=stable) 8 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 9 | 10 | Python multi-task communication library. Used by [OctoBot](https://github.com/Drakkar-Software/OctoBot) project. 11 | 12 | ## Installation 13 | With python3 : `pip install async-channel` 14 | 15 | ## Usage 16 | Example 17 | ```python 18 | import async_channel.consumer as consumer 19 | import async_channel.producer as producer 20 | import async_channel.channels as channels 21 | import async_channel.util as util 22 | 23 | class AwesomeProducer(producer.Producer): 24 | pass 25 | 26 | class AwesomeConsumer(consumer.Consumer): 27 | pass 28 | 29 | class AwesomeChannel(channels.Channel): 30 | PRODUCER_CLASS = AwesomeProducer 31 | CONSUMER_CLASS = AwesomeConsumer 32 | 33 | async def callback(data): 34 | print("Consumer called !") 35 | print("Received : " + data) 36 | 37 | # Creates the channel 38 | await util.create_channel_instance(AwesomeChannel, channels.Channels) 39 | 40 | # Add a new consumer to the channel 41 | await channels.Channels.get_chan("Awesome").new_consumer(callback) 42 | 43 | # Creates a producer that send data to the consumer through the channel 44 | producer = AwesomeProducer(channels.Channels.get_chan("Awesome")) 45 | await producer.run() 46 | await producer.send("test") 47 | 48 | # Stops the channel with all its producers and consumers 49 | # await channels.Channels.get_chan("Awesome").stop() 50 | ``` 51 | 52 | # Developer documentation 53 | On [readthedocs.io](https://octobot-channels.readthedocs.io/en/latest/) 54 | -------------------------------------------------------------------------------- /async_channel/util/channel_creator.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | """ 17 | Define Channel creation helping methods 18 | """ 19 | import typing 20 | 21 | import async_channel.channels as channels 22 | 23 | 24 | async def create_all_subclasses_channel( 25 | channel_class: typing.ClassVar, 26 | set_chan_method: typing.Callable, 27 | is_synchronized: bool = False, 28 | **kwargs: dict 29 | ) -> None: 30 | """ 31 | Calls 'channel_creator.create_channel_instance' for each subclasses of the 'channel_class' param 32 | :param channel_class: The class in which to search for subclasses 33 | :param set_chan_method: The method reference used in 'channel_creator.create_channel_instance' 34 | :param is_synchronized: the channel is_synchronized attribute 35 | :param kwargs: Some additional params passed to 'channel_creator.create_channel_instance' 36 | """ 37 | for to_be_created_channel_class in channel_class.__subclasses__(): 38 | await create_channel_instance( 39 | to_be_created_channel_class, 40 | set_chan_method, 41 | is_synchronized=is_synchronized, 42 | **kwargs 43 | ) 44 | 45 | 46 | async def create_channel_instance( 47 | channel_class: typing.ClassVar, 48 | set_chan_method: typing.Callable, 49 | is_synchronized: bool = False, 50 | channel_name: str = None, 51 | **kwargs: dict 52 | ) -> channels.Channel: 53 | """ 54 | Creates, initialize and start a async_channel instance 55 | :param channel_class: The class to instantiate with optional kwargs params 56 | :param set_chan_method: The method to call to add the created channel instance to a Channel list 57 | :param is_synchronized: the channel is_synchronized attribute 58 | :param channel_name: name of the channel to create. Defaults to channel_class.get_name() 59 | :param kwargs: Some additional params passed to the 'channel_class' constructor 60 | :return: the created 'channel_class' instance 61 | """ 62 | created_channel = channel_class(**kwargs) 63 | set_chan_method(created_channel, name=channel_name or channel_class.get_name()) 64 | created_channel.is_synchronized = is_synchronized 65 | await created_channel.start() 66 | return created_channel 67 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Async-Channel' 21 | copyright = '2020, Drakkar-Software' 22 | author = 'Drakkar-Software' 23 | 24 | # The short X.Y version 25 | version = '1.3' 26 | 27 | # The full version, including alpha/beta/rc tags 28 | release = '1.3.21-beta' 29 | 30 | # https://github.com/readthedocs/readthedocs.org/issues/2569 31 | master_doc = 'index' 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = [ 39 | 'sphinx.ext.autodoc', 40 | 'sphinx.ext.coverage', 41 | 'sphinx.ext.todo', 42 | 'sphinx.ext.intersphinx', 43 | 'sphinx.ext.viewcode', 44 | 'sphinx.ext.githubpages', 45 | 'sphinx.ext.napoleon', 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The language for content autogenerated by Sphinx. Refer to documentation 52 | # for a list of supported languages. 53 | # 54 | # This is also used if you do content translation via gettext catalogs. 55 | # Usually you set "language" from the command line for these cases. 56 | language = 'en' 57 | 58 | # List of patterns, relative to source directory, that match files and 59 | # directories to ignore when looking for source files. 60 | # This pattern also affects html_static_path and html_extra_path. 61 | exclude_patterns = [] 62 | 63 | 64 | # -- Options for HTML output ------------------------------------------------- 65 | 66 | # The theme to use for HTML and HTML Help pages. See the documentation for 67 | # a list of builtin themes. 68 | # 69 | html_theme = 'sphinx_rtd_theme' 70 | 71 | # Add any paths that contain custom static files (such as style sheets) here, 72 | # relative to this directory. They are copied after the builtin static files, 73 | # so a file named "default.css" will overwrite the builtin "default.css". 74 | html_static_path = ['_static'] 75 | 76 | 77 | # -- Extension configuration ------------------------------------------------- 78 | 79 | # -- Options for intersphinx extension --------------------------------------- 80 | 81 | # Example configuration for intersphinx: refer to the Python standard library. 82 | intersphinx_mapping = {'https://docs.python.org/3/': None} 83 | 84 | # -- Options for todo extension ---------------------------------------------- 85 | 86 | # If true, `todo` and `todoList` produce output, else they produce nothing. 87 | todo_include_todos = True -------------------------------------------------------------------------------- /async_channel/channels/channel_instances.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | """ 17 | This module defines created Channels interaction methods 18 | """ 19 | import async_channel.util.logging_util as logging 20 | 21 | 22 | class ChannelInstances: 23 | """ 24 | Singleton that contains Channel instances 25 | Singleton implementation from https://stackoverflow.com/questions/51245056/singleton-is-not-working-in-cython 26 | """ 27 | 28 | _instances = {} 29 | 30 | @classmethod 31 | def instance(cls, *args, **kwargs): 32 | """ 33 | Create the instance if not already created 34 | Return the class instance 35 | :param args: the constructor arguments 36 | :param kwargs: the constructor optional arguments 37 | :return: the class only instance 38 | """ 39 | if cls not in cls._instances: 40 | cls._instances[cls] = cls(*args, **kwargs) 41 | return cls._instances[cls] 42 | 43 | def __init__(self): 44 | self.channels = {} 45 | 46 | 47 | def set_chan_at_id(chan, name) -> None: 48 | """ 49 | Add a new async_channel to the channels instances dictionary at chan.id 50 | :param chan: the channel instance 51 | :param name: the channel name 52 | """ 53 | chan_name = chan.get_name() if name else name 54 | 55 | try: 56 | chan_instance = ChannelInstances.instance().channels[chan.chan_id] 57 | except KeyError: 58 | ChannelInstances.instance().channels[chan.chan_id] = {} 59 | chan_instance = ChannelInstances.instance().channels[chan.chan_id] 60 | 61 | if chan_name not in chan_instance: 62 | chan_instance[chan_name] = chan 63 | return chan 64 | raise ValueError(f"Channel {chan_name} already exists.") 65 | 66 | 67 | def get_channels(chan_id) -> dict: 68 | """ 69 | Get async_channel instances by async_channel id 70 | :param chan_id: the channel id 71 | :return: the channel instances at async_channel id 72 | """ 73 | try: 74 | return ChannelInstances.instance().channels[chan_id] 75 | except KeyError as exception: 76 | raise KeyError(f"Channels not found with chan_id: {chan_id}") from exception 77 | 78 | 79 | def del_channel_container(chan_id) -> None: 80 | """ 81 | Delete all async_channel id instances 82 | :param chan_id: the channel id 83 | """ 84 | ChannelInstances.instance().channels.pop(chan_id, None) 85 | 86 | 87 | def get_chan_at_id(chan_name, chan_id) -> object: 88 | """ 89 | Get the channel instance that matches the name and the id 90 | :param chan_name: the channel name 91 | :param chan_id: the channel id 92 | :return: the channel instance if any 93 | """ 94 | try: 95 | return ChannelInstances.instance().channels[chan_id][chan_name] 96 | except KeyError as exception: 97 | raise KeyError( 98 | f"Channel {chan_name} not found with chan_id: {chan_id}" 99 | ) from exception 100 | 101 | 102 | def del_chan_at_id(chan_name, chan_id) -> None: 103 | """ 104 | Delete the channel instance that matches the name and the id 105 | :param chan_name: the channel name 106 | :param chan_id: the channel id 107 | """ 108 | try: 109 | ChannelInstances.instance().channels[chan_id].pop(chan_name, None) 110 | except KeyError: 111 | logging.get_logger(ChannelInstances.__name__).warning( 112 | f"Can't del chan {chan_name} with chan_id: {chan_id}" 113 | ) 114 | -------------------------------------------------------------------------------- /tests/test_consumer.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | 17 | import pytest 18 | import pytest_asyncio 19 | import mock 20 | 21 | import async_channel.consumer as channel_consumer 22 | import async_channel.channels as channels 23 | import async_channel.util as util 24 | import tests 25 | 26 | 27 | async def init_consumer_test(): 28 | class TestChannel(channels.Channel): 29 | PRODUCER_CLASS = tests.EmptyTestProducer 30 | CONSUMER_CLASS = tests.EmptyTestConsumer 31 | 32 | channels.del_chan(tests.TEST_CHANNEL) 33 | await util.create_channel_instance(TestChannel, channels.set_chan) 34 | producer = tests.EmptyTestProducer(channels.get_chan(tests.TEST_CHANNEL)) 35 | await producer.run() 36 | return await channels.get_chan(tests.TEST_CHANNEL).new_consumer(tests.empty_test_callback) 37 | 38 | 39 | @pytest.mark.asyncio 40 | async def test_perform_called(): 41 | consumer = await init_consumer_test() 42 | with mock.patch.object(consumer, 'perform', new=mock.AsyncMock()) as mocked_consume_ends: 43 | await channels.get_chan(tests.TEST_CHANNEL).get_internal_producer().send({}) 44 | await tests.mock_was_called_once(mocked_consume_ends) 45 | 46 | await channels.get_chan(tests.TEST_CHANNEL).stop() 47 | 48 | 49 | @pytest.mark.asyncio 50 | async def test_consume_ends_called(): 51 | consumer = await init_consumer_test() 52 | with mock.patch.object(consumer, 'consume_ends', new=mock.AsyncMock()) as mocked_consume_ends: 53 | await channels.get_chan(tests.TEST_CHANNEL).get_internal_producer().send({}) 54 | await tests.mock_was_called_once(mocked_consume_ends) 55 | 56 | await channels.get_chan(tests.TEST_CHANNEL).stop() 57 | 58 | 59 | @pytest_asyncio.fixture 60 | async def internal_consumer(): 61 | class TestInternalConsumer(channel_consumer.InternalConsumer): 62 | async def perform(self, kwargs): 63 | pass 64 | 65 | class TestChannel(channels.Channel): 66 | PRODUCER_CLASS = tests.EmptyTestProducer 67 | CONSUMER_CLASS = TestInternalConsumer 68 | 69 | channels.del_chan(tests.TEST_CHANNEL) 70 | await util.create_channel_instance(TestChannel, channels.set_chan) 71 | producer = tests.EmptyTestProducer(channels.get_chan(tests.TEST_CHANNEL)) 72 | await producer.run() 73 | yield TestInternalConsumer() 74 | await channels.get_chan(tests.TEST_CHANNEL).stop() 75 | 76 | 77 | @pytest.mark.asyncio 78 | async def test_internal_consumer(internal_consumer): 79 | await channels.get_chan(tests.TEST_CHANNEL).new_consumer(internal_consumer=internal_consumer) 80 | 81 | with mock.patch.object(internal_consumer, 'perform', new=mock.AsyncMock()) as mocked_consume_ends: 82 | await channels.get_chan(tests.TEST_CHANNEL).get_internal_producer().send({}) 83 | await tests.mock_was_called_once(mocked_consume_ends) 84 | 85 | 86 | @pytest.mark.asyncio 87 | async def test_default_internal_consumer_callback(internal_consumer): 88 | with pytest.raises(NotImplementedError): 89 | await internal_consumer.internal_callback() 90 | 91 | 92 | @pytest.mark.asyncio 93 | async def test_supervised_consumer(): 94 | class TestSupervisedConsumer(channel_consumer.SupervisedConsumer): 95 | pass 96 | 97 | class TestChannel(channels.Channel): 98 | PRODUCER_CLASS = tests.EmptyTestProducer 99 | CONSUMER_CLASS = TestSupervisedConsumer 100 | 101 | channels.del_chan(tests.TEST_CHANNEL) 102 | await util.create_channel_instance(TestChannel, channels.set_chan) 103 | producer = tests.EmptyTestProducer(channels.get_chan(tests.TEST_CHANNEL)) 104 | await producer.run() 105 | consumer = await channels.get_chan(tests.TEST_CHANNEL).new_consumer(tests.empty_test_callback) 106 | await channels.get_chan(tests.TEST_CHANNEL).get_internal_producer().send({}) 107 | await consumer.queue.join() 108 | await channels.get_chan(tests.TEST_CHANNEL).stop() 109 | -------------------------------------------------------------------------------- /tests/test_channel_instances.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | import uuid 17 | 18 | import pytest 19 | import pytest_asyncio 20 | 21 | import async_channel.channels as channels 22 | import async_channel.util as util 23 | import tests 24 | 25 | 26 | @pytest_asyncio.fixture 27 | async def chan_id(): 28 | channel_uuid = uuid.uuid4().hex 29 | await util.create_channel_instance(tests.EmptyTestWithIdChannel, channels.set_chan_at_id, test_id=channel_uuid) 30 | return channel_uuid 31 | 32 | 33 | @pytest_asyncio.fixture 34 | async def channel_id(): 35 | channel_uuid = uuid.uuid4().hex 36 | await util.create_channel_instance(tests.EmptyTestWithIdChannel, channels.set_chan_at_id, test_id=channel_uuid) 37 | yield channel_uuid 38 | await channels.get_chan_at_id(tests.EMPTY_TEST_WITH_ID_CHANNEL, channel_uuid).stop() 39 | channels.del_chan_at_id(tests.EMPTY_TEST_WITH_ID_CHANNEL, channel_uuid) 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_get_chan_at_id(channel_id): 44 | assert channels.get_chan_at_id(tests.EMPTY_TEST_WITH_ID_CHANNEL, channel_id) 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_set_chan_at_id_already_exist(channel_id): 49 | with pytest.raises(ValueError): 50 | await util.create_channel_instance(tests.EmptyTestWithIdChannel, channels.set_chan_at_id, test_id=channel_id) 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_del_channel_container_not_exist_does_not_raise(channel_id): 55 | channels.del_channel_container(channel_id + "test") 56 | 57 | 58 | @pytest.mark.asyncio 59 | async def test_del_channel_container(chan_id): 60 | channels.del_channel_container(chan_id) 61 | with pytest.raises(KeyError): 62 | channels.get_chan_at_id(tests.EMPTY_TEST_WITH_ID_CHANNEL, chan_id) 63 | channels.del_chan_at_id(tests.EMPTY_TEST_WITH_ID_CHANNEL, chan_id) 64 | 65 | 66 | @pytest.mark.asyncio 67 | async def test_get_channels_not_exist(channel_id): 68 | with pytest.raises(KeyError): 69 | channels.get_channels(channel_id + "test") 70 | 71 | 72 | @pytest.mark.asyncio 73 | async def test_get_channels(chan_id): 74 | class EmptyTestWithId2Channel(tests.EmptyTestWithIdChannel): 75 | pass 76 | 77 | class EmptyTestWithId3Channel(tests.EmptyTestWithIdChannel): 78 | pass 79 | 80 | class EmptyTestWithId4Channel(tests.EmptyTestWithIdChannel): 81 | pass 82 | 83 | class EmptyTestWithId5Channel(tests.EmptyTestWithIdChannel): 84 | pass 85 | 86 | class EmptyTestWithId6Channel(tests.EmptyTestWithIdChannel): 87 | pass 88 | 89 | channel_4_id = uuid.uuid4().hex 90 | channel_6_id = uuid.uuid4().hex 91 | ch1 = channels.get_chan_at_id(tests.EMPTY_TEST_WITH_ID_CHANNEL, chan_id) 92 | ch2 = await util.create_channel_instance(EmptyTestWithId2Channel, channels.set_chan_at_id, test_id=chan_id) 93 | ch3 = await util.create_channel_instance(EmptyTestWithId3Channel, channels.set_chan_at_id, test_id=chan_id) 94 | ch4 = await util.create_channel_instance(EmptyTestWithId4Channel, channels.set_chan_at_id, test_id=channel_4_id) 95 | ch5 = await util.create_channel_instance(EmptyTestWithId5Channel, channels.set_chan_at_id, test_id=channel_4_id) 96 | ch6 = await util.create_channel_instance(EmptyTestWithId6Channel, channels.set_chan_at_id, test_id=channel_6_id) 97 | assert len(channels.get_channels(chan_id)) == 3 98 | assert len(channels.get_channels(channel_4_id)) == 2 99 | assert len(channels.get_channels(channel_6_id)) == 1 100 | assert channels.get_channels(chan_id) == { 101 | "EmptyTestWithId": ch1, 102 | "EmptyTestWithId2": ch2, 103 | "EmptyTestWithId3": ch3 104 | } 105 | assert channels.get_channels(channel_4_id) == { 106 | "EmptyTestWithId4": ch4, 107 | "EmptyTestWithId5": ch5 108 | } 109 | assert channels.get_channels(channel_6_id) == { 110 | "EmptyTestWithId6": ch6 111 | } 112 | channels.del_channel_container(chan_id) 113 | channels.del_channel_container(channel_4_id) 114 | channels.del_channel_container(channel_6_id) 115 | -------------------------------------------------------------------------------- /async_channel/producer.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | """ 17 | Define async_channel Producer class 18 | """ 19 | import asyncio 20 | 21 | import async_channel.util.logging_util as logging 22 | 23 | 24 | class Producer: 25 | """ 26 | A Producer is responsible for producing some output that may be placed onto the head of a queue. 27 | A Consumer will consume this data through the same shared queue. 28 | A producer doesn't need to know or care about its consumers. 29 | But if there is no space in the queue, it won't be able to share what it has produced. 30 | It manages its consumer calls by priority levels 31 | When the channel is synchronized priority levels are used to priorities or delay consumer calls 32 | """ 33 | 34 | def __init__(self, channel): 35 | self.logger = logging.get_logger(self.__class__.__name__) 36 | 37 | # Related async_channel instance 38 | self.channel = channel 39 | 40 | """ 41 | Should only be used with .cancel() 42 | """ 43 | self.produce_task = None 44 | 45 | """ 46 | Should be used as the perform while loop condition 47 | while(self.should_stop): 48 | ... 49 | """ 50 | self.should_stop = False 51 | 52 | """ 53 | Should be used to know if the producer is already started 54 | """ 55 | self.is_running = False 56 | 57 | async def send(self, data) -> None: 58 | """ 59 | Send to each consumer data though its queue 60 | :param data: data to be put into consumers queues 61 | 62 | The implementation should use 'self.async_channel.get_consumers' 63 | Example 64 | >>> for consumer in self.async_channel.get_consumers(): 65 | >>> await consumer.queue.put({ 66 | >>> "my_key": my_value 67 | >>> }) 68 | """ 69 | for consumer in self.channel.get_consumers(): 70 | await consumer.queue.put(data) 71 | 72 | async def push(self, **kwargs) -> None: 73 | """ 74 | Push notification that new data should be sent implementation 75 | When nothing should be done on data : self.send() 76 | """ 77 | 78 | async def start(self) -> None: 79 | """ 80 | Should be implemented for producer's non-triggered tasks 81 | """ 82 | 83 | async def pause(self) -> None: 84 | """ 85 | Called when the channel runs out of consumer 86 | """ 87 | self.logger.debug("Pausing...") 88 | self.is_running = False 89 | # Triggers itself if not already paused 90 | if not self.channel.is_paused: 91 | self.channel.is_paused = True 92 | 93 | async def resume(self) -> None: 94 | """ 95 | Called when the channel is no longer out of consumer 96 | """ 97 | self.logger.debug("Resuming...") 98 | # Triggers itself if not already resumed 99 | if self.channel.is_paused: 100 | self.channel.is_paused = False 101 | 102 | async def perform(self, **kwargs) -> None: 103 | """ 104 | Should implement producer's non-triggered tasks 105 | Can be use to force producer to perform tasks 106 | """ 107 | 108 | async def modify(self, **kwargs) -> None: 109 | """ 110 | Should be implemented when producer can be modified during perform() 111 | """ 112 | 113 | async def wait_for_processing(self) -> None: 114 | """ 115 | Should be used only with SupervisedConsumers 116 | It will wait until all consumers have notified that their consume() method have ended 117 | """ 118 | await asyncio.gather( 119 | *(consumer.join_queue() for consumer in self.channel.get_consumers()) 120 | ) 121 | 122 | async def synchronized_perform_consumers_queue( 123 | self, priority_level, join_consumers, timeout 124 | ) -> None: 125 | """ 126 | Empties the queue synchronously for each consumers 127 | :param priority_level: the consumer minimal priority level 128 | :param join_consumers: True if consumer tasks should be joined. Avoids orphaned tasks to run without 129 | :param timeout: Time to wait for consumers in join call 130 | waiting for them when started before this check (when check, their queue is empty but a task is running) 131 | """ 132 | for consumer in self.channel.get_prioritized_consumers(priority_level): 133 | while not consumer.queue.empty(): 134 | await consumer.perform(await consumer.queue.get()) 135 | if join_consumers: 136 | await consumer.join(timeout) 137 | 138 | async def stop(self) -> None: 139 | """ 140 | Stops non-triggered tasks management 141 | """ 142 | self.should_stop = True 143 | self.is_running = False 144 | if self.produce_task: 145 | self.produce_task.cancel() 146 | 147 | def create_task(self) -> None: 148 | """ 149 | Creates a new asyncio task that contains start() execution 150 | """ 151 | self.is_running = True 152 | self.produce_task = asyncio.create_task(self.start()) 153 | 154 | async def run(self) -> None: 155 | """ 156 | Start the producer main task 157 | Shouldn't start the producer main task if the channel is synchronized 158 | Should always call 159 | >>> self.async_channel.register_producer 160 | """ 161 | await self.channel.register_producer(self) 162 | if not self.channel.is_synchronized: 163 | self.create_task() 164 | 165 | def is_consumers_queue_empty(self, priority_level) -> bool: 166 | """ 167 | Check if consumers queue are empty 168 | :param priority_level: the consumer minimal priority level 169 | :return: the check result 170 | """ 171 | for consumer in self.channel.get_consumers(): 172 | if consumer.priority_level <= priority_level and not consumer.queue.empty(): 173 | return False 174 | return True 175 | -------------------------------------------------------------------------------- /async_channel/consumer.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | """ 17 | Define async_channel Consumer class 18 | """ 19 | import asyncio 20 | 21 | import async_channel.util.logging_util as logging 22 | import async_channel.enums 23 | 24 | 25 | class Consumer: 26 | """ 27 | A consumer keeps reading from the channel and processes any data passed to it. 28 | A consumer will start consuming by calling its 'consume' method. 29 | The data processing implementation is coded in the 'perform' method. 30 | A consumer also responds to channel events like pause and stop. 31 | """ 32 | 33 | def __init__( 34 | self, 35 | callback: object, 36 | size: int = async_channel.constants.DEFAULT_QUEUE_SIZE, 37 | priority_level: int = async_channel.enums.ChannelConsumerPriorityLevels.HIGH.value, 38 | ): 39 | self.logger = logging.get_logger(self.__class__.__name__) 40 | 41 | # Consumer data queue. It contains producer's work (received through Producer.send()). 42 | self.queue = asyncio.Queue(maxsize=size) 43 | 44 | # Method to be called when performing task is done 45 | self.callback = callback 46 | 47 | # Should only be used with .cancel() 48 | self.consume_task = None 49 | 50 | """ 51 | Should be used as the perform while loop condition 52 | >>> while(self.should_stop): 53 | ... 54 | """ 55 | self.should_stop = False 56 | 57 | # Default priority level 58 | # Used by Producers to call consumers by prioritization 59 | # The lowest level has the highest priority 60 | self.priority_level = priority_level 61 | 62 | async def consume(self) -> None: 63 | """ 64 | Should be overwritten with a self.queue.get() in a while loop 65 | """ 66 | while not self.should_stop: 67 | try: 68 | await self.perform(await self.queue.get()) 69 | except asyncio.CancelledError: 70 | self.logger.debug("Cancelled task") 71 | except Exception as consume_exception: # pylint: disable=broad-except 72 | self.logger.exception( 73 | exception=consume_exception, 74 | publish_error_if_necessary=True, 75 | error_message=f"Exception when calling callback on {self}: {consume_exception}", 76 | ) 77 | finally: 78 | await self.consume_ends() 79 | 80 | async def perform(self, kwargs) -> None: 81 | """ 82 | Should be overwritten to handle queue data 83 | :param kwargs: queue get content 84 | """ 85 | await self.callback(**kwargs) 86 | 87 | async def consume_ends(self) -> None: 88 | """ 89 | Should be overwritten to handle consumption ends 90 | """ 91 | 92 | async def start(self) -> None: 93 | """ 94 | Should be implemented for consumer's non-triggered tasks 95 | """ 96 | self.should_stop = False 97 | 98 | async def stop(self) -> None: 99 | """ 100 | Stops non-triggered tasks management 101 | """ 102 | self.should_stop = True 103 | if self.consume_task: 104 | self.consume_task.cancel() 105 | 106 | def create_task(self) -> None: 107 | """ 108 | Creates a new asyncio task that contains start() execution 109 | """ 110 | self.consume_task = asyncio.create_task(self.consume()) 111 | 112 | async def run(self, with_task=True) -> None: 113 | """ 114 | - Initialize the consumer 115 | - Start the consumer main task 116 | :param with_task: If the consumer should run in a task 117 | """ 118 | await self.start() 119 | if with_task: 120 | self.create_task() 121 | 122 | async def join(self, timeout) -> None: 123 | """ 124 | Implemented in SupervisedConsumer to wait for any "perform" call to be finished. 125 | Instantly returns on regular consumer 126 | """ 127 | 128 | async def join_queue(self) -> None: 129 | """ 130 | Implemented in SupervisedConsumer to wait for the whole queue to finish 131 | processing. 132 | Instantly returns on regular consumer 133 | """ 134 | 135 | def __str__(self): 136 | return f"{self.__class__.__name__} with callback: {self.callback.__name__}" 137 | 138 | 139 | class InternalConsumer(Consumer): 140 | """ 141 | An InternalConsumer is a classic Consumer except that his callback is declared internally 142 | """ 143 | 144 | def __init__(self): 145 | """ 146 | The constructor only override the callback to be the 'internal_callback' method 147 | """ 148 | super().__init__(None) 149 | self.callback = self.internal_callback 150 | 151 | async def internal_callback(self, **kwargs: dict) -> None: 152 | """ 153 | The method triggered when the producer has pushed into the channel 154 | :param kwargs: Additional params 155 | """ 156 | raise NotImplementedError("internal_callback is not implemented") 157 | 158 | 159 | class SupervisedConsumer(Consumer): 160 | """ 161 | A SupervisedConsumer is a classic Consumer that notifies the queue when its work is done 162 | """ 163 | 164 | def __init__( 165 | self, 166 | callback: object, 167 | size: int = async_channel.constants.DEFAULT_QUEUE_SIZE, 168 | priority_level: int = async_channel.enums.ChannelConsumerPriorityLevels.HIGH.value, 169 | ): 170 | """ 171 | The constructor only override the callback to be the 'internal_callback' method 172 | """ 173 | super().__init__(callback, size=size, priority_level=priority_level) 174 | 175 | # Clear when perform is running (set after) 176 | self.idle = asyncio.Event() 177 | self.idle.set() 178 | 179 | async def join(self, timeout) -> None: 180 | """ 181 | Wait for any perform to be finished. 182 | """ 183 | if not self.idle.is_set(): 184 | await asyncio.wait_for(self.idle.wait(), timeout) 185 | 186 | async def join_queue(self) -> None: 187 | """ 188 | Wait for the consumer queue to finish processing. 189 | """ 190 | await self.queue.join() 191 | 192 | async def perform(self, kwargs) -> None: 193 | """ 194 | Clear self.idle event when perform is being done then set it 195 | :param kwargs: queue get content 196 | """ 197 | try: 198 | self.idle.clear() 199 | await self.callback(**kwargs) 200 | finally: 201 | self.idle.set() 202 | 203 | async def consume_ends(self) -> None: 204 | """ 205 | The method called when the work is done 206 | """ 207 | try: 208 | self.queue.task_done() 209 | except ( 210 | ValueError 211 | ): # when task_done() is called when the Exception was CancelledError 212 | pass 213 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /tests/test_producer.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | 17 | import pytest 18 | 19 | import async_channel.consumer as channel_consumer 20 | import async_channel.channels as channels 21 | import async_channel.producer as channel_producer 22 | import async_channel.util as util 23 | import tests 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_send_internal_producer_without_consumer(): 28 | class TestProducer(channel_producer.Producer): 29 | async def send(self, data, **kwargs): 30 | await super().send(data) 31 | await channels.get_chan(tests.TEST_CHANNEL).stop() 32 | 33 | async def pause(self): 34 | pass 35 | 36 | async def resume(self): 37 | pass 38 | 39 | class TestChannel(channels.Channel): 40 | PRODUCER_CLASS = TestProducer 41 | 42 | channels.del_chan(tests.TEST_CHANNEL) 43 | await util.create_channel_instance(TestChannel, channels.set_chan) 44 | await channels.get_chan(tests.TEST_CHANNEL).get_internal_producer().send({}) 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_send_producer_without_consumer(): 49 | class TestProducer(channel_producer.Producer): 50 | async def send(self, data, **kwargs): 51 | await super().send(data) 52 | await channels.get_chan(tests.TEST_CHANNEL).stop() 53 | 54 | async def pause(self): 55 | pass 56 | 57 | async def resume(self): 58 | pass 59 | 60 | class TestConsumer(channel_consumer.Consumer): 61 | async def consume(self): 62 | while not self.should_stop: 63 | await self.callback(**(await self.queue.get())) 64 | 65 | class TestChannel(channels.Channel): 66 | PRODUCER_CLASS = TestProducer 67 | CONSUMER_CLASS = TestConsumer 68 | 69 | channels.del_chan(tests.TEST_CHANNEL) 70 | await util.create_channel_instance(TestChannel, channels.set_chan) 71 | 72 | producer = TestProducer(channels.get_chan(tests.TEST_CHANNEL)) 73 | await producer.run() 74 | await producer.send({}) 75 | 76 | 77 | @pytest.mark.asyncio 78 | async def test_send_producer_with_consumer(): 79 | class TestConsumer(channel_consumer.Consumer): 80 | pass 81 | 82 | class TestChannel(channels.Channel): 83 | PRODUCER_CLASS = tests.EmptyTestProducer 84 | CONSUMER_CLASS = TestConsumer 85 | 86 | async def callback(data): 87 | assert data == "test" 88 | await channels.get_chan(tests.TEST_CHANNEL).stop() 89 | 90 | channels.del_chan(tests.TEST_CHANNEL) 91 | await util.create_channel_instance(TestChannel, channels.set_chan) 92 | await channels.get_chan(tests.TEST_CHANNEL).new_consumer(callback) 93 | 94 | producer = tests.EmptyTestProducer(channels.get_chan(tests.TEST_CHANNEL)) 95 | await producer.run() 96 | await producer.send({"data": "test"}) 97 | 98 | 99 | @pytest.mark.asyncio 100 | async def test_pause_producer_without_consumers(): 101 | class TestProducer(channel_producer.Producer): 102 | async def pause(self): 103 | await channels.get_chan(tests.TEST_CHANNEL).stop() 104 | 105 | async def pause(self): 106 | pass 107 | 108 | async def resume(self): 109 | pass 110 | 111 | class TestChannel(channels.Channel): 112 | PRODUCER_CLASS = TestProducer 113 | CONSUMER_CLASS = tests.EmptyTestConsumer 114 | 115 | channels.del_chan(tests.TEST_CHANNEL) 116 | await util.create_channel_instance(TestChannel, channels.set_chan) 117 | await TestProducer(channels.get_chan(tests.TEST_CHANNEL)).run() 118 | 119 | 120 | @pytest.mark.asyncio 121 | async def test_pause_producer_with_removed_consumer(): 122 | class TestProducer(channel_producer.Producer): 123 | async def pause(self): 124 | await channels.get_chan(tests.TEST_CHANNEL).stop() 125 | 126 | async def pause(self): 127 | pass 128 | 129 | async def resume(self): 130 | pass 131 | 132 | class TestChannel(channels.Channel): 133 | PRODUCER_CLASS = TestProducer 134 | CONSUMER_CLASS = tests.EmptyTestConsumer 135 | 136 | channels.del_chan(tests.TEST_CHANNEL) 137 | await util.create_channel_instance(TestChannel, channels.set_chan) 138 | consumer = await channels.get_chan(tests.TEST_CHANNEL).new_consumer(tests.empty_test_callback) 139 | await TestProducer(channels.get_chan(tests.TEST_CHANNEL)).run() 140 | await channels.get_chan(tests.TEST_CHANNEL).remove_consumer(consumer) 141 | 142 | 143 | @pytest.mark.asyncio 144 | async def test_resume_producer(): 145 | class TestProducer(channel_producer.Producer): 146 | async def resume(self): 147 | await channels.get_chan(tests.TEST_CHANNEL).stop() 148 | 149 | async def pause(self): 150 | pass 151 | 152 | async def resume(self): 153 | pass 154 | 155 | class TestChannel(channels.Channel): 156 | PRODUCER_CLASS = TestProducer 157 | CONSUMER_CLASS = tests.EmptyTestConsumer 158 | 159 | channels.del_chan(tests.TEST_CHANNEL) 160 | await util.create_channel_instance(TestChannel, channels.set_chan) 161 | await TestProducer(channels.get_chan(tests.TEST_CHANNEL)).run() 162 | await channels.get_chan(tests.TEST_CHANNEL).new_consumer(tests.empty_test_callback) 163 | 164 | 165 | @pytest.mark.asyncio 166 | async def test_resume_producer(): 167 | class TestSupervisedConsumer(channel_consumer.SupervisedConsumer): 168 | pass 169 | 170 | class TestChannel(channels.Channel): 171 | PRODUCER_CLASS = tests.EmptyTestProducer 172 | CONSUMER_CLASS = TestSupervisedConsumer 173 | 174 | channels.del_chan(tests.TEST_CHANNEL) 175 | await util.create_channel_instance(TestChannel, channels.set_chan) 176 | producer = tests.EmptyTestProducer(channels.get_chan(tests.TEST_CHANNEL)) 177 | await producer.run() 178 | await channels.get_chan(tests.TEST_CHANNEL).new_consumer(tests.empty_test_callback) 179 | await channels.get_chan(tests.TEST_CHANNEL).new_consumer(tests.empty_test_callback) 180 | await channels.get_chan(tests.TEST_CHANNEL).new_consumer(tests.empty_test_callback) 181 | await producer.send({"data": "test"}) 182 | await producer.wait_for_processing() 183 | await channels.get_chan(tests.TEST_CHANNEL).stop() 184 | 185 | 186 | @pytest.mark.asyncio 187 | async def test_producer_is_running(): 188 | class TestChannel(channels.Channel): 189 | PRODUCER_CLASS = tests.EmptyTestProducer 190 | 191 | channels.del_chan(tests.TEST_CHANNEL) 192 | await util.create_channel_instance(TestChannel, channels.set_chan) 193 | producer = tests.EmptyTestProducer(channels.get_chan(tests.TEST_CHANNEL)) 194 | assert not producer.is_running 195 | await producer.run() 196 | assert producer.is_running 197 | await channels.get_chan(tests.TEST_CHANNEL).stop() 198 | assert not producer.is_running 199 | 200 | 201 | @pytest.mark.asyncio 202 | async def test_producer_pause_resume(): 203 | class TestChannel(channels.Channel): 204 | PRODUCER_CLASS = channel_producer.Producer 205 | 206 | channels.del_chan(tests.TEST_CHANNEL) 207 | await util.create_channel_instance(TestChannel, channels.set_chan) 208 | producer = channel_producer.Producer(channels.get_chan(tests.TEST_CHANNEL)) 209 | assert producer.channel.is_paused 210 | await producer.pause() 211 | assert producer.channel.is_paused 212 | await producer.resume() 213 | assert not producer.channel.is_paused 214 | await producer.pause() 215 | assert producer.channel.is_paused 216 | await producer.resume() 217 | assert not producer.channel.is_paused 218 | await channels.get_chan(tests.TEST_CHANNEL).stop() 219 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.2.1] - 2023-09-03 8 | ### Added 9 | - channel_name to creator 10 | 11 | ## [2.2.0] - 2023-05-02 12 | ### Updated 13 | - Supported python versions 14 | ### Removed 15 | - Cython 16 | 17 | ## [2.1.0] - 2022-12-23 18 | ### Updated 19 | - [Cython] update to 0.29.32 20 | 21 | ## [2.0.14] - 2022-12-22 22 | ### Added 23 | - [Channel] get_prioritized_consumers 24 | 25 | ## [2.0.13] - 2022-01-08 26 | ### Added 27 | - [SupervisedConsumer] add ability to join current perform task 28 | 29 | ### Updated 30 | - bump requirements 31 | 32 | ## [2.0.12] - 2021-07-19 33 | ### Updated 34 | - bump requirements 35 | 36 | ## [2.0.11] - 2021-07-19 37 | ### Updated 38 | - bump requirements 39 | 40 | ## [2.0.10] - 2021-05-05 41 | ### Updated 42 | - bump requirements 43 | 44 | ## [2.0.9] - 2021-03-03 45 | ### Added 46 | - Python 3.9 support 47 | 48 | ## [2.0.8] - 2021-02-25 49 | ### Updated 50 | - cython requirement 51 | 52 | ## [2.0.7] - 2020-12-07 53 | ### Updated 54 | - revert import statements changes 55 | 56 | ## [2.0.6] - 2020-11-07 57 | ### Updated 58 | - revert import statements changes 59 | 60 | ## [2.0.5] - 2020-11-07 61 | ### Updated 62 | - import statements 63 | 64 | ## [2.0.4] - 2020-11-07 65 | ### Fixed 66 | - async_channel.util python file 67 | 68 | ## [2.0.3] - 2020-10-23 69 | ### Updated 70 | - Python 3.8 support 71 | 72 | ## [2.0.2] - 2020-10-13 73 | ### Fixed 74 | - Cython headers export 75 | 76 | ## [2.0.1] - 2020-10-01 77 | ### Added 78 | - Logging dynamic implementation 79 | - Project name to 'Async-Channel' 80 | 81 | ## [2.0.0] - 2020-10-01 82 | ### Update 83 | - Project name to 'channel' 84 | - python and cython imports behaviour 85 | 86 | ### Removed 87 | - OctoBot-Commons requirement 88 | 89 | ## [1.4.11] - 2020-09-01 90 | ### Update 91 | - Requirements 92 | 93 | ## [1.4.10] - 2020-08-15 94 | ### Update 95 | - Requirements 96 | 97 | ## [1.4.9] - 2020-06-19 98 | ### Update 99 | - Requirements 100 | 101 | ## [1.4.8] - 2020-05-27 102 | ### Update 103 | - Cython version 104 | 105 | ## [1.4.7] - 2020-05-17 106 | ### Fixed 107 | - [Producer] pause is running was not set 108 | 109 | ## [1.4.6] - 2020-05-16 110 | ### Updated 111 | - Requirements 112 | 113 | ## [1.4.5] - 2020-05-13 114 | ### Changed 115 | - [Channel] Default priority value to HIGH 116 | 117 | ## [1.4.4] - 2020-05-13 118 | ### Added 119 | - [Channel] Producer pause and resume check with consumer priority levels 120 | 121 | ## [1.4.2] - 2020-05-11 122 | ### Added 123 | - [CI] Azure pipeline 124 | 125 | ### Removed 126 | - [CI] macOs build on travis 127 | - [CI] Appveyor builds 128 | 129 | ## [1.4.1] - 2020-05-09 130 | ### Added 131 | - [ChannelInstances] Channel id support 132 | 133 | ## [1.4.0] - 2020-05-01 134 | ### Added 135 | - Synchronous Channel 136 | - Synchronous Consumer 137 | - Synchronous Producer 138 | 139 | ## [1.3.25] - 2020-04-27 140 | ### Added 141 | - [Channel] consumer filtering by list 142 | 143 | ## [1.3.24] - 2020-04-17 144 | ### Added 145 | - [Producer] pause and resume default implementation 146 | 147 | ## [1.3.23] - 2020-04-07 148 | ### Fixed 149 | - Wildcard imports 150 | 151 | ## [1.3.22] - 2020-03-26 152 | ### Added 153 | - Documentation basis with sphinx 154 | - Pylint check on CI 155 | - Black check on CI 156 | 157 | ### Fixed 158 | - Documentation issues 159 | - Pylint issues 160 | - Black issues 161 | 162 | ## [1.3.21] - 2020-03-05 163 | ### Changed 164 | - Exception logger from Commons 165 | 166 | ### Updated 167 | - Commons version to >= 1.3.0 168 | 169 | ## [1.3.20] - 2020-02-10 170 | ### Added 171 | - flush method to channels 172 | - ```__str__``` representation for consumers 173 | 174 | ## [1.3.19] - 2020-01-02 175 | ### Changed 176 | - create_channel_instance now returns the created channel 177 | 178 | ### Fixed 179 | - fix set_chan channel name default value inference 180 | 181 | ## [1.3.18] - 2019-12-24 182 | ### Changed 183 | - Channels __ methods to _ methods (syntax update) 184 | 185 | ## [1.3.17] - 2019-12-21 186 | ### Updated 187 | - Commons version to >= 1.2.0 188 | 189 | ### Added 190 | - Makefile 191 | 192 | ## [1.3.16] - 2019-12-14 193 | ### Updated 194 | - Commons version to >= 1.1.50 195 | 196 | ### Fixed 197 | - test_set_chan 198 | 199 | ## [1.3.15] - 2019-11-07 200 | ### Updated 201 | - Cython version to 0.29.14 202 | 203 | ## [1.3.14] - 2019-10-29 204 | ### Added 205 | - OSX support 206 | 207 | ## [1.3.13] - 2019-10-09 208 | ### Added 209 | - PyPi manylinux deployment 210 | 211 | ## [1.3.12] - 2019-10-08 212 | ### Fixed 213 | - Install with setup 214 | 215 | ## [1.3.11] - 2019-10-07 216 | ### Added 217 | - CancelledError catching in consume task 218 | 219 | ## [1.3.10] - 2019-10-05 220 | ### Added 221 | - Producer is_running attribute 222 | 223 | ## [1.3.9] - 2019-10-03 224 | ### Added 225 | - Check if the new producer is already registered before channel registration 226 | 227 | ## [1.3.8] - 2019-10-02 228 | ### Fixed 229 | - kwargs argument cython compatibility 230 | 231 | ## [1.3.7] - 2019-09-25 232 | ### Changed 233 | - Cython compilation directives (optimization purposes) 234 | 235 | ## [1.3.6] - 2019-09-22 236 | ### Fixed 237 | - Fix internal consumer callback 238 | 239 | ## [1.3.5] - 2019-09-21 240 | ### Fixed 241 | - Travis channel '__check_producers_state()' method crash when compiled 242 | 243 | ## [1.3.4] - 2019-09-09 244 | ### Fixed 245 | - Producer 'wait_for_processing' declaration 246 | 247 | ## [1.3.3] - 2019-09-08 248 | ### Changed 249 | - Channel 'get_consumer_from_filters' manage wildcard filters 250 | 251 | ## Related issue 252 | - #9 [Channel] Implement consumer filter 253 | 254 | ## [1.3.2] - 2019-09-07 255 | ### Changed 256 | - Channel 'get_consumer_from_filters' method compilation from cython to python 257 | 258 | ## [1.3.1] - 2019-09-07 259 | ### Added 260 | - Producer supervised consumer wait method 'wait_for_processing' 261 | 262 | ### Changed 263 | - Consumer tests 264 | 265 | ## [1.3.0] - 2019-09-07 266 | ### Added 267 | - Supervised Consumer that notify the consumption end 268 | 269 | ### Fixed 270 | - Consumer tests 271 | 272 | ## [1.2.0] - 2019-09-04 273 | ### Added 274 | - Channel add_new_consumer method to add a new consumer with filters 275 | - Channel get_consumer_from_filters to get a list of consumers that match with filters 276 | 277 | ### Changed 278 | - Channel new_consumer method can handle a consumer filters dict 279 | - Channel __add_new_consumer_and_run to use consumer filters and not consumer name 280 | 281 | ## [1.1.14] - 2019-08-29 282 | ### Added 283 | - Tests 284 | 285 | ## [1.1.13] - 2019-08-29 286 | ### Fixed 287 | - Internal consumer implementation 288 | 289 | ## [1.1.12] - 2019-08-29 290 | ### Fixed 291 | - Internal consumer consume method 292 | 293 | ## [1.1.11] - 2019-08-28 294 | ### Added 295 | - Internal consumer : the callback is defined into the consumer class and is not a constructor param anymore 296 | 297 | ## [1.1.10] - 2019-08-27 298 | ### Added 299 | - Consumer instance param in channel new_consumer to handle a new consumer with an already created instance 300 | 301 | ## [1.1.9] - 2019-08-26 302 | ### Fixed 303 | - Queue to async 304 | 305 | ## [1.1.8] - 2019-08-16 306 | ### Changed 307 | - Replaced Channels class by orphan public methods 308 | 309 | ### Removed 310 | - Channels class 311 | 312 | ## [1.1.7] - 2019-08-14 313 | ### Added 314 | - Setup install requirements 315 | 316 | ## [1.1.6] - 2019-08-14 317 | ### Changed 318 | - ChannelInstances class to commons singleton class implementation 319 | 320 | ## [1.1.5] - 2019-08-13 321 | ### Fixed 322 | - Changed Producer attributes to public 323 | - Changed Consumer attributes to public 324 | 325 | ## [1.1.4] - 2019-08-13 326 | ### Fixed 327 | - Channel is_paused attribute to public 328 | 329 | ## [1.1.3] - 2019-08-13 330 | ### Added 331 | - Producer pause and resume methods 332 | - Channel producers pause/resume management 333 | 334 | ### Related issue 335 | - [Producer] Implement channel pause and resume #8 336 | 337 | ## [1.1.2] - 2019-08-12 338 | ### Fixed 339 | - Channel init_consumer_if_necessary object key type 340 | 341 | ## [1.1.1] - 2019-08-12 342 | ### Fixed 343 | - Channel init_consumer_if_necessary iterable type 344 | 345 | ## [1.1.0] - 2019-08-11 346 | ### Added 347 | - Channel global tests 348 | 349 | ### Changed 350 | - Migrate Consumer start, run and stop methods to async 351 | 352 | ### Fixed 353 | - Consumer attributes queue and filter_size to public 354 | - Channel start and stop methods 355 | - Channels methods Cython compliance 356 | 357 | ## [1.0.12] - 2019-08-09 358 | ### Changed 359 | - PyDoc fixes 360 | 361 | ## [1.0.11] - 2019-08-09 362 | ### Added 363 | - Channel new consumer methods 364 | 365 | ## [1.0.10] - 2019-08-08 366 | ### Added 367 | - Channel internal_producer 368 | 369 | ## [1.0.9] - 2019-08-07 370 | ### Changed 371 | - Channel creation utility refactored in two different methods 372 | 373 | ## [1.0.8] - 2019-08-06 374 | ### Added 375 | - Channel creation utility 376 | 377 | ## [1.0.7] - 2019-08-04 378 | ### Added 379 | - Channel 'modify' method that calls all producers modify method 380 | - Channel 'register_producer' method to register all its producers. 381 | 382 | ## [1.0.6] - 2019-08-03 383 | ### Added 384 | - constants.py file 385 | 386 | ### Modified 387 | - Import way with __init__.py files 388 | 389 | ### Removed 390 | - Unused evaluator package 391 | 392 | ## [1.0.5] - 2019-08-03 393 | ### Added 394 | - Producer 'modify' method 395 | 396 | ## [1.0.4] - 2019-06-10 397 | ### Fixed 398 | - ExchangeChannel deprecated imports 399 | 400 | ## [1.0.3] - 2019-06-10 401 | ### Removed 402 | - [OctoBot-Trading] migrate exchange channels to OctoBot-Trading 403 | 404 | ## [1.0.2] - 2019-06-09 405 | ### Fixed 406 | - [OctoBot-Trading] Exchange get_name() method deprecated 407 | 408 | ## [1.0.1] - 2019-05-27 409 | ### Changed 410 | - Migrate to cython with pure python 411 | -------------------------------------------------------------------------------- /tests/test_synchronized.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | import asyncio 17 | import pytest_asyncio 18 | 19 | import mock 20 | import pytest 21 | 22 | import async_channel.channels as channels 23 | import async_channel.producer as channel_producer 24 | import async_channel.util as util 25 | import tests 26 | 27 | TEST_SYNCHRONIZED_CHANNEL = "SynchronizedTest" 28 | 29 | 30 | class SynchronizedProducerTest(channel_producer.Producer): 31 | async def send(self, data, **kwargs): 32 | await super().send(data) 33 | await channels.get_chan(TEST_SYNCHRONIZED_CHANNEL).stop() 34 | 35 | async def pause(self): 36 | pass 37 | 38 | async def resume(self): 39 | pass 40 | 41 | 42 | class SynchronizedChannelTest(channels.Channel): 43 | PRODUCER_CLASS = SynchronizedProducerTest 44 | CONSUMER_CLASS = tests.EmptyTestConsumer 45 | 46 | 47 | @pytest_asyncio.fixture 48 | async def synchronized_channel(): 49 | yield await util.create_channel_instance(SynchronizedChannelTest, channels.set_chan, is_synchronized=True) 50 | channels.del_chan(TEST_SYNCHRONIZED_CHANNEL) 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_producer_synchronized_perform_consumers_queue_with_one_consumer(synchronized_channel): 55 | async def callback(): 56 | pass 57 | 58 | test_consumer = await synchronized_channel.new_consumer(callback) 59 | 60 | producer = SynchronizedProducerTest(channels.get_chan(TEST_SYNCHRONIZED_CHANNEL)) 61 | await producer.run() 62 | 63 | with mock.patch.object(test_consumer, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_callback: 64 | await producer.send({}) 65 | await tests.mock_was_not_called(mocked_test_consumer_callback) 66 | await producer.synchronized_perform_consumers_queue(1, True, 1) 67 | await tests.mock_was_called_once(mocked_test_consumer_callback) 68 | 69 | 70 | @pytest.mark.asyncio 71 | async def test_producer_synchronized_perform_supervised_consumer_with_processing_empty_queue(synchronized_channel): 72 | continue_event = asyncio.Event() 73 | calls = [] 74 | done_calls = [] 75 | 76 | async def callback(): 77 | calls.append(None) 78 | await asyncio.wait_for(continue_event.wait(), 1) 79 | done_calls.append(None) 80 | 81 | async def set_event_task(): 82 | continue_event.set() 83 | 84 | # use supervised consumers 85 | synchronized_channel.CONSUMER_CLASS = tests.EmptyTestSupervisedConsumer 86 | test_consumer = await synchronized_channel.new_consumer(callback) 87 | 88 | producer = SynchronizedProducerTest(channels.get_chan(TEST_SYNCHRONIZED_CHANNEL)) 89 | await producer.run() 90 | 91 | await producer.send({}) 92 | await test_consumer.run() 93 | try: 94 | await tests.wait_asyncio_next_cycle() 95 | # called already yet 96 | assert calls == [None] 97 | # call not finished 98 | assert done_calls == [] 99 | # queue is empty 100 | assert test_consumer.queue.qsize() == 0 101 | asyncio.create_task(set_event_task()) 102 | # wait for call to finish even though queue is empty => does not work as we are not joining the 103 | # current processing 104 | await producer.synchronized_perform_consumers_queue(1, False, 1) 105 | assert done_calls == [] 106 | # wait for call to finish even though queue is empty with join 107 | await producer.synchronized_perform_consumers_queue(1, True, 1) 108 | # ensure call actually finished (if we did not join the current task, this call would not have finished) 109 | assert done_calls == [None] 110 | finally: 111 | await test_consumer.stop() 112 | 113 | 114 | @pytest.mark.asyncio 115 | async def test_join(): 116 | # just test this does not throw an error on base consumers 117 | base_consumer = tests.EmptyTestConsumer(None) 118 | await base_consumer.join(1) 119 | 120 | supervised_consumer = tests.EmptyTestSupervisedConsumer(None) 121 | assert supervised_consumer.idle.is_set() 122 | 123 | with mock.patch.object(supervised_consumer.idle, "wait", mock.AsyncMock()) as wait_mock: 124 | await supervised_consumer.join(1) 125 | wait_mock.assert_not_called() 126 | 127 | supervised_consumer.idle.clear() 128 | await supervised_consumer.join(1) 129 | wait_mock.assert_called_once() 130 | 131 | 132 | @pytest.mark.asyncio 133 | async def test_join_queue(): 134 | base_consumer = tests.EmptyTestConsumer(None) 135 | with mock.patch.object(base_consumer.queue, "join", mock.AsyncMock()) as join_mock: 136 | await base_consumer.join_queue() 137 | join_mock.assert_not_called() 138 | 139 | supervised_consumer = tests.EmptyTestSupervisedConsumer(None) 140 | with mock.patch.object(supervised_consumer.queue, "join", mock.AsyncMock()) as join_mock: 141 | await supervised_consumer.join_queue() 142 | join_mock.assert_called_once() 143 | 144 | 145 | @pytest.mark.asyncio 146 | async def test_synchronized_no_tasks(synchronized_channel): 147 | async def callback(): 148 | pass 149 | 150 | test_consumer = await synchronized_channel.new_consumer(callback) 151 | 152 | producer = SynchronizedProducerTest(channels.get_chan(TEST_SYNCHRONIZED_CHANNEL)) 153 | await producer.run() 154 | 155 | assert test_consumer.consume_task is None 156 | assert producer.produce_task is None 157 | 158 | 159 | @pytest.mark.asyncio 160 | async def test_is_consumers_queue_empty_with_one_consumer(synchronized_channel): 161 | async def callback(): 162 | pass 163 | 164 | await synchronized_channel.new_consumer(callback) 165 | 166 | producer = SynchronizedProducerTest(channels.get_chan(TEST_SYNCHRONIZED_CHANNEL)) 167 | await producer.run() 168 | 169 | await producer.send({}) 170 | assert not producer.is_consumers_queue_empty(1) 171 | assert not producer.is_consumers_queue_empty(2) 172 | await producer.synchronized_perform_consumers_queue(1, True, 1) 173 | assert producer.is_consumers_queue_empty(1) 174 | assert producer.is_consumers_queue_empty(2) 175 | 176 | 177 | @pytest.mark.asyncio 178 | async def test_is_consumers_queue_empty_with_multiple_consumers(synchronized_channel): 179 | async def callback(): 180 | pass 181 | 182 | await synchronized_channel.new_consumer(callback) 183 | await synchronized_channel.new_consumer(callback) 184 | await synchronized_channel.new_consumer(callback, priority_level=2) 185 | await synchronized_channel.new_consumer(callback, priority_level=2) 186 | await synchronized_channel.new_consumer(callback, priority_level=3) 187 | 188 | producer = SynchronizedProducerTest(channels.get_chan(TEST_SYNCHRONIZED_CHANNEL)) 189 | await producer.run() 190 | 191 | await producer.send({}) 192 | assert not producer.is_consumers_queue_empty(1) 193 | assert not producer.is_consumers_queue_empty(2) 194 | assert not producer.is_consumers_queue_empty(3) 195 | await producer.synchronized_perform_consumers_queue(1, True, 1) 196 | assert producer.is_consumers_queue_empty(1) 197 | assert not producer.is_consumers_queue_empty(2) 198 | assert not producer.is_consumers_queue_empty(3) 199 | await producer.synchronized_perform_consumers_queue(2, True, 1) 200 | assert producer.is_consumers_queue_empty(1) 201 | assert producer.is_consumers_queue_empty(2) 202 | assert not producer.is_consumers_queue_empty(3) 203 | await producer.synchronized_perform_consumers_queue(2, True, 1) 204 | assert not producer.is_consumers_queue_empty(3) 205 | await producer.synchronized_perform_consumers_queue(3, True, 1) 206 | assert producer.is_consumers_queue_empty(3) 207 | 208 | 209 | @pytest.mark.asyncio 210 | async def test_producer_synchronized_perform_consumers_queue_with_multiple_consumer(synchronized_channel): 211 | async def callback(): 212 | pass 213 | 214 | test_consumer_1_1 = await synchronized_channel.new_consumer(callback) 215 | test_consumer_1_2 = await synchronized_channel.new_consumer(callback) 216 | test_consumer_2_1 = await synchronized_channel.new_consumer(callback, priority_level=2) 217 | test_consumer_2_2 = await synchronized_channel.new_consumer(callback, priority_level=2) 218 | test_consumer_3_1 = await synchronized_channel.new_consumer(callback, priority_level=3) 219 | 220 | producer = SynchronizedProducerTest(channels.get_chan(TEST_SYNCHRONIZED_CHANNEL)) 221 | await producer.run() 222 | 223 | with mock.patch.object(test_consumer_1_1, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_1_1_callback, \ 224 | mock.patch.object(test_consumer_1_2, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_1_2_callback, \ 225 | mock.patch.object(test_consumer_2_1, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_2_1_callback, \ 226 | mock.patch.object(test_consumer_2_2, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_2_2_callback, \ 227 | mock.patch.object(test_consumer_3_1, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_3_1_callback: 228 | await producer.send({}) 229 | await tests.mock_was_not_called(mocked_test_consumer_1_1_callback) 230 | await tests.mock_was_not_called(mocked_test_consumer_1_2_callback) 231 | await tests.mock_was_not_called(mocked_test_consumer_2_1_callback) 232 | await tests.mock_was_not_called(mocked_test_consumer_2_2_callback) 233 | await tests.mock_was_not_called(mocked_test_consumer_3_1_callback) 234 | await producer.synchronized_perform_consumers_queue(1, True, 1) 235 | await tests.mock_was_called_once(mocked_test_consumer_1_1_callback) 236 | await tests.mock_was_called_once(mocked_test_consumer_1_2_callback) 237 | await tests.mock_was_not_called(mocked_test_consumer_2_1_callback) 238 | await tests.mock_was_not_called(mocked_test_consumer_2_2_callback) 239 | await tests.mock_was_not_called(mocked_test_consumer_3_1_callback) 240 | await producer.synchronized_perform_consumers_queue(2, True, 1) 241 | await tests.mock_was_called_once(mocked_test_consumer_1_1_callback) 242 | await tests.mock_was_called_once(mocked_test_consumer_1_2_callback) 243 | await tests.mock_was_called_once(mocked_test_consumer_2_1_callback) 244 | await tests.mock_was_called_once(mocked_test_consumer_2_2_callback) 245 | await tests.mock_was_not_called(mocked_test_consumer_3_1_callback) 246 | assert not producer.is_consumers_queue_empty(3) 247 | await producer.synchronized_perform_consumers_queue(3, True, 1) 248 | await tests.mock_was_called_once(mocked_test_consumer_3_1_callback) 249 | assert producer.is_consumers_queue_empty(1) 250 | assert producer.is_consumers_queue_empty(2) 251 | assert producer.is_consumers_queue_empty(3) 252 | 253 | with mock.patch.object(test_consumer_1_1, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_1_1_callback, \ 254 | mock.patch.object(test_consumer_1_2, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_1_2_callback, \ 255 | mock.patch.object(test_consumer_2_1, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_2_1_callback, \ 256 | mock.patch.object(test_consumer_2_2, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_2_2_callback, \ 257 | mock.patch.object(test_consumer_3_1, 'callback', new=mock.AsyncMock()) as mocked_test_consumer_3_1_callback: 258 | await producer.send({}) 259 | await tests.mock_was_not_called(mocked_test_consumer_1_1_callback) 260 | await tests.mock_was_not_called(mocked_test_consumer_1_2_callback) 261 | await tests.mock_was_not_called(mocked_test_consumer_2_1_callback) 262 | await tests.mock_was_not_called(mocked_test_consumer_2_2_callback) 263 | await tests.mock_was_not_called(mocked_test_consumer_3_1_callback) 264 | assert not producer.is_consumers_queue_empty(2) 265 | await producer.synchronized_perform_consumers_queue(3, True, 1) 266 | await tests.mock_was_called_once(mocked_test_consumer_1_1_callback) 267 | await tests.mock_was_called_once(mocked_test_consumer_1_2_callback) 268 | await tests.mock_was_called_once(mocked_test_consumer_2_1_callback) 269 | await tests.mock_was_called_once(mocked_test_consumer_2_2_callback) 270 | await tests.mock_was_called_once(mocked_test_consumer_3_1_callback) 271 | assert producer.is_consumers_queue_empty(1) 272 | assert producer.is_consumers_queue_empty(2) 273 | assert producer.is_consumers_queue_empty(3) 274 | -------------------------------------------------------------------------------- /async_channel/channels/channel.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-many-positional-arguments 2 | # Drakkar-Software Async-Channel 3 | # Copyright (c) Drakkar-Software, All rights reserved. 4 | # 5 | # This library is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU Lesser General Public 7 | # License as published by the Free Software Foundation; either 8 | # version 3.0 of the License, or (at your option) any later version. 9 | # 10 | # This library is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public 16 | # License along with this library. 17 | """ 18 | Defines the channel core class : Channel 19 | """ 20 | import typing 21 | 22 | import async_channel.util.logging_util as logging 23 | import async_channel.enums 24 | import async_channel.channels.channel_instances as channel_instances 25 | 26 | 27 | # pylint: disable=undefined-variable, not-callable 28 | class Channel: 29 | """ 30 | A Channel is the object to connect a producer / producers class(es) to a consumer / consumers class(es) 31 | It contains a registered consumers dict to notify every consumer when a producer 'send' something. 32 | It contains a registered producers list to allow producer modification through 'modify'. 33 | To access channels a 'Channels' singleton is created to manage instances. 34 | """ 35 | 36 | # Channel producer class 37 | PRODUCER_CLASS = None 38 | 39 | # Channel consumer class 40 | CONSUMER_CLASS = None 41 | 42 | # Consumer instance in consumer filters 43 | INSTANCE_KEY = "consumer_instance" 44 | 45 | # Channel default consumer priority level 46 | DEFAULT_PRIORITY_LEVEL = ( 47 | async_channel.enums.ChannelConsumerPriorityLevels.HIGH.value 48 | ) 49 | 50 | def __init__(self): 51 | self.logger = logging.get_logger(self.__class__.__name__) 52 | 53 | # Channel unique id 54 | self.chan_id = None 55 | 56 | # Channel subscribed producers list 57 | self.producers = [] 58 | 59 | # Channel subscribed consumers list 60 | self.consumers = [] 61 | 62 | # Used to perform global send from non-producer context 63 | self.internal_producer = None 64 | 65 | # Used to save producers state (paused or not) 66 | self.is_paused = True 67 | 68 | # Used to synchronize producers and consumer 69 | self.is_synchronized = False 70 | 71 | @classmethod 72 | def get_name(cls) -> str: 73 | """ 74 | Default implementation is to return the name of the class without the 'Channel' substring 75 | :returns the channel name 76 | """ 77 | return cls.__name__.replace("Channel", "") 78 | 79 | # pylint: disable=too-many-arguments 80 | async def new_consumer( 81 | self, 82 | callback: object = None, 83 | consumer_filters: dict = None, 84 | internal_consumer: object = None, 85 | size: int = 0, 86 | priority_level: int = DEFAULT_PRIORITY_LEVEL, 87 | ) -> CONSUMER_CLASS: 88 | """ 89 | Create an appropriate consumer instance for this async_channel and add it to the consumer list 90 | Should end by calling '_check_producers_state' 91 | :param callback: method that should be called when consuming the queue 92 | :param consumer_filters: the consumer filters 93 | :param size: queue size, default 0 94 | :param priority_level: used by Producers the lowest level has the highest priority 95 | :param internal_consumer: internal consumer instance to use if specified 96 | :return: consumer instance created 97 | """ 98 | consumer = ( 99 | internal_consumer 100 | if internal_consumer 101 | else self.CONSUMER_CLASS(callback, size=size, priority_level=priority_level) 102 | ) 103 | await self._add_new_consumer_and_run(consumer, consumer_filters) 104 | await self._check_producers_state() 105 | return consumer 106 | 107 | # pylint: disable=unused-argument 108 | async def _add_new_consumer_and_run( 109 | self, consumer: CONSUMER_CLASS, consumer_filters: dict, **kwargs 110 | ) -> None: 111 | """ 112 | Should be called by 'new_consumer' to add the consumer to self.consumers and call 'consumer.run()' 113 | :param consumer: the consumer to add 114 | :param kwargs: additional params for consumer list 115 | :return: None 116 | """ 117 | if consumer_filters is None: 118 | consumer_filters = {} 119 | 120 | self.add_new_consumer(consumer, consumer_filters) 121 | await consumer.run(with_task=not self.is_synchronized) 122 | 123 | def add_new_consumer(self, consumer, consumer_filters) -> None: 124 | """ 125 | Add a new consumer to consumer list with filters 126 | :param consumer: the consumer to add 127 | :param consumer_filters: the consumer selection filters (used by 'get_consumer_from_filters') 128 | :return: None 129 | """ 130 | consumer_filters[self.INSTANCE_KEY] = consumer 131 | self.consumers.append(consumer_filters) 132 | 133 | def get_consumer_from_filters(self, consumer_filters) -> list: 134 | """ 135 | Returns the instance filtered consumers list 136 | WARNING: 137 | >>> get_consumer_from_filters({"A": 1}) 138 | Can return a consumer described by {"A": True} because in python 1 == True 139 | :param consumer_filters: The consumer filters dict 140 | :return: the filtered consumer list 141 | """ 142 | return self._filter_consumers(consumer_filters) 143 | 144 | def get_consumers(self) -> list: 145 | """ 146 | Returns all consumers instance 147 | Can be overwritten according to the class needs 148 | :return: the subscribed consumers list 149 | """ 150 | return [consumer[self.INSTANCE_KEY] for consumer in self.consumers] 151 | 152 | def get_prioritized_consumers(self, priority_level) -> list: 153 | """ 154 | Returns all consumers instance 155 | Can be overwritten according to the class needs 156 | :return: the subscribed consumers list 157 | """ 158 | return [ 159 | consumer[self.INSTANCE_KEY] 160 | for consumer in self.consumers 161 | if consumer[self.INSTANCE_KEY].priority_level <= priority_level 162 | ] 163 | 164 | def _filter_consumers(self, consumer_filters) -> list: 165 | """ 166 | Returns the consumers that match the selection 167 | Returns all consumer instances if consumer_filter is empty 168 | :param consumer_filters: listed consumer filters 169 | :return: the list of the filtered consumers 170 | """ 171 | return [ 172 | consumer[self.INSTANCE_KEY] 173 | for consumer in self.consumers 174 | if _check_filters(consumer, consumer_filters) 175 | ] 176 | 177 | async def remove_consumer(self, consumer: CONSUMER_CLASS) -> None: 178 | """ 179 | Should be overwritten according to the class needs 180 | Should end by calling '_check_producers_state' and then 'consumer.stop' 181 | :param consumer: consumer instance to remove from consumers list 182 | """ 183 | for consumer_candidate in self.consumers: 184 | if consumer == consumer_candidate[self.INSTANCE_KEY]: 185 | self.consumers.remove(consumer_candidate) 186 | await self._check_producers_state() 187 | await consumer.stop() 188 | 189 | async def _check_producers_state(self) -> None: 190 | """ 191 | Checks if producers should be paused or resumed after a consumer addition or removal 192 | """ 193 | if self._should_pause_producers(): 194 | self.is_paused = True 195 | for producer in self.get_producers(): 196 | await producer.pause() 197 | return 198 | if self._should_resume_producers(): 199 | self.is_paused = False 200 | for producer in self.get_producers(): 201 | await producer.resume() 202 | 203 | def _should_pause_producers(self) -> bool: 204 | """ 205 | Check if channel producers should be paused 206 | :return: True if channel producers should be paused 207 | """ 208 | if self.is_paused: 209 | return False 210 | if not self.get_consumers(): 211 | return True 212 | for consumer in self.get_consumers(): 213 | if ( 214 | consumer.priority_level 215 | < async_channel.ChannelConsumerPriorityLevels.OPTIONAL.value 216 | ): 217 | return False 218 | return True 219 | 220 | def _should_resume_producers(self) -> bool: 221 | """ 222 | Check if channel producers should be resumed 223 | :return: True if channel producers should be resumed 224 | """ 225 | if not self.is_paused: 226 | return False 227 | if not self.get_consumers(): 228 | return False 229 | for consumer in self.get_consumers(): 230 | if ( 231 | consumer.priority_level 232 | < async_channel.ChannelConsumerPriorityLevels.OPTIONAL.value 233 | ): 234 | return True 235 | return False 236 | 237 | async def register_producer(self, producer) -> None: 238 | """ 239 | Add the producer to producers list 240 | Can be overwritten to perform additional action when registering 241 | Should end by calling 'pause' if self.is_paused 242 | :param Producer producer: created channel producer to register 243 | """ 244 | if producer not in self.producers: 245 | self.producers.append(producer) 246 | 247 | if self.is_paused: 248 | await producer.pause() 249 | 250 | def unregister_producer(self, producer) -> None: 251 | """ 252 | Remove the producer from producers list 253 | Can be overwritten to perform additional action when registering 254 | :param Producer producer: created channel producer to unregister 255 | """ 256 | if producer in self.producers: 257 | self.producers.remove(producer) 258 | 259 | def get_producers(self) -> typing.Iterable: 260 | """ 261 | Should be overwritten according to the class needs 262 | :return: async_channel producers iterable 263 | """ 264 | return self.producers 265 | 266 | async def start(self) -> None: 267 | """ 268 | Call each registered consumers start method 269 | """ 270 | for consumer in self.get_consumers(): 271 | await consumer.start() 272 | 273 | async def stop(self) -> None: 274 | """ 275 | Call each registered consumers and producers stop method 276 | """ 277 | for consumer in self.get_consumers(): 278 | await consumer.stop() 279 | 280 | for producer in self.get_producers(): 281 | await producer.stop() 282 | 283 | if self.internal_producer is not None: 284 | await self.internal_producer.stop() 285 | 286 | def flush(self) -> None: 287 | """ 288 | Flush the channel object before stopping 289 | """ 290 | if self.internal_producer is not None: 291 | self.internal_producer.channel = None 292 | for producer in self.get_producers(): 293 | producer.channel = None 294 | 295 | async def run(self) -> None: 296 | """ 297 | Call each registered consumers run method 298 | """ 299 | for consumer in self.get_consumers(): 300 | await consumer.run(with_task=not self.is_synchronized) 301 | 302 | async def modify(self, **kwargs) -> None: 303 | """ 304 | Call each registered producers modify method 305 | """ 306 | for producer in self.get_producers(): 307 | await producer.modify(**kwargs) 308 | 309 | def get_internal_producer(self, **kwargs) -> PRODUCER_CLASS: 310 | """ 311 | Returns internal producer if exists else creates it 312 | :param kwargs: arguments for internal producer __init__ 313 | :return: internal producer instance 314 | """ 315 | if not self.internal_producer: 316 | try: 317 | self.internal_producer = self.PRODUCER_CLASS(self, **kwargs) 318 | except TypeError: 319 | self.logger.exception("PRODUCER_CLASS not defined") 320 | raise 321 | return self.internal_producer 322 | 323 | 324 | def set_chan(chan, name) -> Channel: 325 | """ 326 | Set a new Channel instance in the channels list according to channel name 327 | :param chan: new Channel instance 328 | :param name: name of the channel 329 | :return: the channel instance if succeed else raise a ValueError 330 | """ 331 | chan_name = name if name else chan.get_name() 332 | if chan_name not in channel_instances.ChannelInstances.instance().channels: 333 | channel_instances.ChannelInstances.instance().channels[chan_name] = chan 334 | return chan 335 | raise ValueError(f"Channel {chan_name} already exists.") 336 | 337 | 338 | def del_chan(name) -> None: 339 | """ 340 | Delete a Channel instance from the channels list according to channel name 341 | :param name: name of the channel to delete 342 | """ 343 | if name in channel_instances.ChannelInstances.instance().channels: 344 | channel_instances.ChannelInstances.instance().channels.pop(name, None) 345 | 346 | 347 | def get_chan(chan_name) -> Channel: 348 | """ 349 | Return the channel instance from channel name 350 | :param chan_name: the channel name 351 | :return: the Channel instance 352 | """ 353 | return channel_instances.ChannelInstances.instance().channels[chan_name] 354 | 355 | 356 | def _check_filters(consumer_filters, expected_filters) -> bool: 357 | """ 358 | Checks if the consumer match the specified filters 359 | Returns True if expected_filters is empty 360 | :param consumer_filters: consumer filters 361 | :param expected_filters: selected filters 362 | :return: True if the consumer match the selection, else False 363 | """ 364 | try: 365 | for key, value in expected_filters.items(): 366 | if value == async_channel.CHANNEL_WILDCARD: 367 | continue 368 | if isinstance(consumer_filters[key], list): 369 | if set(consumer_filters[key]) & {value, async_channel.CHANNEL_WILDCARD}: 370 | continue 371 | return False 372 | if consumer_filters[key] not in [value, async_channel.CHANNEL_WILDCARD]: 373 | return False 374 | return True 375 | except KeyError: 376 | return False 377 | -------------------------------------------------------------------------------- /standard.rc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist=octobot_commons.logging.logging_util 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 21 | # number of processors available to use. 22 | jobs=1 23 | 24 | # Control the amount of potential inferred values when inferring a single 25 | # object. This can help the performance when dealing with large functions or 26 | # complex, nested conditions. 27 | limit-inference-results=100 28 | 29 | # List of plugins (as comma separated values of python module names) to load, 30 | # usually to register additional checkers. 31 | load-plugins= 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=yes 35 | 36 | # Specify a configuration file. 37 | #rcfile= 38 | 39 | # Allow loading of arbitrary C extensions. Extensions are imported into the 40 | # active Python interpreter and may run arbitrary code. 41 | unsafe-load-any-extension=no 42 | 43 | 44 | [MESSAGES CONTROL] 45 | 46 | # Only show warnings with the listed confidence levels. Leave empty to show 47 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 48 | confidence= 49 | 50 | # Disable the message, report, category or checker with the given id(s). You 51 | # can either give multiple identifiers separated by comma (,) or put this 52 | # option multiple times (only on the command line, not in the configuration 53 | # file where it should appear only once). You can also use "--disable=all" to 54 | # disable everything first and then reenable specific checks. For example, if 55 | # you want to run only the similarities checker, you can use "--disable=all 56 | # --enable=similarities". If you want to run only the classes checker, but have 57 | # no Warning level messages displayed, use "--disable=all --enable=classes 58 | # --disable=W". 59 | disable=too-few-public-methods, 60 | logging-fstring-interpolation, 61 | consider-using-from-import 62 | 63 | # Enable the message, report, category or checker with the given id(s). You can 64 | # either give multiple identifier separated by comma (,) or put this option 65 | # multiple time (only on the command line, not in the configuration file where 66 | # it should appear only once). See also the "--disable" option for examples. 67 | enable=c-extension-no-member 68 | 69 | 70 | [REPORTS] 71 | 72 | # Python expression which should return a score less than or equal to 10. You 73 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 74 | # which contain the number of messages in each category, as well as 'statement' 75 | # which is the total number of statements analyzed. This score is used by the 76 | # global evaluation report (RP0004). 77 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 78 | 79 | # Template used to display messages. This is a python new-style format string 80 | # used to format the message information. See doc for all details. 81 | #msg-template= 82 | 83 | # Set the output format. Available formats are text, parseable, colorized, json 84 | # and msvs (visual studio). You can also give a reporter class, e.g. 85 | # mypackage.mymodule.MyReporterClass. 86 | output-format=text 87 | 88 | # Tells whether to display a full report or only the messages. 89 | reports=no 90 | 91 | # Activate the evaluation score. 92 | score=yes 93 | 94 | 95 | [REFACTORING] 96 | 97 | # Maximum number of nested blocks for function / method body 98 | max-nested-blocks=5 99 | 100 | # Complete name of functions that never returns. When checking for 101 | # inconsistent-return-statements if a never returning function is called then 102 | # it will be considered as an explicit return statement and no message will be 103 | # printed. 104 | never-returning-functions=sys.exit 105 | 106 | 107 | [SIMILARITIES] 108 | 109 | # Ignore comments when computing similarities. 110 | ignore-comments=yes 111 | 112 | # Ignore docstrings when computing similarities. 113 | ignore-docstrings=yes 114 | 115 | # Ignore imports when computing similarities. 116 | ignore-imports=no 117 | 118 | # Minimum lines number of a similarity. 119 | min-similarity-lines=4 120 | 121 | 122 | [LOGGING] 123 | 124 | # Format style used to check logging format string. `old` means using % 125 | # formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. 126 | logging-format-style=old 127 | 128 | # Logging modules to check that the string format arguments are in logging 129 | # function parameter format. 130 | logging-modules=logging 131 | 132 | 133 | [STRING] 134 | 135 | # This flag controls whether the implicit-str-concat-in-sequence should 136 | # generate a warning on implicit string concatenation in sequences defined over 137 | # several lines. 138 | check-str-concat-over-line-jumps=no 139 | 140 | 141 | [SPELLING] 142 | 143 | # Limits count of emitted suggestions for spelling mistakes. 144 | max-spelling-suggestions=4 145 | 146 | # Spelling dictionary name. Available dictionaries: none. To make it work, 147 | # install the python-enchant package. 148 | spelling-dict= 149 | 150 | # List of comma separated words that should not be checked. 151 | spelling-ignore-words= 152 | 153 | # A path to a file that contains the private dictionary; one word per line. 154 | spelling-private-dict-file= 155 | 156 | # Tells whether to store unknown words to the private dictionary (see the 157 | # --spelling-private-dict-file option) instead of raising a message. 158 | spelling-store-unknown-words=no 159 | 160 | 161 | [FORMAT] 162 | 163 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 164 | expected-line-ending-format= 165 | 166 | # Regexp for a line that is allowed to be longer than the limit. 167 | ignore-long-lines=^\s*(# )??$ 168 | 169 | # Number of spaces of indent required inside a hanging or continued line. 170 | indent-after-paren=4 171 | 172 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 173 | # tab). 174 | indent-string=' ' 175 | 176 | # Maximum number of characters on a single line. 177 | max-line-length=120 178 | 179 | # Maximum number of lines in a module. 180 | max-module-lines=1000 181 | 182 | # Allow the body of a class to be on the same line as the declaration if body 183 | # contains single statement. 184 | single-line-class-stmt=no 185 | 186 | # Allow the body of an if to be on the same line as the test if there is no 187 | # else. 188 | single-line-if-stmt=no 189 | 190 | 191 | [MISCELLANEOUS] 192 | 193 | # List of note tags to take in consideration, separated by a comma. 194 | notes=FIXME, 195 | XXX, 196 | TODO 197 | 198 | 199 | [BASIC] 200 | 201 | # Naming style matching correct argument names. 202 | argument-naming-style=snake_case 203 | 204 | # Regular expression matching correct argument names. Overrides argument- 205 | # naming-style. 206 | #argument-rgx= 207 | 208 | # Naming style matching correct attribute names. 209 | attr-naming-style=snake_case 210 | 211 | # Regular expression matching correct attribute names. Overrides attr-naming- 212 | # style. 213 | #attr-rgx= 214 | 215 | # Bad variable names which should always be refused, separated by a comma. 216 | bad-names=foo, 217 | bar, 218 | baz, 219 | toto, 220 | tutu, 221 | tata 222 | 223 | # Naming style matching correct class attribute names. 224 | class-attribute-naming-style=any 225 | 226 | # Regular expression matching correct class attribute names. Overrides class- 227 | # attribute-naming-style. 228 | #class-attribute-rgx= 229 | 230 | # Naming style matching correct class names. 231 | class-naming-style=PascalCase 232 | 233 | # Regular expression matching correct class names. Overrides class-naming- 234 | # style. 235 | #class-rgx= 236 | 237 | # Naming style matching correct constant names. 238 | const-naming-style=UPPER_CASE 239 | 240 | # Regular expression matching correct constant names. Overrides const-naming- 241 | # style. 242 | #const-rgx= 243 | 244 | # Minimum line length for functions/classes that require docstrings, shorter 245 | # ones are exempt. 246 | docstring-min-length=-1 247 | 248 | # Naming style matching correct function names. 249 | function-naming-style=snake_case 250 | 251 | # Regular expression matching correct function names. Overrides function- 252 | # naming-style. 253 | #function-rgx= 254 | 255 | # Good variable names which should always be accepted, separated by a comma. 256 | good-names=i, 257 | j, 258 | k, 259 | ex, 260 | Run, 261 | _ 262 | 263 | # Include a hint for the correct naming format with invalid-name. 264 | include-naming-hint=no 265 | 266 | # Naming style matching correct inline iteration names. 267 | inlinevar-naming-style=any 268 | 269 | # Regular expression matching correct inline iteration names. Overrides 270 | # inlinevar-naming-style. 271 | #inlinevar-rgx= 272 | 273 | # Naming style matching correct method names. 274 | method-naming-style=snake_case 275 | 276 | # Regular expression matching correct method names. Overrides method-naming- 277 | # style. 278 | #method-rgx= 279 | 280 | # Naming style matching correct module names. 281 | module-naming-style=snake_case 282 | 283 | # Regular expression matching correct module names. Overrides module-naming- 284 | # style. 285 | #module-rgx= 286 | 287 | # Colon-delimited sets of names that determine each other's naming style when 288 | # the name regexes allow several styles. 289 | name-group= 290 | 291 | # Regular expression which should only match function or class names that do 292 | # not require a docstring. 293 | no-docstring-rgx=^_ 294 | 295 | # List of decorators that produce properties, such as abc.abstractproperty. Add 296 | # to this list to register other decorators that produce valid properties. 297 | # These decorators are taken in consideration only for invalid-name. 298 | property-classes=abc.abstractproperty 299 | 300 | # Naming style matching correct variable names. 301 | variable-naming-style=snake_case 302 | 303 | # Regular expression matching correct variable names. Overrides variable- 304 | # naming-style. 305 | #variable-rgx= 306 | 307 | 308 | [VARIABLES] 309 | 310 | # List of additional names supposed to be defined in builtins. Remember that 311 | # you should avoid defining new builtins when possible. 312 | additional-builtins= 313 | 314 | # Tells whether unused global variables should be treated as a violation. 315 | allow-global-unused-variables=yes 316 | 317 | # List of strings which can identify a callback function by name. A callback 318 | # name must start or end with one of those strings. 319 | callbacks=cb_, 320 | _cb 321 | 322 | # A regular expression matching the name of dummy variables (i.e. expected to 323 | # not be used). 324 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 325 | 326 | # Argument names that match this expression will be ignored. Default to name 327 | # with leading underscore. 328 | ignored-argument-names=_.*|^ignored_|^unused_ 329 | 330 | # Tells whether we should check for unused import in __init__ files. 331 | init-import=no 332 | 333 | # List of qualified module names which can have objects that can redefine 334 | # builtins. 335 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 336 | 337 | 338 | [TYPECHECK] 339 | 340 | # List of decorators that produce context managers, such as 341 | # contextlib.contextmanager. Add to this list to register other decorators that 342 | # produce valid context managers. 343 | contextmanager-decorators=contextlib.contextmanager 344 | 345 | # List of members which are set dynamically and missed by pylint inference 346 | # system, and so shouldn't trigger E1101 when accessed. Python regular 347 | # expressions are accepted. 348 | generated-members= 349 | 350 | # Tells whether missing members accessed in mixin class should be ignored. A 351 | # mixin class is detected if its name ends with "mixin" (case insensitive). 352 | ignore-mixin-members=yes 353 | 354 | # Tells whether to warn about missing members when the owner of the attribute 355 | # is inferred to be None. 356 | ignore-none=yes 357 | 358 | # This flag controls whether pylint should warn about no-member and similar 359 | # checks whenever an opaque object is returned when inferring. The inference 360 | # can return multiple potential results while evaluating a Python object, but 361 | # some branches might not be evaluated, which results in partial inference. In 362 | # that case, it might be useful to still emit no-member and other checks for 363 | # the rest of the inferred objects. 364 | ignore-on-opaque-inference=yes 365 | 366 | # List of class names for which member attributes should not be checked (useful 367 | # for classes with dynamically set attributes). This supports the use of 368 | # qualified names. 369 | ignored-classes=optparse.Values,thread._local,_thread._local 370 | 371 | # List of module names for which member attributes should not be checked 372 | # (useful for modules/projects where namespaces are manipulated during runtime 373 | # and thus existing member attributes cannot be deduced by static analysis). It 374 | # supports qualified module names, as well as Unix pattern matching. 375 | ignored-modules= 376 | 377 | # Show a hint with possible names when a member name was not found. The aspect 378 | # of finding the hint is based on edit distance. 379 | missing-member-hint=yes 380 | 381 | # The minimum edit distance a name should have in order to be considered a 382 | # similar match for a missing member name. 383 | missing-member-hint-distance=1 384 | 385 | # The total number of similar names that should be taken in consideration when 386 | # showing a hint for a missing member. 387 | missing-member-max-choices=1 388 | 389 | # List of decorators that change the signature of a decorated function. 390 | signature-mutators= 391 | 392 | 393 | [IMPORTS] 394 | 395 | # List of modules that can be imported at any level, not just the top level 396 | # one. 397 | allow-any-import-level= 398 | 399 | # Allow wildcard imports from modules that define __all__. 400 | allow-wildcard-with-all=no 401 | 402 | # Analyse import fallback blocks. This can be used to support both Python 2 and 403 | # 3 compatible code, which means that the block might have code that exists 404 | # only in one or another interpreter, leading to false positives when analysed. 405 | analyse-fallback-blocks=no 406 | 407 | # Deprecated modules which should not be used, separated by a comma. 408 | deprecated-modules=optparse,tkinter.tix 409 | 410 | # Create a graph of external dependencies in the given file (report RP0402 must 411 | # not be disabled). 412 | ext-import-graph= 413 | 414 | # Create a graph of every (i.e. internal and external) dependencies in the 415 | # given file (report RP0402 must not be disabled). 416 | import-graph= 417 | 418 | # Create a graph of internal dependencies in the given file (report RP0402 must 419 | # not be disabled). 420 | int-import-graph= 421 | 422 | # Force import order to recognize a module as part of the standard 423 | # compatibility libraries. 424 | known-standard-library= 425 | 426 | # Force import order to recognize a module as part of a third party library. 427 | known-third-party=enchant 428 | 429 | # Couples of modules and preferred modules, separated by a comma. 430 | preferred-modules= 431 | 432 | 433 | [CLASSES] 434 | 435 | # List of method names used to declare (i.e. assign) instance attributes. 436 | defining-attr-methods=__init__, 437 | __new__, 438 | setUp, 439 | __post_init__ 440 | 441 | # List of member names, which should be excluded from the protected access 442 | # warning. 443 | exclude-protected=_asdict, 444 | _fields, 445 | _replace, 446 | _source, 447 | _make 448 | 449 | # List of valid names for the first argument in a class method. 450 | valid-classmethod-first-arg=cls 451 | 452 | # List of valid names for the first argument in a metaclass class method. 453 | valid-metaclass-classmethod-first-arg=cls 454 | 455 | 456 | [DESIGN] 457 | 458 | # Maximum number of arguments for function / method. 459 | max-args=5 460 | 461 | # Maximum number of attributes for a class (see R0902). 462 | max-attributes=7 463 | 464 | # Maximum number of boolean expressions in an if statement (see R0916). 465 | max-bool-expr=5 466 | 467 | # Maximum number of branch for function / method body. 468 | max-branches=12 469 | 470 | # Maximum number of locals for function / method body. 471 | max-locals=15 472 | 473 | # Maximum number of parents for a class (see R0901). 474 | max-parents=7 475 | 476 | # Maximum number of public methods for a class (see R0904). 477 | max-public-methods=20 478 | 479 | # Maximum number of return / yield for function / method body. 480 | max-returns=6 481 | 482 | # Maximum number of statements in function / method body. 483 | max-statements=50 484 | 485 | # Minimum number of public methods for a class (see R0903). 486 | min-public-methods=2 487 | 488 | 489 | [EXCEPTIONS] 490 | 491 | # Exceptions that will emit a warning when being caught. Defaults to 492 | # "BaseException, Exception". 493 | overgeneral-exceptions=builtins.BaseException, 494 | builtins.Exception 495 | -------------------------------------------------------------------------------- /tests/test_channel.py: -------------------------------------------------------------------------------- 1 | # Drakkar-Software Async-Channel 2 | # Copyright (c) Drakkar-Software, All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 3.0 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library. 16 | import os 17 | 18 | import pytest 19 | import pytest_asyncio 20 | import mock 21 | 22 | import async_channel.channels as channels 23 | import async_channel.util as util 24 | import async_channel 25 | 26 | import tests 27 | 28 | 29 | @pytest_asyncio.fixture 30 | async def test_channel(): 31 | channels.del_chan(tests.EMPTY_TEST_CHANNEL) 32 | yield await util.create_channel_instance(tests.EmptyTestChannel, channels.set_chan) 33 | await channels.get_chan(tests.EMPTY_TEST_CHANNEL).stop() 34 | 35 | 36 | @pytest.mark.asyncio 37 | async def test_get_chan(): 38 | class TestChannel(channels.Channel): 39 | pass 40 | 41 | channels.del_chan(tests.TEST_CHANNEL) 42 | await util.create_channel_instance(TestChannel, channels.set_chan) 43 | await channels.get_chan(tests.TEST_CHANNEL).stop() 44 | 45 | 46 | @pytest.mark.asyncio 47 | async def test_set_chan(): 48 | class TestChannel(channels.Channel): 49 | pass 50 | 51 | channels.del_chan(tests.TEST_CHANNEL) 52 | await util.create_channel_instance(TestChannel, channels.set_chan) 53 | with pytest.raises(ValueError): 54 | channels.set_chan(TestChannel(), name=TestChannel.get_name()) 55 | await channels.get_chan(tests.TEST_CHANNEL).stop() 56 | 57 | 58 | @pytest.mark.asyncio 59 | async def test_set_chan_using_default_name(): 60 | class TestChannel(channels.Channel): 61 | pass 62 | 63 | channels.del_chan(tests.TEST_CHANNEL) 64 | channel = TestChannel() 65 | returned_channel = channels.set_chan(channel, name=None) 66 | assert returned_channel is channel 67 | assert channel.get_name() is not None 68 | assert channels.ChannelInstances.instance().channels[channel.get_name()] == channel 69 | with pytest.raises(ValueError): 70 | channels.set_chan(TestChannel(), name=TestChannel.get_name()) 71 | await channels.get_chan(tests.TEST_CHANNEL).stop() 72 | 73 | 74 | @pytest.mark.asyncio 75 | async def test_get_internal_producer(): 76 | class TestChannel(channels.Channel): 77 | pass 78 | 79 | channels.del_chan(tests.TEST_CHANNEL) 80 | await util.create_channel_instance(TestChannel, channels.set_chan) 81 | with pytest.raises(TypeError): 82 | channels.get_chan(tests.TEST_CHANNEL).get_internal_producer() 83 | await channels.get_chan(tests.TEST_CHANNEL).stop() 84 | 85 | 86 | @pytest.mark.asyncio 87 | async def test_new_consumer_without_producer(test_channel): 88 | await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback) 89 | assert len(channels.get_chan(tests.EMPTY_TEST_CHANNEL).consumers) == 1 90 | 91 | 92 | @pytest.mark.asyncio 93 | async def test_new_consumer_without_filters(test_channel): 94 | consumer = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback) 95 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumers() == [consumer] 96 | 97 | 98 | @pytest.mark.asyncio 99 | async def test_new_consumer_with_filters(test_channel): 100 | consumer = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback, {"test_key": 1}) 101 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumers() == [consumer] 102 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({}) == [consumer] # returns all if empty 103 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 2}) == [] 104 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 1, "test2": 2}) == [] 105 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 1}) == [consumer] 106 | 107 | 108 | @pytest.mark.asyncio 109 | async def test_new_consumer_with_expected_wildcard_filters(test_channel): 110 | consumer = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback, {"test_key": 1, 111 | "test_key_2": "abc"}) 112 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumers() == [consumer] 113 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({}) == [consumer] # returns all if empty 114 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 1, "test_key_2": "abc"}) == [consumer] 115 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 116 | {"test_key": 1, "test_key_2": "abc", "test_key_3": 45}) == [] 117 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 118 | {"test_key": 1, "test_key_2": "abc", "test_key_3": async_channel.CHANNEL_WILDCARD}) == [consumer] 119 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 4, "test_key_2": "bc"}) == [] 120 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 1, "test_key_2": async_channel.CHANNEL_WILDCARD}) == [ 121 | consumer] 122 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 3, "test_key_2": async_channel.CHANNEL_WILDCARD}) == [] 123 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 124 | {"test_key": async_channel.CHANNEL_WILDCARD, "test_key_2": "abc"}) == [consumer] 125 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 126 | {"test_key": async_channel.CHANNEL_WILDCARD, "test_key_2": "a"}) == [] 127 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 128 | {"test_key": async_channel.CHANNEL_WILDCARD, "test_key_2": async_channel.CHANNEL_WILDCARD}) == [consumer] 129 | 130 | 131 | @pytest.mark.asyncio 132 | async def test_new_consumer_with_consumer_wildcard_filters(test_channel): 133 | consumer = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback, {"test_key": 1, 134 | "test_key_2": "abc", 135 | "test_key_3": async_channel.CHANNEL_WILDCARD}) 136 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumers() == [consumer] 137 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({}) == [consumer] # returns all if empty 138 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 1, "test_key_2": "abc"}) == [consumer] 139 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 140 | {"test_key": 1, "test_key_2": "abc", "test_key_3": 45}) == [consumer] 141 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 142 | {"test_key": 1, "test_key_2": "abc", "test_key_3": async_channel.CHANNEL_WILDCARD}) == [consumer] 143 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 4, "test_key_2": "bc"}) == [] 144 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 1, "test_key_2": async_channel.CHANNEL_WILDCARD}) == [ 145 | consumer] 146 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 1}) == [consumer] 147 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key_2": async_channel.CHANNEL_WILDCARD}) == [consumer] 148 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key_3": async_channel.CHANNEL_WILDCARD}) == [consumer] 149 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key_3": "e"}) == [consumer] 150 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"test_key": 3, "test_key_2": async_channel.CHANNEL_WILDCARD}) == [] 151 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 152 | {"test_key": async_channel.CHANNEL_WILDCARD, "test_key_2": "abc"}) == [consumer] 153 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 154 | {"test_key": async_channel.CHANNEL_WILDCARD, "test_key_2": "a"}) == [] 155 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 156 | {"test_key": async_channel.CHANNEL_WILDCARD, "test_key_2": "a", "test_key_3": async_channel.CHANNEL_WILDCARD}) == [] 157 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters( 158 | {"test_key": async_channel.CHANNEL_WILDCARD, "test_key_2": async_channel.CHANNEL_WILDCARD}) == [consumer] 159 | 160 | 161 | @pytest.mark.asyncio 162 | async def test_new_consumer_with_multiple_consumer_filtering(test_channel): 163 | consumers_descriptions = [ 164 | {"A": 1, "B": 2, "C": async_channel.CHANNEL_WILDCARD}, # 0 165 | {"A": False, "B": "BBBB", "C": async_channel.CHANNEL_WILDCARD}, # 1 166 | {"A": 3, "B": async_channel.CHANNEL_WILDCARD, "C": async_channel.CHANNEL_WILDCARD}, # 2 167 | {"A": async_channel.CHANNEL_WILDCARD, "B": async_channel.CHANNEL_WILDCARD, "C": async_channel.CHANNEL_WILDCARD}, # 3 168 | {"A": async_channel.CHANNEL_WILDCARD, "B": 2, "C": 1}, # 4 169 | {"A": True, "B": async_channel.CHANNEL_WILDCARD, "C": async_channel.CHANNEL_WILDCARD}, # 5 170 | {"A": None, "B": None, "C": async_channel.CHANNEL_WILDCARD}, # 6 171 | {"A": "PPP", "B": 1, "C": async_channel.CHANNEL_WILDCARD, "D": 5}, # 7 172 | {"A": async_channel.CHANNEL_WILDCARD, "B": 2, "C": "ABC"}, # 8 173 | {"A": async_channel.CHANNEL_WILDCARD, "B": True, "C": async_channel.CHANNEL_WILDCARD}, # 9 174 | {"A": async_channel.CHANNEL_WILDCARD, "B": 6, "C": async_channel.CHANNEL_WILDCARD, "D": async_channel.CHANNEL_WILDCARD}, # 10 175 | {"A": async_channel.CHANNEL_WILDCARD, "B": async_channel.CHANNEL_WILDCARD, "C": async_channel.CHANNEL_WILDCARD, "D": async_channel.CHANNEL_WILDCARD}, # 11 176 | {"A": None, "B": False, "C": "LLLL", "D": async_channel.CHANNEL_WILDCARD}, # 12 177 | {"A": None, "B": None, "C": async_channel.CHANNEL_WILDCARD, "D": None}, # 13 178 | {"A": async_channel.CHANNEL_WILDCARD, "B": 2, "C": async_channel.CHANNEL_WILDCARD, "D": None}, # 14 179 | {"A": async_channel.CHANNEL_WILDCARD, "B": [2, 3, 4, 5, 6], "C": async_channel.CHANNEL_WILDCARD, "D": None}, # 15 180 | {"A": async_channel.CHANNEL_WILDCARD, "B": ["A", 5, "G"], "C": async_channel.CHANNEL_WILDCARD, "D": None}, # 16 181 | {"A": [1, 2, 3], "B": 2, "C": async_channel.CHANNEL_WILDCARD, "D": async_channel.CHANNEL_WILDCARD}, # 17 182 | {"A": ["A", "B", "C"], "B": 2, "C": async_channel.CHANNEL_WILDCARD, "D": async_channel.CHANNEL_WILDCARD}, # 18 183 | {"A": async_channel.CHANNEL_WILDCARD, "B": [2], "C": async_channel.CHANNEL_WILDCARD, "D": async_channel.CHANNEL_WILDCARD}, # 19 184 | {"A": async_channel.CHANNEL_WILDCARD, "B": ["B"], "C": async_channel.CHANNEL_WILDCARD, "D": async_channel.CHANNEL_WILDCARD}, # 20 185 | {"A": 18, "B": ["A", "B", "C"], "C": ["---", "9", "#"], "D": async_channel.CHANNEL_WILDCARD}, # 21 186 | {"A": [9, 18], "B": ["B", "C", "D"], "C": ["---", "9", "#", "@", "{"], "D": ["P", "__str__"]} # 22 187 | ] 188 | 189 | consumers = [ 190 | await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback, consumers_description) 191 | for consumers_description in consumers_descriptions 192 | ] 193 | 194 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumers() == consumers 195 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({}) == consumers 196 | # Warning : consumer[5] is returned because 1 == True 197 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"A": 1, "B": "6"}) == \ 198 | [consumers[3], consumers[5], consumers[11]] 199 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"A": async_channel.CHANNEL_WILDCARD, "B": "G", "C": "1A"}) == \ 200 | [consumers[2], consumers[3], consumers[5], consumers[11], consumers[16]] 201 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"A": async_channel.CHANNEL_WILDCARD, "B": async_channel.CHANNEL_WILDCARD, 202 | "C": async_channel.CHANNEL_WILDCARD}) == consumers 203 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"A": 18, "B": "A", "C": "#"}) == \ 204 | [consumers[3], consumers[11], consumers[16], consumers[21]] 205 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"A": 18, "B": "C", "C": "#", "D": None}) == \ 206 | [consumers[11], consumers[21]] 207 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"A": 18, "B": "C", "C": "^", "D": None}) == \ 208 | [consumers[11]] 209 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumer_from_filters({"A": 18, "B": "C", "C": "#", "D": "__str__"}) == \ 210 | [consumers[11], consumers[21], consumers[22]] 211 | 212 | 213 | @pytest.mark.asyncio 214 | async def test_remove_consumer(test_channel): 215 | consumer = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback) 216 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumers() == [consumer] 217 | await channels.get_chan(tests.EMPTY_TEST_CHANNEL).remove_consumer(consumer) 218 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).get_consumers() == [] 219 | 220 | 221 | @pytest.mark.asyncio 222 | async def test_unregister_producer(test_channel): 223 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).producers == [] 224 | producer = tests.EmptyTestProducer(None) 225 | await channels.get_chan(tests.EMPTY_TEST_CHANNEL).register_producer(producer) 226 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).producers == [producer] 227 | 228 | 229 | @pytest.mark.asyncio 230 | async def test_register_producer(test_channel): 231 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).producers == [] 232 | producer = tests.EmptyTestProducer(None) 233 | await channels.get_chan(tests.EMPTY_TEST_CHANNEL).register_producer(producer) 234 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).producers == [producer] 235 | channels.get_chan(tests.EMPTY_TEST_CHANNEL).unregister_producer(producer) 236 | assert channels.get_chan(tests.EMPTY_TEST_CHANNEL).producers == [] 237 | 238 | 239 | @pytest.mark.asyncio 240 | async def test_flush(test_channel): 241 | producer = tests.EmptyTestProducer(test_channel) 242 | await test_channel.register_producer(producer) 243 | producer2 = tests.EmptyTestProducer(test_channel) 244 | await test_channel.register_producer(producer2) 245 | producer3 = tests.EmptyTestProducer(test_channel) 246 | test_channel.internal_producer = producer3 247 | 248 | assert producer3.channel is test_channel 249 | for producer in test_channel.producers: 250 | assert producer.channel is test_channel 251 | 252 | test_channel.flush() 253 | assert test_channel.internal_producer.channel is None 254 | for producer in test_channel.producers: 255 | assert producer.channel is None 256 | 257 | 258 | @pytest.mark.asyncio 259 | async def test_start(test_channel): 260 | consumer_1 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback) 261 | consumer_2 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback) 262 | with mock.patch.object(consumer_1, 'start', new=mock.AsyncMock()) as mocked_consumer_1_start: 263 | with mock.patch.object(consumer_2, 'start', new=mock.AsyncMock()) as mocked_consumer_2_start: 264 | await channels.get_chan(tests.EMPTY_TEST_CHANNEL).start() 265 | await tests.mock_was_called_once(mocked_consumer_1_start) 266 | await tests.mock_was_called_once(mocked_consumer_2_start) 267 | 268 | 269 | @pytest.mark.asyncio 270 | async def test_run(test_channel): 271 | consumer_1 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback) 272 | consumer_2 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer(tests.empty_test_callback) 273 | with mock.patch.object(consumer_1, 'run', new=mock.AsyncMock()) as mocked_consumer_1_run: 274 | with mock.patch.object(consumer_2, 'run', new=mock.AsyncMock()) as mocked_consumer_2_run: 275 | await channels.get_chan(tests.EMPTY_TEST_CHANNEL).run() 276 | await tests.mock_was_called_once(mocked_consumer_1_run) 277 | await tests.mock_was_called_once(mocked_consumer_2_run) 278 | 279 | 280 | @pytest.mark.asyncio 281 | async def test_modify(test_channel): 282 | producer = tests.EmptyTestProducer(test_channel) 283 | await test_channel.register_producer(producer) 284 | producer_2 = tests.EmptyTestProducer(test_channel) 285 | await test_channel.register_producer(producer_2) 286 | with mock.patch.object(producer, 'modify', new=mock.AsyncMock()) as mocked_producer_1_modify: 287 | with mock.patch.object(producer_2, 'modify', new=mock.AsyncMock()) as mocked_producer_2_modify: 288 | await channels.get_chan(tests.EMPTY_TEST_CHANNEL).modify() 289 | await tests.mock_was_called_once(mocked_producer_1_modify) 290 | await tests.mock_was_called_once(mocked_producer_2_modify) 291 | 292 | 293 | @pytest.mark.asyncio 294 | async def test_should_pause_producers_with_no_consumers(test_channel): 295 | producer = tests.EmptyTestProducer(test_channel) 296 | await test_channel.register_producer(producer) 297 | test_channel.is_paused = False 298 | if not os.getenv('CYTHON_IGNORE'): 299 | assert test_channel._should_pause_producers() 300 | 301 | 302 | @pytest.mark.asyncio 303 | async def test_should_pause_producers_when_already_paused(test_channel): 304 | producer = tests.EmptyTestProducer(test_channel) 305 | await test_channel.register_producer(producer) 306 | test_channel.is_paused = True 307 | if not os.getenv('CYTHON_IGNORE'): 308 | assert not test_channel._should_pause_producers() 309 | 310 | 311 | @pytest.mark.asyncio 312 | async def test_should_pause_producers_with_priority_consumers(test_channel): 313 | producer = tests.EmptyTestProducer(test_channel) 314 | await test_channel.register_producer(producer) 315 | consumer_1 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 316 | tests.empty_test_callback, 317 | priority_level=async_channel.ChannelConsumerPriorityLevels.HIGH.value) 318 | consumer_2 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 319 | tests.empty_test_callback, 320 | priority_level=async_channel.ChannelConsumerPriorityLevels.MEDIUM.value) 321 | consumer_3 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 322 | tests.empty_test_callback, 323 | priority_level=async_channel.ChannelConsumerPriorityLevels.OPTIONAL.value) 324 | test_channel.is_paused = False 325 | if not os.getenv('CYTHON_IGNORE'): 326 | assert not test_channel._should_pause_producers() 327 | await test_channel.remove_consumer(consumer_1) 328 | await test_channel.remove_consumer(consumer_2) 329 | await test_channel.remove_consumer(consumer_3) 330 | 331 | 332 | @pytest.mark.asyncio 333 | async def test_should_pause_producers_with_optional_consumers(test_channel): 334 | producer = tests.EmptyTestProducer(test_channel) 335 | await test_channel.register_producer(producer) 336 | consumer_1 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 337 | tests.empty_test_callback, 338 | priority_level=async_channel.ChannelConsumerPriorityLevels.OPTIONAL.value) 339 | consumer_2 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 340 | tests.empty_test_callback, 341 | priority_level=async_channel.ChannelConsumerPriorityLevels.OPTIONAL.value) 342 | consumer_3 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 343 | tests.empty_test_callback, 344 | priority_level=async_channel.ChannelConsumerPriorityLevels.OPTIONAL.value) 345 | test_channel.is_paused = False 346 | if not os.getenv('CYTHON_IGNORE'): 347 | assert test_channel._should_pause_producers() 348 | await test_channel.remove_consumer(consumer_1) 349 | await test_channel.remove_consumer(consumer_2) 350 | await test_channel.remove_consumer(consumer_3) 351 | 352 | 353 | @pytest.mark.asyncio 354 | async def test_should_resume_producers_with_no_consumers(test_channel): 355 | producer = tests.EmptyTestProducer(test_channel) 356 | await test_channel.register_producer(producer) 357 | test_channel.is_paused = True 358 | if not os.getenv('CYTHON_IGNORE'): 359 | assert not test_channel._should_resume_producers() 360 | 361 | 362 | @pytest.mark.asyncio 363 | async def test_should_resume_producers_when_already_resumed(test_channel): 364 | producer = tests.EmptyTestProducer(test_channel) 365 | await test_channel.register_producer(producer) 366 | test_channel.is_paused = False 367 | if not os.getenv('CYTHON_IGNORE'): 368 | assert not test_channel._should_resume_producers() 369 | 370 | 371 | @pytest.mark.asyncio 372 | async def test_should_resume_producers_with_priority_consumers(test_channel): 373 | producer = tests.EmptyTestProducer(test_channel) 374 | await test_channel.register_producer(producer) 375 | consumer_1 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 376 | tests.empty_test_callback, 377 | priority_level=async_channel.ChannelConsumerPriorityLevels.HIGH.value) 378 | consumer_2 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 379 | tests.empty_test_callback, 380 | priority_level=async_channel.ChannelConsumerPriorityLevels.MEDIUM.value) 381 | consumer_3 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 382 | tests.empty_test_callback, 383 | priority_level=async_channel.ChannelConsumerPriorityLevels.OPTIONAL.value) 384 | test_channel.is_paused = True 385 | if not os.getenv('CYTHON_IGNORE'): 386 | assert test_channel._should_resume_producers() 387 | await test_channel.remove_consumer(consumer_1) 388 | await test_channel.remove_consumer(consumer_2) 389 | await test_channel.remove_consumer(consumer_3) 390 | 391 | 392 | @pytest.mark.asyncio 393 | async def test_should_resume_producers_with_optional_consumers(test_channel): 394 | producer = tests.EmptyTestProducer(test_channel) 395 | await test_channel.register_producer(producer) 396 | consumer_1 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 397 | tests.empty_test_callback, 398 | priority_level=async_channel.ChannelConsumerPriorityLevels.OPTIONAL.value) 399 | consumer_2 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 400 | tests.empty_test_callback, 401 | priority_level=async_channel.ChannelConsumerPriorityLevels.OPTIONAL.value) 402 | consumer_3 = await channels.get_chan(tests.EMPTY_TEST_CHANNEL).new_consumer( 403 | tests.empty_test_callback, 404 | priority_level=async_channel.ChannelConsumerPriorityLevels.OPTIONAL.value) 405 | test_channel.is_paused = True 406 | if not os.getenv('CYTHON_IGNORE'): 407 | assert not test_channel._should_resume_producers() 408 | await test_channel.remove_consumer(consumer_1) 409 | await test_channel.remove_consumer(consumer_2) 410 | await test_channel.remove_consumer(consumer_3) 411 | --------------------------------------------------------------------------------