async-graph-data-flow 1.5.0__tar.gz → 1.6.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.
Files changed (20) hide show
  1. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/CHANGELOG.md +21 -0
  2. {async_graph_data_flow-1.5.0/src/async_graph_data_flow.egg-info → async_graph_data_flow-1.6.0}/PKG-INFO +9 -8
  3. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/README.md +1 -1
  4. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/pyproject.toml +7 -6
  5. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/src/async_graph_data_flow/executor.py +39 -29
  6. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/src/async_graph_data_flow/graph.py +34 -13
  7. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0/src/async_graph_data_flow.egg-info}/PKG-INFO +9 -8
  8. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/src/async_graph_data_flow.egg-info/requires.txt +6 -5
  9. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/tests/test_graph.py +3 -0
  10. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/LICENSE.txt +0 -0
  11. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/MANIFEST.in +0 -0
  12. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/setup.cfg +0 -0
  13. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/src/async_graph_data_flow/__init__.py +0 -0
  14. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/src/async_graph_data_flow/py.typed +0 -0
  15. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/src/async_graph_data_flow.egg-info/SOURCES.txt +0 -0
  16. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/src/async_graph_data_flow.egg-info/dependency_links.txt +0 -0
  17. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/src/async_graph_data_flow.egg-info/not-zip-safe +0 -0
  18. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/src/async_graph_data_flow.egg-info/top_level.txt +0 -0
  19. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/tests/test_executor.py +0 -0
  20. {async_graph_data_flow-1.5.0 → async_graph_data_flow-1.6.0}/tests/test_version_number.py +0 -0
@@ -19,6 +19,27 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
19
19
 
20
20
  ### Security
21
21
 
