├── pyproject.toml ├── .flake8 ├── src └── dagio │ ├── __init__.py │ ├── run_async_decorator.py │ └── depends_decorator.py ├── .bumpversion.cfg ├── Makefile ├── .github └── workflows │ ├── python-publish.yml │ └── tests.yml ├── LICENSE ├── setup.py ├── .gitignore ├── test ├── test_depends.py └── test_run_async.py └── README.md /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | multi_line_output = 3 -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | per-file-ignores = 3 | src/dagio/__init__.py:F401 -------------------------------------------------------------------------------- /src/dagio/__init__.py: -------------------------------------------------------------------------------- 1 | from .depends_decorator import depends 2 | from .run_async_decorator import run_async 3 | 4 | __version__ = "0.0.2" 5 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.0.1 3 | 4 | [bumpversion:file:setup.py] 5 | 6 | [bumpversion:file:src/dagio/__init__.py] -------------------------------------------------------------------------------- /src/dagio/run_async_decorator.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from functools import partial 3 | 4 | 5 | def run_async(fn): 6 | """Runs a non-async function in a thread pool.""" 7 | 8 | async def wrapped_fn(*args, **kwargs): 9 | loop = asyncio.get_event_loop() 10 | return await loop.run_in_executor(None, partial(fn, *args, **kwargs)) 11 | 12 | return wrapped_fn 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: init test format bump-minor bump-patch package push-package clean 3 | 4 | init: 5 | python3 -m venv .venv; \ 6 | . .venv/bin/activate; \ 7 | pip install -e .[dev] 8 | 9 | test: 10 | . .venv/bin/activate; \ 11 | pytest test 12 | 13 | format: 14 | . .venv/bin/activate; \ 15 | autoflake -r --in-place --remove-all-unused-imports --ignore-init-module-imports src test; \ 16 | isort src test; \ 17 | black src test; \ 18 | flake8 src test 19 | 20 | bump-minor: 21 | . .venv/bin/activate; \ 22 | bumpversion minor 23 | 24 | bump-patch: 25 | . .venv/bin/activate; \ 26 | bumpversion patch 27 | 28 | package: 29 | . .venv/bin/activate; \ 30 | python setup.py sdist bdist_wheel; \ 31 | twine check dist/* 32 | 33 | push-package: 34 | . .venv/bin/activate; \ 35 | twine upload dist/* 36 | 37 | clean: 38 | rm -rf .pytest_cache docs/_html build dist src/dagio.egg-info 39 | find . -type d -name __pycache__ -exec rm -rf {} \+ -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine check dist/* 32 | twine upload dist/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Brendan Hasz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("README.md", "r") as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name="dagio", 8 | version="0.0.2", 9 | author="Brendan Hasz", 10 | author_email="winsto99@gmail.com", 11 | description="A python package for running directed acyclic graphs of asynchronous I/O operations", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/brendanhasz/dagio", 15 | license="MIT", 16 | packages=find_packages(where="src"), 17 | package_dir={"": "src"}, 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | "Development Status :: 3 - Alpha", 23 | ], 24 | zip_safe=False, 25 | extras_require={ 26 | "dev": [ 27 | "autoflake >= 1.4", 28 | "black >= 19.10b0", 29 | "bumpversion", 30 | "flake8 >= 3.8.3", 31 | "isort >= 5.1.2", 32 | "pytest >= 6.0.0rc1", 33 | "pytest-asyncio >= 0.15.1", 34 | "pytest-cov >= 2.7.1", 35 | "setuptools >= 49.1.0", 36 | "twine >= 3.2.0", 37 | "wheel >= 0.34.2", 38 | ], 39 | }, 40 | ) -------------------------------------------------------------------------------- /src/dagio/depends_decorator.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | def depends(*dependencies): 5 | def outer_wrapped_fn(fn): 6 | async def inner_wrapped_fn(self): 7 | 8 | # Set up task list if this is the top level node 9 | top_level_fn = False 10 | if getattr(self, "__task_list_lock", None) is None: 11 | self.__task_list_lock = asyncio.Lock() 12 | self.__task_list = dict() 13 | top_level_fn = True 14 | 15 | # Start dependencies which have not already been started 16 | async with self.__task_list_lock: 17 | for dependency in dependencies: 18 | if dependency not in self.__task_list: 19 | self.__task_list[dependency] = asyncio.create_task( 20 | getattr(self, dependency)() 21 | ) 22 | 23 | # Wait for dependencies to finish 24 | for dependency in dependencies: 25 | await self.__task_list[dependency] 26 | 27 | # Run this function 28 | await fn(self) 29 | 30 | # Clean up 31 | if top_level_fn: 32 | delattr(self, "__task_list_lock") 33 | delattr(self, "__task_list") 34 | 35 | return inner_wrapped_fn 36 | 37 | return outer_wrapped_fn 38 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: tests 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | python-version: ['3.7', '3.8', '3.9'] 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies and dagio 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install -e .[dev] 31 | - name: Lint with flake8 32 | run: | 33 | flake8 src/dagio test 34 | - name: Style checks with black 35 | run: | 36 | black --check src/dagio test 37 | - name: Run tests 38 | run: | 39 | pytest test --cov=probflow --cov-report xml:coverage.xml 40 | #- name: Upload coverage to Codecov 41 | # uses: codecov/codecov-action@v1 42 | # with: 43 | # file: ./coverage.xml 44 | - name: Ensure the package builds 45 | run: | 46 | python setup.py sdist bdist_wheel 47 | twine check dist/* -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /test/test_depends.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from dagio import depends 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_depends_single_dependency(): 10 | """Test a simple two-element dag with a single dependency 11 | 12 | a -> b 13 | """ 14 | 15 | task_run_order = [] 16 | 17 | class TestDag: 18 | async def task_a(self): 19 | task_run_order.append("a") 20 | 21 | @depends("task_a") 22 | async def task_b(self): 23 | task_run_order.append("b") 24 | 25 | test_dag_object = TestDag() 26 | await test_dag_object.task_b() 27 | 28 | assert len(task_run_order) == 2 29 | assert task_run_order[0] == "a" 30 | assert task_run_order[1] == "b" 31 | 32 | 33 | @pytest.mark.asyncio 34 | async def test_depends_two_dependencies(): 35 | r"""Test a simple two-element dag with a multiple dependencies 36 | 37 | A 38 | | 39 | B C 40 | \ /| 41 | D | 42 | / \| 43 | E F 44 | \ / 45 | G 46 | """ 47 | 48 | task_start_order = [] 49 | task_end_order = [] 50 | 51 | class TestDag: 52 | async def task_a(self): 53 | task_start_order.append("a") 54 | await asyncio.sleep(0.1) 55 | task_end_order.append("a") 56 | 57 | @depends("task_a") 58 | async def task_b(self): 59 | task_start_order.append("b") 60 | await asyncio.sleep(0.1) 61 | task_end_order.append("b") 62 | 63 | async def task_c(self): 64 | task_start_order.append("c") 65 | await asyncio.sleep(0.3) 66 | task_end_order.append("c") 67 | 68 | @depends("task_b", "task_c") 69 | async def task_d(self): 70 | task_start_order.append("d") 71 | await asyncio.sleep(0.1) 72 | task_end_order.append("d") 73 | 74 | @depends("task_d") 75 | async def task_e(self): 76 | task_start_order.append("e") 77 | await asyncio.sleep(0.1) 78 | task_end_order.append("e") 79 | 80 | @depends("task_c", "task_d") 81 | async def task_f(self): 82 | task_start_order.append("f") 83 | await asyncio.sleep(0.2) 84 | task_end_order.append("f") 85 | 86 | @depends("task_e", "task_f") 87 | async def task_g(self): 88 | task_start_order.append("g") 89 | await asyncio.sleep(0.2) 90 | task_end_order.append("g") 91 | 92 | test_dag_object = TestDag() 93 | await test_dag_object.task_g() 94 | 95 | assert len(task_start_order) == 7 96 | assert "a" in task_start_order[:2] 97 | assert "c" in task_start_order[:2] 98 | assert task_start_order[2] == "b" 99 | assert task_start_order[3] == "d" 100 | assert "e" in task_start_order[4:6] 101 | assert "f" in task_start_order[4:6] 102 | assert task_start_order[6] == "g" 103 | 104 | assert len(task_end_order) == 7 105 | assert task_end_order[0] == "a" 106 | assert task_end_order[1] == "b" 107 | assert task_end_order[2] == "c" 108 | assert task_end_order[3] == "d" 109 | assert task_end_order[4] == "e" 110 | assert task_end_order[5] == "f" 111 | assert task_end_order[6] == "g" 112 | -------------------------------------------------------------------------------- /test/test_run_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | import pytest 5 | 6 | from dagio import depends, run_async 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_run_async_single_dependency(): 11 | """Test a simple two-element dag with a single dependency 12 | 13 | a -> b 14 | """ 15 | 16 | task_run_order = [] 17 | 18 | class TestDag: 19 | @run_async 20 | def task_a(self): 21 | task_run_order.append("a") 22 | 23 | @depends("task_a") 24 | async def task_b(self): 25 | task_run_order.append("b") 26 | 27 | test_dag_object = TestDag() 28 | await test_dag_object.task_b() 29 | 30 | assert len(task_run_order) == 2 31 | assert task_run_order[0] == "a" 32 | assert task_run_order[1] == "b" 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_run_async_two_dependencies(): 37 | r"""Test a simple two-element dag with a multiple dependencies 38 | 39 | A 40 | | 41 | B C 42 | \ /| 43 | D | 44 | / \| 45 | E F 46 | \ / 47 | G 48 | """ 49 | 50 | task_start_order = [] 51 | task_end_order = [] 52 | 53 | class TestDag: 54 | @run_async 55 | def task_a(self): 56 | task_start_order.append("a") 57 | time.sleep(0.1) 58 | task_end_order.append("a") 59 | 60 | @depends("task_a") 61 | @run_async 62 | def task_b(self): 63 | task_start_order.append("b") 64 | time.sleep(0.1) 65 | task_end_order.append("b") 66 | 67 | async def task_c(self): 68 | task_start_order.append("c") 69 | await asyncio.sleep(0.3) 70 | task_end_order.append("c") 71 | 72 | @depends("task_b", "task_c") 73 | async def task_d(self): 74 | task_start_order.append("d") 75 | await asyncio.sleep(0.1) 76 | task_end_order.append("d") 77 | 78 | @depends("task_d") 79 | async def task_e(self): 80 | task_start_order.append("e") 81 | await asyncio.sleep(0.1) 82 | task_end_order.append("e") 83 | 84 | @depends("task_c", "task_d") 85 | async def task_f(self): 86 | task_start_order.append("f") 87 | await asyncio.sleep(0.2) 88 | task_end_order.append("f") 89 | 90 | @depends("task_e", "task_f") 91 | async def task_g(self): 92 | task_start_order.append("g") 93 | await asyncio.sleep(0.2) 94 | task_end_order.append("g") 95 | 96 | test_dag_object = TestDag() 97 | await test_dag_object.task_g() 98 | 99 | assert len(task_start_order) == 7 100 | assert "a" in task_start_order[:2] 101 | assert "c" in task_start_order[:2] 102 | assert task_start_order[2] == "b" 103 | assert task_start_order[3] == "d" 104 | assert "e" in task_start_order[4:6] 105 | assert "f" in task_start_order[4:6] 106 | assert task_start_order[6] == "g" 107 | 108 | assert len(task_end_order) == 7 109 | assert task_end_order[0] == "a" 110 | assert task_end_order[1] == "b" 111 | assert task_end_order[2] == "c" 112 | assert task_end_order[3] == "d" 113 | assert task_end_order[4] == "e" 114 | assert task_end_order[5] == "f" 115 | assert task_end_order[6] == "g" 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dagio: Asynchronous I/O - with DAGs! 2 | 3 | [![Version Badge](https://img.shields.io/pypi/v/dagio)](https://pypi.org/project/dagio/) 4 | [![Test Status](https://github.com/brendanhasz/dagio/workflows/tests/badge.svg)](https://github.com/brendanhasz/probflow/actions?query=branch%3Amaster) 5 | 6 | 7 | `dagio` is an embarassingly simple Python package for running directed acyclic 8 | graphs of asynchronous I/O operations. It is built using and to be used with 9 | Python's built-in [`asyncio`](https://docs.python.org/3/library/asyncio.html) 10 | module, and provides a _veeeery_ thin layer of functionality on top of it. 11 | :sweat_smile: 12 | 13 | * Git repository: https://github.com/brendanhasz/dagio 14 | * Bug reports: https://github.com/brendanhasz/dagio/issues 15 | 16 | 17 | ## Getting Started 18 | 19 | Suppose you have a set of potentially long-running I/O tasks (e.g. hit a web 20 | service, query a database, read a large file from disk, etc), where some of the 21 | tasks depend on other tasks having finished. That is, you've got a directed 22 | acyclic graph (DAG) of tasks, where non-interdependent tasks can be run 23 | asynchronously. 24 | 25 | For example, if you've got a task `G` which depends on `E` and `F`, but `E` 26 | depends on `D`, and `F` depends on both `C` and `D`, etc: 27 | 28 | ``` 29 | A 30 | | 31 | B C 32 | \ /| 33 | D | 34 | / \| 35 | E F 36 | \ / 37 | G 38 | ``` 39 | 40 | Coding that up using raw `asyncio` might look something like this: 41 | 42 | ```python 43 | import asyncio 44 | 45 | 46 | class MyDag: 47 | 48 | async def task_a(self): 49 | # does task a stuff... 50 | 51 | async def task_b(self): 52 | # does task b stuff... 53 | 54 | async def task_c(self): 55 | # does task c stuff... 56 | 57 | async def task_d(self): 58 | # does task d stuff... 59 | 60 | async def task_e(self): 61 | # does task e stuff... 62 | 63 | async def task_f(self): 64 | # does task f stuff... 65 | 66 | async def task_g(self): 67 | # does task g stuff... 68 | 69 | 70 | async def run(): 71 | 72 | obj = MyDag() 73 | 74 | task_a = asyncio.create_task(obj.task_a()) 75 | task_c = asyncio.create_task(obj.task_c()) 76 | 77 | await task_a 78 | 79 | await obj.task_b() 80 | 81 | await task_c 82 | 83 | await obj.task_d() 84 | 85 | task_e = asyncio.create_task(obj.task_e()) 86 | task_f = asyncio.create_task(obj.task_f()) 87 | 88 | await task_e 89 | await task_f 90 | 91 | await obj.task_g() 92 | 93 | 94 | asyncio.run(run()) 95 | ``` 96 | 97 | Which is... _fine_, I guess :roll_eyes: But, you have to be careful about what 98 | task you start before what other task, and which tasks can safely be run 99 | asynchronously vs those which can't. And then you have to type out all that 100 | logic and ordering manually! With the confusing `asyncio` API! So: a lot of 101 | thought has to go into it, especially for complex DAGs. 102 | 103 | And thinking is hard! Less thinking! :fist: 104 | 105 | With `dagio`, you just use the `depends` decorator to specify what methods any 106 | other given method depends on, and it'll figure everything out for you, and run 107 | them in the correct order, asynchronously where possible: 108 | 109 | ```python 110 | import asyncio 111 | from dagio import depends 112 | 113 | 114 | class MyDag: 115 | 116 | async def task_a(self): 117 | # does task a stuff... 118 | 119 | @depends("task_a") 120 | async def task_b(self): 121 | # does task b stuff... 122 | 123 | async def task_c(self): 124 | # does task c stuff... 125 | 126 | @depends("task_b", "task_c") 127 | async def task_d(self): 128 | # does task d stuff... 129 | 130 | @depends("task_d") 131 | async def task_e(self): 132 | # does task e stuff... 133 | 134 | @depends("task_c", "task_d") 135 | async def task_f(self): 136 | # does task f stuff... 137 | 138 | @depends("task_e", "task_f") 139 | async def task_g(self): 140 | # does task g stuff... 141 | 142 | 143 | async def run(): 144 | obj = MyDag() 145 | await obj.task_g() 146 | 147 | 148 | asyncio.run(run()) 149 | ``` 150 | 151 | Note that: 152 | 153 | 1) Each task in your DAG has to be a method of the same class 154 | 2) Task methods must be `async` methods 155 | 3) Calling a task method decorated with `depends` runs that task _and all its dependencies_ 156 | 4) Task methods should not take arguments nor return values. You can handle 157 | inter-task communication using object attributes (e.g. `self._task_a_output = ...`). 158 | If you need a lock, you can set up an [`asyncio.Lock`](https://docs.python.org/3/library/asyncio-sync.html#lock) 159 | in your class's `__init__`. 160 | 161 | You can also run a non-async method asynchronously in a thread pool using the `run_async` decorator: 162 | 163 | ```python 164 | import asyncio 165 | from dagio import depends, run_async 166 | 167 | 168 | class MyDag: 169 | 170 | @run_async 171 | def task_a(self): 172 | # a sync method which does task a stuff... 173 | 174 | @run_async 175 | def task_b(self): 176 | # a sync method which does task b stuff... 177 | 178 | @depends("task_a", "task_b") 179 | async def task_c(self): 180 | # does task c stuff... 181 | 182 | 183 | async def run(): 184 | obj = MyDag() 185 | await obj.task_c() #runs a and b concurrently, then c 186 | ``` 187 | 188 | That's it. That's all this package does. 189 | 190 | 191 | ## Installation 192 | 193 | ``` 194 | pip install dagio 195 | ``` 196 | 197 | 198 | ## Support 199 | 200 | Post bug reports, feature requests, and tutorial requests in [GitHub 201 | issues](https://github.com/brendanhasz/dagio). 202 | 203 | 204 | ## Contributing 205 | 206 | [Pull requests](https://github.com/brendanhasz/dagio/pulls) are totally 207 | welcome! Any contribution would be appreciated, from things as minor as fixing 208 | typos to things as major as adding new functionality. :smile: 209 | 210 | 211 | ## Why the name, dagio? 212 | 213 | It's for making DAGs of IO operations. DAG IO. Technically it's _asynchronous_ 214 | DAG-based I/O, and the name `adagio` would have been siiiick, but it was 215 | [already taken](https://pypi.org/project/adagio/)! :sob: 216 | --------------------------------------------------------------------------------