From 6511da99f2055388a81c04c0d8d96b1960976a2e Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 13 Jun 2021 20:42:02 +0200 Subject: [PATCH] First commit (0.1.0) --- .gitignore | 138 +++++++++++++++ .idea/.gitignore | 8 + .idea/codeStyles/Project.xml | 78 +++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/discord.xml | 6 + .idea/inspectionProfiles/Project_Default.xml | 74 ++++++++ .idea/misc.xml | 9 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + LICENSE.txt | 165 ++++++++++++++++++ README.md | 56 ++++++ async-chain.iml | 12 ++ async_chain/__init__.py | 1 + async_chain/chain.py | 117 +++++++++++++ async_chain/decorators.py | 20 +++ async_chain/tests/test_integration.py | 65 +++++++ dist/async-chain-0.1.0.tar.gz | Bin 0 -> 4816 bytes dist/async_chain-0.1.0-py3-none-any.whl | Bin 0 -> 5733 bytes poetry.lock | 169 +++++++++++++++++++ pyproject.toml | 17 ++ 20 files changed, 954 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/discord.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 async-chain.iml create mode 100644 async_chain/__init__.py create mode 100644 async_chain/chain.py create mode 100644 async_chain/decorators.py create mode 100644 async_chain/tests/test_integration.py create mode 100644 dist/async-chain-0.1.0.tar.gz create mode 100644 dist/async_chain-0.1.0-py3-none-any.whl create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a81c8ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,138 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..76f258f --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..cd711a0 --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7fd6565 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,74 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d3cb8ef --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + IDE + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ab5e9b2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..06abb52 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# `async-chain` + +A coroutine builder + + +## What? + +Have you ever felt that the `await` syntax in Python was a bit clunky when chaining multiple methods together? + +```python +async def on_message(event): + message = await event.get_message() + author = await message.get_author() + await author.send_message("Hello world!") +``` + +Or even worse: + +```python +async def on_message(event): + (await (await (await event.get_message()).get_author()).send_message("Hello world!")) +``` + +`async-chain` is here to solve your problem! + +```python +async def on_message(event): + await event.get_message().get_author().send_message("Hello world!") +``` + + +## How? + +First, install `async_chain` with your favorite package manager: + +```console +$ pip install async_chain +``` +```console +$ pipenv install async_chain +``` +```console +$ poetry add async_chain +``` + +Then, add the `@async_chain.method` decorator to any async method you wish to make chainable, and the problem will be +magically solved! + +```python +import async_chain + +class MyEvent: + @async_chain.method + async def get_message(self): + ... +``` diff --git a/async-chain.iml b/async-chain.iml new file mode 100644 index 0000000..2478cb7 --- /dev/null +++ b/async-chain.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/async_chain/__init__.py b/async_chain/__init__.py new file mode 100644 index 0000000..58ebebc --- /dev/null +++ b/async_chain/__init__.py @@ -0,0 +1 @@ +from .decorators import method_deco as method diff --git a/async_chain/chain.py b/async_chain/chain.py new file mode 100644 index 0000000..94a5f86 --- /dev/null +++ b/async_chain/chain.py @@ -0,0 +1,117 @@ +import inspect +import typing as t +import abc + + +class Chain(metaclass=abc.ABCMeta): + def __init__(self): + pass + + def __getattr__(self, item): + return ChainGetAttr(previous=self, item=item) + + def __getitem__(self, item): + return ChainGetItem(previous=self, item=item) + + def __call__(self, *args, **kwargs): + return ChainCall(previous=self, args=args, kwargs=kwargs) + + def __await__(self): + return self.__evaluate__().__await__() + + @abc.abstractmethod + def __repr__(self): + raise NotImplementedError() + + @abc.abstractmethod + def __display__(self): + raise NotImplementedError() + + @abc.abstractmethod + async def __evaluate__(self): + raise NotImplementedError() + + +class ChainStart(Chain): + def __init__(self, start: t.Any): + super().__init__() + self.__start__: t.Any = start + + def __repr__(self): + return f"" + + def __display__(self): + return f"{self.__start__!r}" + + async def __evaluate__(self): + return self.__start__ + + +class ChainNode(Chain, metaclass=abc.ABCMeta): + def __init__(self, previous: Chain): + super().__init__() + self.__previous__: Chain = previous + + def __repr__(self): + return f"" + + +class ChainGetAttr(ChainNode): + def __init__(self, previous: Chain, item: str): + super().__init__(previous=previous) + self._item: str = item + + def __display__(self): + return f"{self.__previous__.__display__()}.{self._item!s}" + + async def __evaluate__(self): + previous = await self.__previous__.__evaluate__() + + current = getattr(previous, self._item) + + if inspect.isawaitable(current): + return await current + else: + return current + + +class ChainGetItem(ChainNode): + def __init__(self, previous: Chain, item: t.Any): + super().__init__(previous=previous) + self._item: t.Any = item + + def __display__(self): + return f"{self.__previous__.__display__()}[{self._item!r}]" + + async def __evaluate__(self): + previous = await self.__previous__.__evaluate__() + + current = previous[self._item] + + if inspect.isawaitable(current): + return await current + else: + return current + + +class ChainCall(ChainNode): + def __init__(self, previous: Chain, args, kwargs): + super().__init__(previous=previous) + self._args = args + self._kwargs = kwargs + + def __display__(self): + args = map(lambda a: f"{a!r}", self._args) + kwargs = map(lambda k, v: f"{k!s}={v!r}", self._kwargs) + allargs = ", ".join([*args, *kwargs]) + return f"{self.__previous__.__display__()}({allargs})" + + async def __evaluate__(self): + previous = await self.__previous__.__evaluate__() + + current = previous(*self._args, **self._kwargs) + + if inspect.isawaitable(current): + return await current + else: + return current diff --git a/async_chain/decorators.py b/async_chain/decorators.py new file mode 100644 index 0000000..546aa59 --- /dev/null +++ b/async_chain/decorators.py @@ -0,0 +1,20 @@ +import functools +from .chain import ChainStart + + +class FunctionWrapper: + def __init__(self, func): + self._func = func + + def __repr__(self): + return self._func.__name__ + + def __call__(self, *args, **kwargs): + return self._func(*args, **kwargs) + + +def method_deco(func): + @functools.wraps(func) + def decorated(*args, **kwargs): + return ChainStart(start=FunctionWrapper(func))(*args, **kwargs) + return decorated diff --git a/async_chain/tests/test_integration.py b/async_chain/tests/test_integration.py new file mode 100644 index 0000000..1410f8f --- /dev/null +++ b/async_chain/tests/test_integration.py @@ -0,0 +1,65 @@ +import pytest +import asyncio +import async_chain + +pytestmark = pytest.mark.asyncio + + +class ExampleClass: + def __init__(self, value): + self.value = value + + def __repr__(self): + return f"Example(value={self.value})" + + @async_chain.method + async def plus(self, n): + await asyncio.sleep(0) + return self.__class__(self.value + n) + + @property + @async_chain.method + async def incremented(self): + await asyncio.sleep(0) + return self.__class__(self.value + 1) + + +async def test_class_creation(): + example = ExampleClass(0) + assert example + + +@pytest.fixture() +def example(): + return ExampleClass(0) + + +async def test_method_existence(example): + assert hasattr(example, "plus") + assert hasattr(example, "minus") + assert hasattr(example, "incremented") + assert hasattr(example, "decremented") + + +async def test_chain_single_property(example): + one = await example.incremented + assert isinstance(one, ExampleClass) + assert one.value == 1 + + +async def test_chain_multiple_properties(example): + three = await example.incremented.incremented.incremented + assert isinstance(three, ExampleClass) + assert three.value == 3 + + +async def test_chain_single_method(example): + one = await example.plus(1) + assert isinstance(one, ExampleClass) + assert one.value == 1 + + +async def test_chain_multiple_methods(example): + three = await example.plus(1).plus(1).plus(1) + assert isinstance(three, ExampleClass) + assert three.value == 3 diff --git a/dist/async-chain-0.1.0.tar.gz b/dist/async-chain-0.1.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..45e4605dc8af1fcda49184c2d34f6cc5d07c7a3c GIT binary patch literal 4816 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 B^-ur+ literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..1e8e389 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,169 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "20.9" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.4" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.15.1" +description = "Pytest support for asyncio." +category = "dev" +optional = false +python-versions = ">= 3.6" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +testing = ["coverage", "hypothesis (>=5.7.1)"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "c2b7c085b84345a56b06880b4b394c66fce9dfc78324db16be8289e9d9f750c2" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +packaging = [ + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"}, + {file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e67c522 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "async-chain" +version = "0.1.0" +description = "" +authors = ["Stefano Pigozzi "] +license = "LGPL-3.0-or-later" + +[tool.poetry.dependencies] +python = "^3.9" + +[tool.poetry.dev-dependencies] +pytest = "^6.2.4" +pytest-asyncio = "^0.15.1" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api"