22
+ ## [1.6.0] - 2025-03-19
23
+
24
+ ### Added
25
+ - Added support for custom edge behaviors, through the new `queue` argument at `add_node`
26
+ for a custom queue object. (#14)
27
+
28
+ ### Deprecated
29
+ - Deprecated the `queue_size` argument at `add_node` -- use the new `queue` instead. (#14)
30
+
31
+ ### Security
32
+ - Resolved CVE-2024-56326, CVE-2024-56201, and CVE-2025-27516
33
+ by updating jinja2 to v3.1.6 in requirements for Sphinx docs. (#14)
34
+
35
+ ## [1.5.1] - 2024-11-07
36
+
37
+ ### Added
38
+ - Added static type checking using mypy. (#12)
39
+
40
+ ### Fixed
41
+ - Fixed documentation to actually show example code. (#12)
42
+
22
43
  ## [1.5.0] - 2024-10-30
23
44
 
24
45
  ### Added
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: async-graph-data-flow
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Asynchronous functions that pass data along a directed acyclic graph
5
5
  Author-email: Civis Analytics <opensource@civisanalytics.com>
6
6
  License: BSD 3-Clause
@@ -24,13 +24,14 @@ 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==24.10.0; extra == "dev"
28
- Requires-Dist: flake8==7.1.1; extra == "dev"
29
- Requires-Dist: pytest==8.3.3; extra == "dev"
27
+ Requires-Dist: black==25.1.0; extra == "dev"
28
+ Requires-Dist: flake8==7.1.2; extra == "dev"
29
+ Requires-Dist: pytest==8.3.5; extra == "dev"
30
+ Requires-Dist: mypy==1.15.0; extra == "dev"
30
31
  Requires-Dist: build==1.2.2; extra == "dev"
31
- Requires-Dist: twine==5.1.1; extra == "dev"
32
+ Requires-Dist: twine==6.1.0; extra == "dev"
32
33
  Provides-Extra: docs
33
- Requires-Dist: Sphinx==8.1.3; extra == "docs"
34
+ Requires-Dist: Sphinx==8.2.3; extra == "docs"
34
35
  Requires-Dist: furo==2024.8.6; extra == "docs"
35
36
  Requires-Dist: numpydoc==1.8.0; extra == "docs"
36
37
  Requires-Dist: sphinx-copybutton==0.5.2; extra == "docs"
@@ -76,7 +77,7 @@ black --check src tests examples
76
77
 
77
78
  ## Building Documentation
78
79
 
79
- We use the Sphinx framework. The documentation source files are in `docs/source/`.
80
+ We use the Sphinx framework. The documentation source files are in `docs/`.
80
81
  These files can be updated as necessary.
81
82
 
82
83
  The public documentation is accessible at https://async-graph-data-flow.readthedocs.io.
@@ -38,7 +38,7 @@ black --check src tests examples
38
38
 
39
39
  ## Building Documentation
40
40
 
41
- We use the Sphinx framework. The documentation source files are in `docs/source/`.
41
+ We use the Sphinx framework. The documentation source files are in `docs/`.
42
42
  These files can be updated as necessary.
43
43
 
44
44
  The public documentation is accessible at https://async-graph-data-flow.readthedocs.io.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "async-graph-data-flow"
7
- version = "1.5.0"
7
+ version = "1.6.0"
8
8
  description = "Asynchronous functions that pass data along a directed acyclic graph"
9
9
  readme = "README.md"
10
10
  requires-python = ">= 3.10"
@@ -40,16 +40,17 @@ 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 == 24.10.0",
44
- "flake8 == 7.1.1",
45
- "pytest == 8.3.3",
43
+ "black == 25.1.0",
44
+ "flake8 == 7.1.2",
45
+ "pytest == 8.3.5",
46
+ "mypy == 1.15.0",
46
47
 
47
48
  # Managing source distributions
48
49
  "build == 1.2.2",
49
- "twine == 5.1.1",
50
+ "twine == 6.1.0",
50
51
  ]
51
52
  docs = [
52
- "Sphinx == 8.1.3",
53
+ "Sphinx == 8.2.3",
53
54
  "furo == 2024.8.6",
54
55
  "numpydoc == 1.8.0",
55
56
  "sphinx-copybutton == 0.5.2",
@@ -4,7 +4,8 @@ import logging
4
4
  import time
5
5
  import traceback
6
6
  from collections import deque
7
- from typing import Any, Iterable
7
+ from collections.abc import Iterable
8
+ from typing import Any
8
9
 
9
10
  from .graph import AsyncGraph, InvalidAsyncGraphError
10
11
 
@@ -20,7 +21,7 @@ class AsyncExecutor:
20
21
  self,
21
22
  graph: AsyncGraph,
22
23
  *,
23
- logger: logging.Logger = None,
24
+ logger: logging.Logger | None = None,
24
25
  max_exceptions: int = 1_000,
25
26
  ):
26
27
  """Initialize an executor.
@@ -41,23 +42,23 @@ class AsyncExecutor:
41
42
  if not isinstance(self._graph, AsyncGraph):
42
43
  raise TypeError(f"{self._graph} must be an AsyncGraph instance")
43
44
 
44
- self._node_queues = {}
45
- self._consumer_tasks = {}
45
+ self._node_queues: dict[str, asyncio.Queue] = {}
46
+ self._consumer_tasks: dict[str, asyncio.Task] = {}
46
47
  self._halt_pipeline_execution = False
47
48
  self._logger = logger if logger else _LOG
48
49
  self._max_exceptions = max_exceptions
49
50
 
50
- self._data_flow_stats = None
51
+ self._data_flow_stats: dict[str, dict[str, int]] | None = None
51
52
  self._data_flow_logging_lock = asyncio.Lock()
52
53
  self._data_flow_logging = False
53
54
  self._data_flow_logging_node_format = _DEFAULT_DATA_FLOW_LOGGING_NODE_FORMAT
54
55
  self._data_flow_logging_time_interval = _DEFAULT_DATA_FLOW_LOGGING_TIME_INTERVAL
55
- self._data_flow_logging_node_filter = self._graph._nodes.keys()
56
- self._data_flow_logging_last_timestamp = 0
56
+ self._data_flow_logging_node_filter: Iterable[str] = self._graph._nodes.keys()
57
+ self._data_flow_logging_last_timestamp = 0.0
57
58
 
58
- self._start_node_args = None
59
+ self._start_node_args: dict[str, tuple] | None = None
59
60
 
60
- self._exceptions = None
61
+ self._exceptions: dict[str, deque[Exception]] | None = None
61
62
 
62
63
  @property
63
64
  def graph(self) -> AsyncGraph:
@@ -72,7 +73,7 @@ class AsyncExecutor:
72
73
  raised from the node.
73
74
  """
74
75
  if self._exceptions is None:
75
- return
76
+ return None
76
77
  from_deque_to_list = {}
77
78
  for node_name, excs in self._exceptions.items():
78
79
  # `excs` is a deque. Turning it into a list for user-friendliness.
@@ -152,7 +153,7 @@ class AsyncExecutor:
152
153
  """Turn off data flow logging."""
153
154
  self._data_flow_logging = False
154
155
 
155
- async def _log_data_flow_nodes(self):
156
+ def _log_data_flow_nodes(self):
156
157
  for node, flow in self._data_flow_stats.items():
157
158
  if (
158
159
  self._data_flow_logging_node_filter
@@ -186,7 +187,7 @@ class AsyncExecutor:
186
187
  current_timestamp - self._data_flow_logging_last_timestamp
187
188
  > self._data_flow_logging_time_interval
188
189
  ):
189
- await self._log_data_flow_nodes()
190
+ self._log_data_flow_nodes()
190
191
  self._data_flow_logging_last_timestamp = current_timestamp
191
192
 
192
193
  queue = self._node_queues[node_name]
@@ -202,17 +203,17 @@ class AsyncExecutor:
202
203
  try:
203
204
  if len(params) == 0:
204
205
  coro = node.func()
205
- elif node.unpack_input and (
206
- (is_tuple := isinstance(data, tuple)) or isinstance(data, dict)
207
- ):
208
- coro = node.func(*data) if is_tuple else node.func(**data)
206
+ elif node.unpack_input and isinstance(data, tuple):
207
+ coro = node.func(*data)
208
+ elif node.unpack_input and isinstance(data, dict):
209
+ coro = node.func(**data)
209
210
  else:
210
211
  coro = node.func(data)
211
212
  except asyncio.CancelledError:
212
213
  continue
213
214
  except Exception as exc:
214
- await self._update_data_flow_error_stats(node_name)
215
- await self._update_exceptions(node_name, exc)
215
+ self._update_data_flow_error_stats(node_name)
216
+ self._update_exceptions(node_name, exc)
216
217
  self._logger.error(traceback.format_exc())
217
218
  if self._graph.halt_on_exception or node.halt_on_exception:
218
219
  self._logger.error(
@@ -238,8 +239,8 @@ class AsyncExecutor:
238
239
  except asyncio.CancelledError:
239
240
  break
240
241
  except Exception as exc:
241
- await self._update_data_flow_error_stats(node_name)
242
- await self._update_exceptions(node_name, exc)
242
+ self._update_data_flow_error_stats(node_name)
243
+ self._update_exceptions(node_name, exc)
243
244
  self._logger.error(traceback.format_exc())
244
245
  if self._graph.halt_on_exception or node.halt_on_exception:
245
246
  # close current agen
@@ -255,30 +256,39 @@ class AsyncExecutor:
255
256
  continue
256
257
  else:
257
258
  node_edges = self._graph._nodes_to_edges[node_name]
258
- await self._update_data_flow_in_out_stats(node_name, node_edges)
259
+ self._update_data_flow_in_out_stats(node_name, node_edges)
259
260
  await self._add_to_node_queue(node_edges, next_data_item)
260
261
 
261
262
  queue.task_done()
262
263
  except asyncio.CancelledError:
263
264
  break
264
265
 
265
- async def _update_data_flow_in_out_stats(self, in_node: str, out_nodes: set[str]):
266
+ def _update_data_flow_in_out_stats(self, in_node: str, out_nodes: set[str]):
267
+ if self._data_flow_stats is None:
268
+ return None
266
269
  self._data_flow_stats[in_node]["out"] += 1
267
270
  for node in out_nodes:
268
271
  self._data_flow_stats[node]["in"] += 1
269
272
 
270
- async def _update_data_flow_error_stats(self, node: str):
273
+ def _update_data_flow_error_stats(self, node: str):
274
+ if self._data_flow_stats is None:
275
+ return None
271
276
  self._data_flow_stats[node]["err"] += 1
272
277
 
273
- async def _update_exceptions(self, node: str, exc: Exception):
278
+ def _update_exceptions(self, node: str, exc: Exception):
279
+ if self._exceptions is None:
280
+ return None
274
281
  self._exceptions[node].append(exc)
275
282
 
276
283
  async def _pipeline_execution(self):
277
- self._data_flow_stats: dict[str, dict[str, int]] = {}
278
- self._exceptions: dict[str, deque[Exception]] = {}
284
+ self._data_flow_stats = {}
285
+ self._exceptions = {}
279
286
 
280
287
  for node_name, node in self._graph._nodes.items():
281
- queue = asyncio.Queue(maxsize=node.queue_size)
288
+ if node.queue is None:
289
+ queue = asyncio.Queue(maxsize=node.queue_size)
290
+ else:
291
+ queue = node.queue
282
292
  self._node_queues[node_name] = queue
283
293
  self._data_flow_stats[node_name] = {"in": 0, "out": 0, "err": 0}
284
294
  self._exceptions[node_name] = deque(maxlen=self._max_exceptions)
@@ -299,7 +309,7 @@ class AsyncExecutor:
299
309
  await asyncio.gather(*self._consumer_tasks.values())
300
310
 
301
311
  if self._data_flow_logging:
302
- await self._log_data_flow_nodes()
312
+ self._log_data_flow_nodes()
303
313
 
304
314
  def _get_start_node_args(self, start_node_args) -> dict[str, tuple]:
305
315
  if start_node_args is None:
@@ -313,7 +323,7 @@ class AsyncExecutor:
313
323
  raise TypeError(f"args for the node '{node}' isn't a tuple: {args}")
314
324
  return start_node_args
315
325
 
316
- def execute(self, start_nodes: dict[str, tuple] = None) -> None:
326
+ def execute(self, start_nodes: dict[str, tuple] | None = None) -> None:
317
327
  """Start executing the functions along the graph.
318
328
 
319
329
  Parameters
@@ -1,9 +1,8 @@
1
+ import asyncio
1
2
  import inspect
2
3
  from collections import OrderedDict
3
- from typing import Any, Callable, NamedTuple
4
-
5
-
6
- _DEFAULT_NUM_OF_WORKERS = 1
4
+ from collections.abc import AsyncGenerator, Callable
5
+ from typing import Any, NamedTuple
7
6
 
8
7
 
9
8
  class InvalidAsyncGraphError(Exception):
@@ -11,8 +10,9 @@ class InvalidAsyncGraphError(Exception):
11
10
 
12
11
 
13
12
  class _Node(NamedTuple):
14
- func: Callable
13
+ func: Callable[..., AsyncGenerator]
15
14
  name: str
15
+ queue: asyncio.Queue | None
16
16
  queue_size: int
17
17
  max_tasks: int
18
18
  halt_on_exception: bool
@@ -35,12 +35,13 @@ class AsyncGraph:
35
35
 
36
36
  def add_node(
37
37
  self,
38
- func: Callable,
38
+ func: Callable[..., AsyncGenerator],
39
39
  *,
40
40
  name: str | None = None,
41
41
  halt_on_exception: bool = False,
42
42
  unpack_input: bool = True,
43
43
  max_tasks: int = 1,
44
+ queue: asyncio.Queue | None = None,
44
45
  queue_size: int = 10_000,
45
46
  check_async_gen: bool = True,
46
47
  ) -> None:
@@ -48,8 +49,8 @@ class AsyncGraph:
48
49
 
49
50
  Parameters
50
51
  ----------
51
- func : Callable
52
- The function that this node runs.
52
+ func : Callable[..., AsyncGenerator]
53
+ The asynchronous generator function that this node runs.
53
54
  See notes below for the function's requirements.
54
55
  name : str, optional
55
56
  The name of this node. If not provided, the ``__name__`` attribute
@@ -65,10 +66,23 @@ class AsyncGraph:
65
66
  See notes below for more details.
66
67
  max_tasks : int, optional
67
68
  The number of tasks that this node runs concurrently.
69
+ queue : asyncio.Queue, optional
70
+ The queue object that collects items from this node's source nodes,
71
+ via ``await queue.put(item)``, and then feed into this node
72
+ with items retrieved by ``await queue.get()``.
73
+ This queue object must be an instance of either :class:`~asyncio.Queue` or
74
+ a subclass of :class:`~asyncio.Queue`.
75
+ If ``None`` or not given, it defaults to an ``asyncio.Queue()`` with max
76
+ size set by ``queue_size``.
68
77
  queue_size : int, optional
69
78
  The maximum number of data items allowed to be
70
- in the :class:`~asyncio.Queue` object between this node as a source
71
- and its destination node(s).
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.
72
86
  check_async_gen : bool, optional
73
87
  If ``True`` (the default), the callable ``func`` is verified to be an async
74
88
  generator function by :func:`inspect.isasyncgenfunction`.
@@ -132,9 +146,12 @@ class AsyncGraph:
132
146
  raise TypeError(f"node '{name}' isn't an async generator function")
133
147
  if name in self._nodes:
134
148
  raise ValueError(f"node '{name}' already exists in the graph")
149
+ if queue is not None and not isinstance(queue, asyncio.Queue):
150
+ raise TypeError(f"queue must be an instance of asyncio.Queue: {queue}")
135
151
  self._nodes[name] = _Node(
136
152
  func=func,
137
153
  name=name,
154
+ queue=queue,
138
155
  queue_size=queue_size,
139
156
  max_tasks=max_tasks,
140
157
  halt_on_exception=halt_on_exception,
@@ -142,14 +159,18 @@ class AsyncGraph:
142
159
  )
143
160
  self._nodes_to_edges[name] = set()
144
161
 
145
- def add_edge(self, src_node: str | Callable, dst_node: str | Callable) -> None:
162
+ def add_edge(
163
+ self,
164
+ src_node: str | Callable[..., AsyncGenerator],
165
+ dst_node: str | Callable[..., AsyncGenerator],
166
+ ) -> None:
146
167
  """Add an edge.
147
168
 
148
169
  Parameters
149
170
  ----------
150
- src_node : str | Callable
171
+ src_node : str | Callable[..., AsyncGenerator]
151
172
  The source node, either the function name or the function itself.
152
- dst_node : str | Callable
173
+ dst_node : str | Callable[..., AsyncGenerator]
153
174
  The destination node, either the function name or the function itself.
154
175
  """
155
176
  if not isinstance(src_node, str):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: async-graph-data-flow
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Asynchronous functions that pass data along a directed acyclic graph
5
5
  Author-email: Civis Analytics <opensource@civisanalytics.com>
6
6
  License: BSD 3-Clause
@@ -24,13 +24,14 @@ 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==24.10.0; extra == "dev"
28
- Requires-Dist: flake8==7.1.1; extra == "dev"
29
- Requires-Dist: pytest==8.3.3; extra == "dev"
27
+ Requires-Dist: black==25.1.0; extra == "dev"
28
+ Requires-Dist: flake8==7.1.2; extra == "dev"
29
+ Requires-Dist: pytest==8.3.5; extra == "dev"
30
+ Requires-Dist: mypy==1.15.0; extra == "dev"
30
31
  Requires-Dist: build==1.2.2; extra == "dev"
31
- Requires-Dist: twine==5.1.1; extra == "dev"
32
+ Requires-Dist: twine==6.1.0; extra == "dev"
32
33
  Provides-Extra: docs
33
- Requires-Dist: Sphinx==8.1.3; extra == "docs"
34
+ Requires-Dist: Sphinx==8.2.3; extra == "docs"
34
35
  Requires-Dist: furo==2024.8.6; extra == "docs"
35
36
  Requires-Dist: numpydoc==1.8.0; extra == "docs"
36
37
  Requires-Dist: sphinx-copybutton==0.5.2; extra == "docs"
@@ -76,7 +77,7 @@ black --check src tests examples
76
77
 
77
78
  ## Building Documentation
78
79
 
79
- We use the Sphinx framework. The documentation source files are in `docs/source/`.
80
+ We use the Sphinx framework. The documentation source files are in `docs/`.
80
81
  These files can be updated as necessary.
81
82
 
82
83
  The public documentation is accessible at https://async-graph-data-flow.readthedocs.io.
@@ -1,13 +1,14 @@
1
1
 
2
2
  [dev]
3
- black==24.10.0
4
- flake8==7.1.1
5
- pytest==8.3.3
3
+ black==25.1.0
4
+ flake8==7.1.2
5
+ pytest==8.3.5
6
+ mypy==1.15.0
6
7
  build==1.2.2
7
- twine==5.1.1
8
+ twine==6.1.0
8
9
 
9
10
  [docs]
10
- Sphinx==8.1.3
11
+ Sphinx==8.2.3
11
12
  furo==2024.8.6
12
13
  numpydoc==1.8.0
13
14
  sphinx-copybutton==0.5.2
@@ -84,6 +84,7 @@ class TestAsyncGraphAddNode:
84
84
  "halt_on_exception": False,
85
85
  "max_tasks": 1,
86
86
  "name": "extract_node",
87
+ "queue": None,
87
88
  "queue_size": 10_000,
88
89
  "unpack_input": True,
89
90
  },
@@ -92,6 +93,7 @@ class TestAsyncGraphAddNode:
92
93
  "halt_on_exception": False,
93
94
  "max_tasks": 1,
94
95
  "name": "transform_node",
96
+ "queue": None,
95
97
  "queue_size": 10_000,
96
98
  "unpack_input": True,
97
99
  },
@@ -100,6 +102,7 @@ class TestAsyncGraphAddNode:
100
102
  "halt_on_exception": False,
101
103
  "max_tasks": 1,
102
104
  "name": "load_node",
105
+ "queue": None,
103
106
  "queue_size": 10_000,
104
107
  "unpack_input": True,
105
108
  },