async-graph-data-flow 1.6.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/CHANGELOG.md +18 -4
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/LICENSE.txt +1 -1
- {async_graph_data_flow-1.6.0/src/async_graph_data_flow.egg-info → async_graph_data_flow-2.0.0}/PKG-INFO +25 -23
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/README.md +8 -7
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/pyproject.toml +16 -16
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow/__init__.py +0 -1
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow/executor.py +8 -13
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow/graph.py +78 -56
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0/src/async_graph_data_flow.egg-info}/PKG-INFO +25 -23
- async_graph_data_flow-2.0.0/src/async_graph_data_flow.egg-info/requires.txt +15 -0
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/tests/test_executor.py +98 -0
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/tests/test_graph.py +95 -3
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/tests/test_version_number.py +0 -1
- async_graph_data_flow-1.6.0/src/async_graph_data_flow.egg-info/requires.txt +0 -15
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/MANIFEST.in +0 -0
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/setup.cfg +0 -0
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow/py.typed +0 -0
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow.egg-info/SOURCES.txt +0 -0
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow.egg-info/dependency_links.txt +0 -0
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow.egg-info/not-zip-safe +0 -0
- {async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow.egg-info/top_level.txt +0 -0
|
@@ -8,16 +8,30 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
8
8
|
## [Unreleased] - YYYY-MM-DD
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
-
|
|
12
11
|
### Changed
|
|
13
|
-
|
|
14
12
|
### Deprecated
|
|
13
|
+
### Removed
|
|
14
|
+
### Fixed
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
## [2.0.0] - 2026-04-21
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Python 3.14 is officially supported and tested on CI. (#16)
|
|
21
|
+
- `AsyncGraph.add_node` now accepts `functools.partial` objects and functions
|
|
22
|
+
decorated with `functools.wraps` as node functions. The default node name and
|
|
23
|
+
the async-generator function check resolve through the embedded function, so
|
|
24
|
+
neither `name=` nor `check_async_gen=False` is required in these cases. (#16)
|
|
25
|
+
- Added GitHub Actions for PyPI releases. (#16)
|
|
15
26
|
|
|
16
27
|
### Removed
|
|
28
|
+
- Removed the `queue_size` argument at `add_node`, deprecated in v1.6.0.
|
|
29
|
+
To configure queue size, pass a pre-sized `asyncio.Queue` via the `queue`
|
|
30
|
+
argument instead. (#16)
|
|
17
31
|
|
|
18
32
|
### Fixed
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
- Fixed a bug where tail items could be dropped when nodes were not added in
|
|
34
|
+
topological order. Queues are now drained in topological order. (#16)
|
|
21
35
|
|
|
22
36
|
## [1.6.0] - 2025-03-19
|
|
23
37
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: async-graph-data-flow
|
|
3
|
-
Version:
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Data flow along a directed acyclic graph of asynchronous generator functions
|
|
5
5
|
Author-email: Civis Analytics <opensource@civisanalytics.com>
|
|
6
|
-
License: BSD
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
7
|
Project-URL: Documentation, https://async-graph-data-flow.readthedocs.io
|
|
8
8
|
Project-URL: Source, https://github.com/civisanalytics/async-graph-data-flow
|
|
9
9
|
Keywords: async,asynchronous,asyncio,data flow,graph
|
|
@@ -11,7 +11,6 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Intended Audience :: Information Technology
|
|
13
13
|
Classifier: Intended Audience :: Science/Research
|
|
14
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
15
14
|
Classifier: Operating System :: OS Independent
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -19,23 +18,25 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
22
|
Classifier: Framework :: AsyncIO
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE.txt
|
|
26
26
|
Provides-Extra: dev
|
|
27
|
-
Requires-Dist: black
|
|
28
|
-
Requires-Dist: flake8
|
|
29
|
-
Requires-Dist: pytest
|
|
30
|
-
Requires-Dist: mypy
|
|
31
|
-
Requires-Dist: build
|
|
32
|
-
Requires-Dist: twine
|
|
27
|
+
Requires-Dist: black; extra == "dev"
|
|
28
|
+
Requires-Dist: flake8; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy; extra == "dev"
|
|
31
|
+
Requires-Dist: build; extra == "dev"
|
|
32
|
+
Requires-Dist: twine; extra == "dev"
|
|
33
33
|
Provides-Extra: docs
|
|
34
|
-
Requires-Dist: Sphinx
|
|
35
|
-
Requires-Dist: furo
|
|
36
|
-
Requires-Dist: numpydoc
|
|
37
|
-
Requires-Dist: sphinx-copybutton
|
|
38
|
-
Requires-Dist: sphinxcontrib-mermaid
|
|
34
|
+
Requires-Dist: Sphinx; extra == "docs"
|
|
35
|
+
Requires-Dist: furo; extra == "docs"
|
|
36
|
+
Requires-Dist: numpydoc; extra == "docs"
|
|
37
|
+
Requires-Dist: sphinx-copybutton; extra == "docs"
|
|
38
|
+
Requires-Dist: sphinxcontrib-mermaid; extra == "docs"
|
|
39
|
+
Dynamic: license-file
|
|
39
40
|
|
|
40
41
|
# async-graph-data-flow
|
|
41
42
|
|
|
@@ -59,20 +60,21 @@ BSD 3-Clause License. Please see `LICENSE.txt` in the GitHub source code for det
|
|
|
59
60
|
|
|
60
61
|
The latest code under development is available on GitHub at
|
|
61
62
|
https://github.com/civisanalytics/async-graph-data-flow.
|
|
62
|
-
To obtain this version for experimental features or for development
|
|
63
|
+
To obtain this version for experimental features or for development,
|
|
64
|
+
we recommend using [uv](https://docs.astral.sh/uv/) to manage the development environment:
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
67
|
git clone https://github.com/civisanalytics/async-graph-data-flow.git
|
|
66
68
|
cd async-graph-data-flow
|
|
67
|
-
pip install -e ".[dev]"
|
|
69
|
+
uv pip install -e ".[dev]"
|
|
68
70
|
```
|
|
69
71
|
|
|
70
72
|
To run tests and styling checks:
|
|
71
73
|
|
|
72
74
|
```bash
|
|
73
|
-
pytest
|
|
74
|
-
flake8 src tests examples
|
|
75
|
-
black --check src tests examples
|
|
75
|
+
uv run pytest
|
|
76
|
+
uv run flake8 src tests examples
|
|
77
|
+
uv run black --check src tests examples
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
## Building Documentation
|
|
@@ -88,5 +90,5 @@ The builds by the Read The Docs site generate the necessary files (the HTML page
|
|
|
88
90
|
for the public documentation. All these auto-generated files are explicitly not versioned (see `.gitignore`).
|
|
89
91
|
|
|
90
92
|
To build the documentation locally (for testing and development),
|
|
91
|
-
install the full doc-related dependencies: `pip install -r docs/requirements.txt`,
|
|
92
|
-
then run `sphinx-build -b html docs/ docs/build/`.
|
|
93
|
+
install the full doc-related dependencies: `uv pip install -r docs/requirements.txt`,
|
|
94
|
+
then run `uv run sphinx-build -b html docs/ docs/build/`.
|
|
@@ -20,20 +20,21 @@ BSD 3-Clause License. Please see `LICENSE.txt` in the GitHub source code for det
|
|
|
20
20
|
|
|
21
21
|
The latest code under development is available on GitHub at
|
|
22
22
|
https://github.com/civisanalytics/async-graph-data-flow.
|
|
23
|
-
To obtain this version for experimental features or for development
|
|
23
|
+
To obtain this version for experimental features or for development,
|
|
24
|
+
we recommend using [uv](https://docs.astral.sh/uv/) to manage the development environment:
|
|
24
25
|
|
|
25
26
|
```bash
|
|
26
27
|
git clone https://github.com/civisanalytics/async-graph-data-flow.git
|
|
27
28
|
cd async-graph-data-flow
|
|
28
|
-
pip install -e ".[dev]"
|
|
29
|
+
uv pip install -e ".[dev]"
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
To run tests and styling checks:
|
|
32
33
|
|
|
33
34
|
```bash
|
|
34
|
-
pytest
|
|
35
|
-
flake8 src tests examples
|
|
36
|
-
black --check src tests examples
|
|
35
|
+
uv run pytest
|
|
36
|
+
uv run flake8 src tests examples
|
|
37
|
+
uv run black --check src tests examples
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
## Building Documentation
|
|
@@ -49,5 +50,5 @@ The builds by the Read The Docs site generate the necessary files (the HTML page
|
|
|
49
50
|
for the public documentation. All these auto-generated files are explicitly not versioned (see `.gitignore`).
|
|
50
51
|
|
|
51
52
|
To build the documentation locally (for testing and development),
|
|
52
|
-
install the full doc-related dependencies: `pip install -r docs/requirements.txt`,
|
|
53
|
-
then run `sphinx-build -b html docs/ docs/build/`.
|
|
53
|
+
install the full doc-related dependencies: `uv pip install -r docs/requirements.txt`,
|
|
54
|
+
then run `uv run sphinx-build -b html docs/ docs/build/`.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools >=
|
|
2
|
+
requires = ["setuptools >= 77.0.0", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "async-graph-data-flow"
|
|
7
|
-
version = "
|
|
8
|
-
description = "
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Data flow along a directed acyclic graph of asynchronous generator functions"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">= 3.10"
|
|
11
|
-
license =
|
|
11
|
+
license = "BSD-3-Clause"
|
|
12
12
|
authors = [ { name = "Civis Analytics", email = "opensource@civisanalytics.com" } ]
|
|
13
13
|
keywords = [
|
|
14
14
|
"async",
|
|
@@ -22,7 +22,6 @@ classifiers = [
|
|
|
22
22
|
"Intended Audience :: Developers",
|
|
23
23
|
"Intended Audience :: Information Technology",
|
|
24
24
|
"Intended Audience :: Science/Research",
|
|
25
|
-
"License :: OSI Approved :: BSD License",
|
|
26
25
|
"Operating System :: OS Independent",
|
|
27
26
|
"Programming Language :: Python :: 3",
|
|
28
27
|
"Programming Language :: Python :: 3 :: Only",
|
|
@@ -30,6 +29,7 @@ classifiers = [
|
|
|
30
29
|
"Programming Language :: Python :: 3.11",
|
|
31
30
|
"Programming Language :: Python :: 3.12",
|
|
32
31
|
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
33
33
|
"Framework :: AsyncIO",
|
|
34
34
|
]
|
|
35
35
|
|
|
@@ -40,21 +40,21 @@ Source = "https://github.com/civisanalytics/async-graph-data-flow"
|
|
|
40
40
|
[project.optional-dependencies]
|
|
41
41
|
dev = [
|
|
42
42
|
# Running tests and linters
|
|
43
|
-
"black
|
|
44
|
-
"flake8
|
|
45
|
-
"pytest
|
|
46
|
-
"mypy
|
|
43
|
+
"black",
|
|
44
|
+
"flake8",
|
|
45
|
+
"pytest",
|
|
46
|
+
"mypy",
|
|
47
47
|
|
|
48
48
|
# Managing source distributions
|
|
49
|
-
"build
|
|
50
|
-
"twine
|
|
49
|
+
"build",
|
|
50
|
+
"twine",
|
|
51
51
|
]
|
|
52
52
|
docs = [
|
|
53
|
-
"Sphinx
|
|
54
|
-
"furo
|
|
55
|
-
"numpydoc
|
|
56
|
-
"sphinx-copybutton
|
|
57
|
-
"sphinxcontrib-mermaid
|
|
53
|
+
"Sphinx",
|
|
54
|
+
"furo",
|
|
55
|
+
"numpydoc",
|
|
56
|
+
"sphinx-copybutton",
|
|
57
|
+
"sphinxcontrib-mermaid",
|
|
58
58
|
]
|
|
59
59
|
|
|
60
60
|
[tool.setuptools.packages.find]
|
{async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow/executor.py
RENAMED
|
@@ -9,7 +9,6 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
from .graph import AsyncGraph, InvalidAsyncGraphError
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
_LOG = logging.getLogger(__name__)
|
|
14
13
|
|
|
15
14
|
_DEFAULT_DATA_FLOW_LOGGING_NODE_FORMAT = " {node} - in={in}, out={out}, err={err}"
|
|
@@ -133,20 +132,16 @@ class AsyncExecutor:
|
|
|
133
132
|
"""
|
|
134
133
|
self._data_flow_logging = True
|
|
135
134
|
|
|
136
|
-
if node_format
|
|
135
|
+
if node_format:
|
|
137
136
|
self._data_flow_logging_node_format = node_format
|
|
138
137
|
|
|
139
|
-
if
|
|
140
|
-
node_filter
|
|
141
|
-
and not isinstance(node_filter, str)
|
|
142
|
-
and all(map(lambda x: isinstance(x, str), node_filter))
|
|
143
|
-
):
|
|
138
|
+
if node_filter:
|
|
144
139
|
self._data_flow_logging_node_filter = set(node_filter)
|
|
145
140
|
|
|
146
|
-
if time_interval
|
|
141
|
+
if time_interval:
|
|
147
142
|
self._data_flow_logging_time_interval = time_interval
|
|
148
143
|
|
|
149
|
-
if logger
|
|
144
|
+
if logger:
|
|
150
145
|
self._logger = logger
|
|
151
146
|
|
|
152
147
|
def turn_off_data_flow_logging(self) -> None:
|
|
@@ -286,7 +281,7 @@ class AsyncExecutor:
|
|
|
286
281
|
|
|
287
282
|
for node_name, node in self._graph._nodes.items():
|
|
288
283
|
if node.queue is None:
|
|
289
|
-
queue = asyncio.Queue(
|
|
284
|
+
queue = asyncio.Queue()
|
|
290
285
|
else:
|
|
291
286
|
queue = node.queue
|
|
292
287
|
self._node_queues[node_name] = queue
|
|
@@ -300,8 +295,8 @@ class AsyncExecutor:
|
|
|
300
295
|
|
|
301
296
|
await self._producer()
|
|
302
297
|
|
|
303
|
-
for
|
|
304
|
-
await
|
|
298
|
+
for node_name in self._graph._topological_sort():
|
|
299
|
+
await self._node_queues[node_name].join()
|
|
305
300
|
|
|
306
301
|
for task in self._consumer_tasks.values():
|
|
307
302
|
task.cancel()
|
|
@@ -333,7 +328,7 @@ class AsyncExecutor:
|
|
|
333
328
|
Each key in this dictionary is the name (str) of the node function,
|
|
334
329
|
and its corresponding value is the args (tuple)
|
|
335
330
|
(in which case the node function will be called as ``func(*args)``
|
|
336
|
-
-- provide ``
|
|
331
|
+
-- provide ``()`` (an empty tuple) if you want ``func()`` with no args).
|
|
337
332
|
If ``start_nodes`` is ``None`` or isn't provided,
|
|
338
333
|
nodes that have no incoming edges are treated as start nodes.
|
|
339
334
|
"""
|
{async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow/graph.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import functools
|
|
2
3
|
import inspect
|
|
3
4
|
from collections import OrderedDict
|
|
4
5
|
from collections.abc import AsyncGenerator, Callable
|
|
@@ -13,7 +14,6 @@ class _Node(NamedTuple):
|
|
|
13
14
|
func: Callable[..., AsyncGenerator]
|
|
14
15
|
name: str
|
|
15
16
|
queue: asyncio.Queue | None
|
|
16
|
-
queue_size: int
|
|
17
17
|
max_tasks: int
|
|
18
18
|
halt_on_exception: bool
|
|
19
19
|
unpack_input: bool
|
|
@@ -42,7 +42,6 @@ class AsyncGraph:
|
|
|
42
42
|
unpack_input: bool = True,
|
|
43
43
|
max_tasks: int = 1,
|
|
44
44
|
queue: asyncio.Queue | None = None,
|
|
45
|
-
queue_size: int = 10_000,
|
|
46
45
|
check_async_gen: bool = True,
|
|
47
46
|
) -> None:
|
|
48
47
|
"""Add a node by providing its function and optional configurations.
|
|
@@ -53,8 +52,15 @@ class AsyncGraph:
|
|
|
53
52
|
The asynchronous generator function that this node runs.
|
|
54
53
|
See notes below for the function's requirements.
|
|
55
54
|
name : str, optional
|
|
56
|
-
The name of this node. If not provided, the
|
|
57
|
-
``
|
|
55
|
+
The name of this node. If not provided, the name is taken from
|
|
56
|
+
the node function's ``__name__``, resolved as follows:
|
|
57
|
+
|
|
58
|
+
* If ``func`` is a :func:`functools.partial` object, use
|
|
59
|
+
``func.func.__name__``.
|
|
60
|
+
* If ``func`` has a ``__wrapped__`` attribute (because it was
|
|
61
|
+
decorated via :func:`functools.wraps`), use
|
|
62
|
+
``func.__wrapped__.__name__``.
|
|
63
|
+
* Otherwise, use ``func.__name__``.
|
|
58
64
|
halt_on_exception : bool, optional
|
|
59
65
|
To halt graph execution when this node has an unhandled exception,
|
|
60
66
|
set this argument to ``True``. Defaults to ``False``.
|
|
@@ -72,23 +78,15 @@ class AsyncGraph:
|
|
|
72
78
|
with items retrieved by ``await queue.get()``.
|
|
73
79
|
This queue object must be an instance of either :class:`~asyncio.Queue` or
|
|
74
80
|
a subclass of :class:`~asyncio.Queue`.
|
|
75
|
-
If ``None`` or not given, it defaults to an ``asyncio.Queue()
|
|
76
|
-
size set by ``queue_size``.
|
|
77
|
-
queue_size : int, optional
|
|
78
|
-
The maximum number of data items allowed to be
|
|
79
|
-
in the queue object between this node as a destination node
|
|
80
|
-
and its source node(s).
|
|
81
|
-
|
|
82
|
-
.. deprecated:: 1.6.0
|
|
83
|
-
The argument ``queue_size`` is deprecated and will be removed in
|
|
84
|
-
v2.0.0. To configure the queue size, please use the argument ``queue``
|
|
85
|
-
for a queue object whose queue size is set.
|
|
81
|
+
If ``None`` or not given, it defaults to an unbounded ``asyncio.Queue()``.
|
|
86
82
|
check_async_gen : bool, optional
|
|
87
83
|
If ``True`` (the default), the callable ``func`` is verified to be an async
|
|
88
|
-
generator function by :func:`inspect.isasyncgenfunction`.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
generator function by :func:`inspect.isasyncgenfunction`. If ``func`` is a
|
|
85
|
+
:func:`functools.partial` object, or if it has a ``__wrapped__`` attribute
|
|
86
|
+
set by :func:`functools.wraps`, the embedded function is checked instead.
|
|
87
|
+
Pass in ``False`` to disable this check if ``func`` would still fail the
|
|
88
|
+
check after that unwrapping (e.g., your decorator doesn't use
|
|
89
|
+
:func:`functools.wraps`).
|
|
92
90
|
|
|
93
91
|
Notes
|
|
94
92
|
-----
|
|
@@ -141,8 +139,9 @@ class AsyncGraph:
|
|
|
141
139
|
own default values.
|
|
142
140
|
""" # noqa: E501
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
embedded_func = self._get_embedded_func(func)
|
|
143
|
+
name = name or embedded_func.__name__
|
|
144
|
+
if check_async_gen and not inspect.isasyncgenfunction(embedded_func):
|
|
146
145
|
raise TypeError(f"node '{name}' isn't an async generator function")
|
|
147
146
|
if name in self._nodes:
|
|
148
147
|
raise ValueError(f"node '{name}' already exists in the graph")
|
|
@@ -152,13 +151,31 @@ class AsyncGraph:
|
|
|
152
151
|
func=func,
|
|
153
152
|
name=name,
|
|
154
153
|
queue=queue,
|
|
155
|
-
queue_size=queue_size,
|
|
156
154
|
max_tasks=max_tasks,
|
|
157
155
|
halt_on_exception=halt_on_exception,
|
|
158
156
|
unpack_input=unpack_input,
|
|
159
157
|
)
|
|
160
158
|
self._nodes_to_edges[name] = set()
|
|
161
159
|
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _get_embedded_func(
|
|
162
|
+
func: Callable[..., AsyncGenerator],
|
|
163
|
+
) -> Callable[..., AsyncGenerator]:
|
|
164
|
+
"""Resolve the underlying function for name and async-gen detection.
|
|
165
|
+
|
|
166
|
+
Repeatedly unwraps :func:`functools.partial` objects and callables with
|
|
167
|
+
a ``__wrapped__`` attribute (set by :func:`functools.wraps`), so that
|
|
168
|
+
stacked decorators and ``partial(wrapped_func)`` combinations resolve
|
|
169
|
+
to the innermost function.
|
|
170
|
+
"""
|
|
171
|
+
while True:
|
|
172
|
+
if isinstance(func, functools.partial):
|
|
173
|
+
func = func.func
|
|
174
|
+
elif hasattr(func, "__wrapped__"):
|
|
175
|
+
func = func.__wrapped__
|
|
176
|
+
else:
|
|
177
|
+
return func
|
|
178
|
+
|
|
162
179
|
def add_edge(
|
|
163
180
|
self,
|
|
164
181
|
src_node: str | Callable[..., AsyncGenerator],
|
|
@@ -174,12 +191,12 @@ class AsyncGraph:
|
|
|
174
191
|
The destination node, either the function name or the function itself.
|
|
175
192
|
"""
|
|
176
193
|
if not isinstance(src_node, str):
|
|
177
|
-
src_node = src_node.__name__
|
|
194
|
+
src_node = self._get_embedded_func(src_node).__name__
|
|
178
195
|
if src_node not in self._nodes:
|
|
179
196
|
raise ValueError(f"src_node '{src_node}' not registered in the graph")
|
|
180
197
|
|
|
181
198
|
if not isinstance(dst_node, str):
|
|
182
|
-
dst_node = dst_node.__name__
|
|
199
|
+
dst_node = self._get_embedded_func(dst_node).__name__
|
|
183
200
|
if dst_node not in self._nodes:
|
|
184
201
|
raise ValueError(f"dst_node '{dst_node}' not registered in the graph")
|
|
185
202
|
|
|
@@ -212,29 +229,16 @@ class AsyncGraph:
|
|
|
212
229
|
i: int,
|
|
213
230
|
is_checked: list[bool],
|
|
214
231
|
iter_stack: list[bool],
|
|
215
|
-
|
|
232
|
+
nodes: list[str],
|
|
216
233
|
) -> bool:
|
|
217
234
|
"""Code based on: https://www.geeksforgeeks.org/detect-cycle-in-a-graph/"""
|
|
218
|
-
# Use the OrderedDict self._nodes_to_edges for its ordering.
|
|
219
|
-
nodes = list(self._nodes_to_edges.keys())
|
|
220
235
|
is_checked[i] = True
|
|
221
236
|
iter_stack[i] = True
|
|
222
237
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
except KeyError:
|
|
226
|
-
iter_stack[i] = False
|
|
227
|
-
return False
|
|
228
|
-
|
|
229
|
-
for dst_node in dst_nodes:
|
|
230
|
-
try:
|
|
231
|
-
edge_pos = nodes.index(dst_node)
|
|
232
|
-
except ValueError:
|
|
233
|
-
iter_stack[i] = False
|
|
234
|
-
return False
|
|
235
|
-
|
|
238
|
+
for dst_node in self._nodes_to_edges[nodes[i]]:
|
|
239
|
+
edge_pos = nodes.index(dst_node)
|
|
236
240
|
if not is_checked[edge_pos] and self._graph_validator(
|
|
237
|
-
edge_pos, is_checked, iter_stack,
|
|
241
|
+
edge_pos, is_checked, iter_stack, nodes
|
|
238
242
|
):
|
|
239
243
|
return True
|
|
240
244
|
elif iter_stack[edge_pos]:
|
|
@@ -244,24 +248,42 @@ class AsyncGraph:
|
|
|
244
248
|
return False
|
|
245
249
|
|
|
246
250
|
def _is_graph_cyclic(self) -> bool:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
nodes = list(self._nodes_to_edges.keys())
|
|
252
|
+
is_checked = [False] * len(nodes)
|
|
253
|
+
iter_stack = [False] * len(nodes)
|
|
254
|
+
for i in range(len(nodes)):
|
|
250
255
|
if not is_checked[i] and self._graph_validator(
|
|
251
|
-
i, is_checked, iter_stack,
|
|
256
|
+
i, is_checked, iter_stack, nodes
|
|
252
257
|
):
|
|
253
258
|
return True
|
|
254
259
|
return False
|
|
255
260
|
|
|
261
|
+
def _topological_sort(self) -> list[str]:
|
|
262
|
+
"""Return node names in a topological order (sources before sinks).
|
|
263
|
+
|
|
264
|
+
Sibling order among nodes at the same depth is unspecified (the
|
|
265
|
+
underlying edge storage is a ``set``). Callers must not rely on it.
|
|
266
|
+
The graph is guaranteed acyclic by :meth:`add_edge`, so this always succeeds.
|
|
267
|
+
"""
|
|
268
|
+
indegree: dict[str, int] = {name: 0 for name in self._nodes}
|
|
269
|
+
for dst_nodes in self._nodes_to_edges.values():
|
|
270
|
+
for dst in dst_nodes:
|
|
271
|
+
indegree[dst] += 1
|
|
272
|
+
|
|
273
|
+
ready = [name for name in self._nodes if indegree[name] == 0]
|
|
274
|
+
ordered: list[str] = []
|
|
275
|
+
while ready:
|
|
276
|
+
name = ready.pop(0)
|
|
277
|
+
ordered.append(name)
|
|
278
|
+
for dst in self._nodes_to_edges[name]:
|
|
279
|
+
indegree[dst] -= 1
|
|
280
|
+
if indegree[dst] == 0:
|
|
281
|
+
ready.append(dst)
|
|
282
|
+
return ordered
|
|
283
|
+
|
|
256
284
|
def _get_start_nodes(self) -> set[str]:
|
|
257
|
-
|
|
258
|
-
for
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
root_node = False
|
|
263
|
-
break
|
|
264
|
-
|
|
265
|
-
if root_node:
|
|
266
|
-
root_nodes.add(src_node)
|
|
267
|
-
return root_nodes
|
|
285
|
+
indegree: dict[str, int] = {name: 0 for name in self._nodes}
|
|
286
|
+
for dst_nodes in self._nodes_to_edges.values():
|
|
287
|
+
for dst in dst_nodes:
|
|
288
|
+
indegree[dst] += 1
|
|
289
|
+
return {name for name, d in indegree.items() if d == 0}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: async-graph-data-flow
|
|
3
|
-
Version:
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Data flow along a directed acyclic graph of asynchronous generator functions
|
|
5
5
|
Author-email: Civis Analytics <opensource@civisanalytics.com>
|
|
6
|
-
License: BSD
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
7
|
Project-URL: Documentation, https://async-graph-data-flow.readthedocs.io
|
|
8
8
|
Project-URL: Source, https://github.com/civisanalytics/async-graph-data-flow
|
|
9
9
|
Keywords: async,asynchronous,asyncio,data flow,graph
|
|
@@ -11,7 +11,6 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Intended Audience :: Information Technology
|
|
13
13
|
Classifier: Intended Audience :: Science/Research
|
|
14
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
15
14
|
Classifier: Operating System :: OS Independent
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -19,23 +18,25 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
22
|
Classifier: Framework :: AsyncIO
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE.txt
|
|
26
26
|
Provides-Extra: dev
|
|
27
|
-
Requires-Dist: black
|
|
28
|
-
Requires-Dist: flake8
|
|
29
|
-
Requires-Dist: pytest
|
|
30
|
-
Requires-Dist: mypy
|
|
31
|
-
Requires-Dist: build
|
|
32
|
-
Requires-Dist: twine
|
|
27
|
+
Requires-Dist: black; extra == "dev"
|
|
28
|
+
Requires-Dist: flake8; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy; extra == "dev"
|
|
31
|
+
Requires-Dist: build; extra == "dev"
|
|
32
|
+
Requires-Dist: twine; extra == "dev"
|
|
33
33
|
Provides-Extra: docs
|
|
34
|
-
Requires-Dist: Sphinx
|
|
35
|
-
Requires-Dist: furo
|
|
36
|
-
Requires-Dist: numpydoc
|
|
37
|
-
Requires-Dist: sphinx-copybutton
|
|
38
|
-
Requires-Dist: sphinxcontrib-mermaid
|
|
34
|
+
Requires-Dist: Sphinx; extra == "docs"
|
|
35
|
+
Requires-Dist: furo; extra == "docs"
|
|
36
|
+
Requires-Dist: numpydoc; extra == "docs"
|
|
37
|
+
Requires-Dist: sphinx-copybutton; extra == "docs"
|
|
38
|
+
Requires-Dist: sphinxcontrib-mermaid; extra == "docs"
|
|
39
|
+
Dynamic: license-file
|
|
39
40
|
|
|
40
41
|
# async-graph-data-flow
|
|
41
42
|
|
|
@@ -59,20 +60,21 @@ BSD 3-Clause License. Please see `LICENSE.txt` in the GitHub source code for det
|
|
|
59
60
|
|
|
60
61
|
The latest code under development is available on GitHub at
|
|
61
62
|
https://github.com/civisanalytics/async-graph-data-flow.
|
|
62
|
-
To obtain this version for experimental features or for development
|
|
63
|
+
To obtain this version for experimental features or for development,
|
|
64
|
+
we recommend using [uv](https://docs.astral.sh/uv/) to manage the development environment:
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
67
|
git clone https://github.com/civisanalytics/async-graph-data-flow.git
|
|
66
68
|
cd async-graph-data-flow
|
|
67
|
-
pip install -e ".[dev]"
|
|
69
|
+
uv pip install -e ".[dev]"
|
|
68
70
|
```
|
|
69
71
|
|
|
70
72
|
To run tests and styling checks:
|
|
71
73
|
|
|
72
74
|
```bash
|
|
73
|
-
pytest
|
|
74
|
-
flake8 src tests examples
|
|
75
|
-
black --check src tests examples
|
|
75
|
+
uv run pytest
|
|
76
|
+
uv run flake8 src tests examples
|
|
77
|
+
uv run black --check src tests examples
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
## Building Documentation
|
|
@@ -88,5 +90,5 @@ The builds by the Read The Docs site generate the necessary files (the HTML page
|
|
|
88
90
|
for the public documentation. All these auto-generated files are explicitly not versioned (see `.gitignore`).
|
|
89
91
|
|
|
90
92
|
To build the documentation locally (for testing and development),
|
|
91
|
-
install the full doc-related dependencies: `pip install -r docs/requirements.txt`,
|
|
92
|
-
then run `sphinx-build -b html docs/ docs/build/`.
|
|
93
|
+
install the full doc-related dependencies: `uv pip install -r docs/requirements.txt`,
|
|
94
|
+
then run `uv run sphinx-build -b html docs/ docs/build/`.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import functools
|
|
2
3
|
import time
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -75,6 +76,34 @@ def test_async_executor_linear():
|
|
|
75
76
|
assert etl_executor.data_flow_stats["load"].get("err") == 0
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
def test_async_executor_with_functools_partial_node():
|
|
80
|
+
async def transform(prefix, data):
|
|
81
|
+
yield f"{prefix}:{data}"
|
|
82
|
+
|
|
83
|
+
async def extract():
|
|
84
|
+
yield "hello"
|
|
85
|
+
yield "world"
|
|
86
|
+
|
|
87
|
+
collected: list[str] = []
|
|
88
|
+
|
|
89
|
+
async def load(data):
|
|
90
|
+
collected.append(data)
|
|
91
|
+
yield
|
|
92
|
+
|
|
93
|
+
partial_transform = functools.partial(transform, "greet")
|
|
94
|
+
|
|
95
|
+
etl_graph = AsyncGraph()
|
|
96
|
+
etl_graph.add_node(extract)
|
|
97
|
+
etl_graph.add_node(partial_transform)
|
|
98
|
+
etl_graph.add_node(load)
|
|
99
|
+
etl_graph.add_edge("extract", "transform")
|
|
100
|
+
etl_graph.add_edge("transform", "load")
|
|
101
|
+
|
|
102
|
+
AsyncExecutor(etl_graph).execute()
|
|
103
|
+
|
|
104
|
+
assert collected == ["greet:hello", "greet:world"]
|
|
105
|
+
|
|
106
|
+
|
|
78
107
|
def test_async_executor_linear_with_exception_with_halt():
|
|
79
108
|
async def extract():
|
|
80
109
|
yield "data1"
|
|
@@ -469,3 +498,72 @@ def test_graph_with_no_edges():
|
|
|
469
498
|
|
|
470
499
|
executor.execute(start_nodes={"node1": ("foo",)})
|
|
471
500
|
assert executor.start_nodes == {"node1": ("foo",)}
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def test_diamond_dag_reverse_insertion_order():
|
|
504
|
+
"""Regression test for issue #15: tail items must be processed even when
|
|
505
|
+
nodes are added in reverse topological order (sink before sources)."""
|
|
506
|
+
sink_items: list[int] = []
|
|
507
|
+
|
|
508
|
+
async def src():
|
|
509
|
+
for i in range(4):
|
|
510
|
+
yield i
|
|
511
|
+
|
|
512
|
+
async def branch_a(x):
|
|
513
|
+
yield x
|
|
514
|
+
|
|
515
|
+
async def branch_b(x):
|
|
516
|
+
yield x + 100
|
|
517
|
+
|
|
518
|
+
async def sink(x):
|
|
519
|
+
sink_items.append(x)
|
|
520
|
+
yield
|
|
521
|
+
|
|
522
|
+
graph = AsyncGraph()
|
|
523
|
+
graph.add_node(sink, unpack_input=False)
|
|
524
|
+
graph.add_node(branch_b, unpack_input=False)
|
|
525
|
+
graph.add_node(branch_a, unpack_input=False)
|
|
526
|
+
graph.add_node(src, unpack_input=False)
|
|
527
|
+
|
|
528
|
+
graph.add_edge(src, branch_a)
|
|
529
|
+
graph.add_edge(src, branch_b)
|
|
530
|
+
graph.add_edge(branch_a, sink)
|
|
531
|
+
graph.add_edge(branch_b, sink)
|
|
532
|
+
|
|
533
|
+
AsyncExecutor(graph).execute()
|
|
534
|
+
|
|
535
|
+
assert sorted(sink_items) == [0, 1, 2, 3, 100, 101, 102, 103]
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def test_diamond_dag_forward_insertion_order():
|
|
539
|
+
"""Symmetric sanity check: adding nodes in forward topological order still works."""
|
|
540
|
+
sink_items: list[int] = []
|
|
541
|
+
|
|
542
|
+
async def src():
|
|
543
|
+
for i in range(4):
|
|
544
|
+
yield i
|
|
545
|
+
|
|
546
|
+
async def branch_a(x):
|
|
547
|
+
yield x
|
|
548
|
+
|
|
549
|
+
async def branch_b(x):
|
|
550
|
+
yield x + 100
|
|
551
|
+
|
|
552
|
+
async def sink(x):
|
|
553
|
+
sink_items.append(x)
|
|
554
|
+
yield
|
|
555
|
+
|
|
556
|
+
graph = AsyncGraph()
|
|
557
|
+
graph.add_node(src, unpack_input=False)
|
|
558
|
+
graph.add_node(branch_a, unpack_input=False)
|
|
559
|
+
graph.add_node(branch_b, unpack_input=False)
|
|
560
|
+
graph.add_node(sink, unpack_input=False)
|
|
561
|
+
|
|
562
|
+
graph.add_edge(src, branch_a)
|
|
563
|
+
graph.add_edge(src, branch_b)
|
|
564
|
+
graph.add_edge(branch_a, sink)
|
|
565
|
+
graph.add_edge(branch_b, sink)
|
|
566
|
+
|
|
567
|
+
AsyncExecutor(graph).execute()
|
|
568
|
+
|
|
569
|
+
assert sorted(sink_items) == [0, 1, 2, 3, 100, 101, 102, 103]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import functools
|
|
1
2
|
import inspect
|
|
2
3
|
from unittest import mock
|
|
3
4
|
|
|
@@ -64,6 +65,66 @@ class TestAsyncGraphAddNode:
|
|
|
64
65
|
)
|
|
65
66
|
AsyncGraph().add_node(some_func, check_async_gen=False)
|
|
66
67
|
|
|
68
|
+
def test_add_node_with_functools_partial(self):
|
|
69
|
+
async def producer(prefix, message):
|
|
70
|
+
yield f"{prefix}:{message}"
|
|
71
|
+
|
|
72
|
+
partial_producer = functools.partial(producer, "hello")
|
|
73
|
+
|
|
74
|
+
graph = AsyncGraph()
|
|
75
|
+
graph.add_node(partial_producer)
|
|
76
|
+
|
|
77
|
+
assert "producer" in graph._nodes
|
|
78
|
+
assert graph._nodes["producer"].name == "producer"
|
|
79
|
+
assert graph._nodes["producer"].func is partial_producer
|
|
80
|
+
|
|
81
|
+
def test_add_node_with_functools_partial_explicit_name(self):
|
|
82
|
+
async def producer(prefix, message):
|
|
83
|
+
yield f"{prefix}:{message}"
|
|
84
|
+
|
|
85
|
+
partial_producer = functools.partial(producer, "hello")
|
|
86
|
+
|
|
87
|
+
graph = AsyncGraph()
|
|
88
|
+
graph.add_node(partial_producer, name="custom")
|
|
89
|
+
|
|
90
|
+
assert "custom" in graph._nodes
|
|
91
|
+
assert "producer" not in graph._nodes
|
|
92
|
+
assert graph._nodes["custom"].func is partial_producer
|
|
93
|
+
|
|
94
|
+
def test_add_node_with_functools_partial_non_async_gen(self):
|
|
95
|
+
def not_async_gen(prefix, message):
|
|
96
|
+
return f"{prefix}:{message}"
|
|
97
|
+
|
|
98
|
+
partial_fn = functools.partial(not_async_gen, "hello")
|
|
99
|
+
|
|
100
|
+
with pytest.raises(TypeError) as excinfo:
|
|
101
|
+
AsyncGraph().add_node(partial_fn)
|
|
102
|
+
assert str(excinfo.value) == (
|
|
103
|
+
"node 'not_async_gen' isn't an async generator function"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def test_add_node_with_functools_wraps_decorator(self):
|
|
107
|
+
async def some_func():
|
|
108
|
+
yield "foo"
|
|
109
|
+
|
|
110
|
+
def some_decorator(func):
|
|
111
|
+
@functools.wraps(func)
|
|
112
|
+
def inner_func(*args, **kwargs):
|
|
113
|
+
return func(*args, **kwargs)
|
|
114
|
+
|
|
115
|
+
return inner_func
|
|
116
|
+
|
|
117
|
+
wrapped = some_decorator(some_func)
|
|
118
|
+
|
|
119
|
+
# The wrapper itself is not an async generator function, but because
|
|
120
|
+
# functools.wraps sets __wrapped__, add_node unwraps to the inner
|
|
121
|
+
# async generator and the check passes without check_async_gen=False.
|
|
122
|
+
assert not inspect.isasyncgenfunction(wrapped)
|
|
123
|
+
graph = AsyncGraph()
|
|
124
|
+
graph.add_node(wrapped)
|
|
125
|
+
assert "some_func" in graph._nodes
|
|
126
|
+
assert graph._nodes["some_func"].func is wrapped
|
|
127
|
+
|
|
67
128
|
def test_add_node_with_valid_node_args(self):
|
|
68
129
|
etl_graph = async_graph_without_nodes_mock()
|
|
69
130
|
assert len(etl_graph._nodes.keys()) == 0
|
|
@@ -85,7 +146,6 @@ class TestAsyncGraphAddNode:
|
|
|
85
146
|
"max_tasks": 1,
|
|
86
147
|
"name": "extract_node",
|
|
87
148
|
"queue": None,
|
|
88
|
-
"queue_size": 10_000,
|
|
89
149
|
"unpack_input": True,
|
|
90
150
|
},
|
|
91
151
|
{
|
|
@@ -94,7 +154,6 @@ class TestAsyncGraphAddNode:
|
|
|
94
154
|
"max_tasks": 1,
|
|
95
155
|
"name": "transform_node",
|
|
96
156
|
"queue": None,
|
|
97
|
-
"queue_size": 10_000,
|
|
98
157
|
"unpack_input": True,
|
|
99
158
|
},
|
|
100
159
|
{
|
|
@@ -103,7 +162,6 @@ class TestAsyncGraphAddNode:
|
|
|
103
162
|
"max_tasks": 1,
|
|
104
163
|
"name": "load_node",
|
|
105
164
|
"queue": None,
|
|
106
|
-
"queue_size": 10_000,
|
|
107
165
|
"unpack_input": True,
|
|
108
166
|
},
|
|
109
167
|
]
|
|
@@ -138,6 +196,40 @@ class TestAsyncGraphAddEdge:
|
|
|
138
196
|
assert len(etl_graph.edges) == 1
|
|
139
197
|
assert etl_graph.edges == {("extract_node", "transform_node")}
|
|
140
198
|
|
|
199
|
+
def test_add_edge_with_functools_partial_callable(self):
|
|
200
|
+
async def producer(prefix, message):
|
|
201
|
+
yield f"{prefix}:{message}"
|
|
202
|
+
|
|
203
|
+
async def consumer(message):
|
|
204
|
+
yield message
|
|
205
|
+
|
|
206
|
+
partial_producer = functools.partial(producer, "hello")
|
|
207
|
+
|
|
208
|
+
graph = AsyncGraph()
|
|
209
|
+
graph.add_node(partial_producer)
|
|
210
|
+
graph.add_node(consumer)
|
|
211
|
+
graph.add_edge(src_node=partial_producer, dst_node=consumer)
|
|
212
|
+
|
|
213
|
+
assert graph.edges == {("producer", "consumer")}
|
|
214
|
+
|
|
215
|
+
def test_add_node_unwraps_stacked_wrappers(self):
|
|
216
|
+
async def some_func():
|
|
217
|
+
yield "foo"
|
|
218
|
+
|
|
219
|
+
def outer(func):
|
|
220
|
+
@functools.wraps(func)
|
|
221
|
+
def inner(*args, **kwargs):
|
|
222
|
+
return func(*args, **kwargs)
|
|
223
|
+
|
|
224
|
+
return inner
|
|
225
|
+
|
|
226
|
+
doubly_wrapped = outer(outer(some_func))
|
|
227
|
+
partial_over_wrapped = functools.partial(doubly_wrapped)
|
|
228
|
+
|
|
229
|
+
graph = AsyncGraph()
|
|
230
|
+
graph.add_node(partial_over_wrapped)
|
|
231
|
+
assert "some_func" in graph._nodes
|
|
232
|
+
|
|
141
233
|
def test_add_edge_graph_acyclic(self):
|
|
142
234
|
etl_graph = async_graph_with_nodes_mock()
|
|
143
235
|
|
|
File without changes
|
|
File without changes
|
{async_graph_data_flow-1.6.0 → async_graph_data_flow-2.0.0}/src/async_graph_data_flow/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|