queutils 0.8.4__tar.gz → 0.9.1__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 (28) hide show
  1. queutils-0.9.1/.github/workflows/codeql.yml +100 -0
  2. {queutils-0.8.4 → queutils-0.9.1}/.github/workflows/python-package.yml +1 -1
  3. {queutils-0.8.4 → queutils-0.9.1}/PKG-INFO +4 -2
  4. {queutils-0.8.4 → queutils-0.9.1}/pyproject.toml +7 -3
  5. {queutils-0.8.4 → queutils-0.9.1}/src/queutils/__init__.py +5 -0
  6. {queutils-0.8.4 → queutils-0.9.1}/src/queutils/asyncqueue.py +3 -5
  7. queutils-0.9.1/src/queutils/categorycounterqueue.py +116 -0
  8. {queutils-0.8.4 → queutils-0.9.1}/src/queutils/iterablequeue.py +14 -13
  9. queutils-0.9.1/tests/test_ccategorycounterqueue.py +333 -0
  10. {queutils-0.8.4 → queutils-0.9.1}/.github/workflows/dependency-review.yml +0 -0
  11. {queutils-0.8.4 → queutils-0.9.1}/.github/workflows/python-publish.yml +0 -0
  12. {queutils-0.8.4 → queutils-0.9.1}/.gitignore +0 -0
  13. {queutils-0.8.4 → queutils-0.9.1}/LICENSE +0 -0
  14. {queutils-0.8.4 → queutils-0.9.1}/README.md +0 -0
  15. {queutils-0.8.4 → queutils-0.9.1}/codecov.yml +0 -0
  16. {queutils-0.8.4 → queutils-0.9.1}/demos/asyncqueue_demo.py +0 -0
  17. {queutils-0.8.4 → queutils-0.9.1}/demos/filequeue_demo.py +0 -0
  18. {queutils-0.8.4 → queutils-0.9.1}/demos/iterablequeue_demo.py +0 -0
  19. {queutils-0.8.4 → queutils-0.9.1}/docs/asyncqueue.md +0 -0
  20. {queutils-0.8.4 → queutils-0.9.1}/docs/filequeue.md +0 -0
  21. {queutils-0.8.4 → queutils-0.9.1}/docs/iterablequeue.md +0 -0
  22. {queutils-0.8.4 → queutils-0.9.1}/src/queutils/countable.py +0 -0
  23. {queutils-0.8.4 → queutils-0.9.1}/src/queutils/filequeue.py +0 -0
  24. {queutils-0.8.4 → queutils-0.9.1}/src/queutils/py.typed +0 -0
  25. {queutils-0.8.4 → queutils-0.9.1}/tests/test_asyncqueue.py +0 -0
  26. {queutils-0.8.4 → queutils-0.9.1}/tests/test_demos.py +0 -0
  27. {queutils-0.8.4 → queutils-0.9.1}/tests/test_filequeue.py +0 -0
  28. {queutils-0.8.4 → queutils-0.9.1}/tests/test_iterablequeue.py +0 -0
@@ -0,0 +1,100 @@
1
+ # For most projects, this workflow file will not need changing; you simply need
2
+ # to commit it to your repository.
3
+ #
4
+ # You may wish to alter this file to override the set of languages analyzed,
5
+ # or to provide custom queries or build logic.
6
+ #
7
+ # ******** NOTE ********
8
+ # We have attempted to detect the languages in your repository. Please check
9
+ # the `language` matrix defined below to confirm you have the correct set of
10
+ # supported CodeQL languages.
11
+ #
12
+ name: "CodeQL Advanced"
13
+
14
+ on:
15
+ push:
16
+ branches: [ "main" ]
17
+ pull_request:
18
+ branches: [ "main" ]
19
+ schedule:
20
+ - cron: '39 23 8 * *'
21
+
22
+ jobs:
23
+ analyze:
24
+ name: Analyze (${{ matrix.language }})
25
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
26
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
27
+ # - https://gh.io/supported-runners-and-hardware-resources
28
+ # - https://gh.io/using-larger-runners (GitHub.com only)
29
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
30
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
31
+ permissions:
32
+ # required for all workflows
33
+ security-events: write
34
+
35
+ # required to fetch internal or private CodeQL packs
36
+ packages: read
37
+
38
+ # only required for workflows in private repositories
39
+ actions: read
40
+ contents: read
41
+
42
+ strategy:
43
+ fail-fast: false
44
+ matrix:
45
+ include:
46
+ - language: actions
47
+ build-mode: none
48
+ - language: python
49
+ build-mode: none
50
+ # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
51
+ # Use `c-cpp` to analyze code written in C, C++ or both
52
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
53
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
54
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
55
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
56
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
57
+ # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
58
+ steps:
59
+ - name: Checkout repository
60
+ uses: actions/checkout@v4
61
+
62
+ # Add any setup steps before running the `github/codeql-action/init` action.
63
+ # This includes steps like installing compilers or runtimes (`actions/setup-node`
64
+ # or others). This is typically only required for manual builds.
65
+ # - name: Setup runtime (example)
66
+ # uses: actions/setup-example@v1
67
+
68
+ # Initializes the CodeQL tools for scanning.
69
+ - name: Initialize CodeQL
70
+ uses: github/codeql-action/init@v3
71
+ with:
72
+ languages: ${{ matrix.language }}
73
+ build-mode: ${{ matrix.build-mode }}
74
+ # If you wish to specify custom queries, you can do so here or in a config file.
75
+ # By default, queries listed here will override any specified in a config file.
76
+ # Prefix the list here with "+" to use these queries and those in the config file.
77
+
78
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
79
+ # queries: security-extended,security-and-quality
80
+
81
+ # If the analyze step fails for one of the languages you are analyzing with
82
+ # "We were unable to automatically build your code", modify the matrix above
83
+ # to set the build mode to "manual" for that language. Then modify this step
84
+ # to build your code.
85
+ # ℹ️ Command-line programs to run using the OS shell.
86
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
87
+ - if: matrix.build-mode == 'manual'
88
+ shell: bash
89
+ run: |
90
+ echo 'If you are using a "manual" build mode for one or more of the' \
91
+ 'languages you are analyzing, replace this with the commands to build' \
92
+ 'your code, for example:'
93
+ echo ' make bootstrap'
94
+ echo ' make release'
95
+ exit 1
96
+
97
+ - name: Perform CodeQL Analysis
98
+ uses: github/codeql-action/analyze@v3
99
+ with:
100
+ category: "/language:${{matrix.language}}"
@@ -14,7 +14,7 @@ jobs:
14
14
  strategy:
15
15
  fail-fast: false
16
16
  matrix:
17
- python-version: ["3.11", "3.12"]
17
+ python-version: ["3.11", "3.12", "3.13"]
18
18
  os: [ ubuntu-latest, windows-latest, macos-latest ]
19
19
  runs-on: ${{ matrix.os }}
20
20
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: queutils
3
- Version: 0.8.4
3
+ Version: 0.9.1
4
4
  Summary: Handy Python Queue utilies
5
5
  Project-URL: Homepage, https://github.com/Jylpah/queutils
6
6
  Project-URL: Bug Tracker, https://github.com/Jylpah/queutils/issues
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Topic :: Software Development :: Libraries
15
15
  Requires-Python: >=3.11
16
16
  Requires-Dist: aioconsole>=0.6
17
+ Requires-Dist: deprecated>=1.2.18
17
18
  Provides-Extra: dev
18
19
  Requires-Dist: build>=0.10; extra == 'dev'
19
20
  Requires-Dist: hatchling>=1.22.4; extra == 'dev'
@@ -25,6 +26,7 @@ Requires-Dist: pytest-datafiles>=3.0; extra == 'dev'
25
26
  Requires-Dist: pytest-timeout>=2.2; extra == 'dev'
26
27
  Requires-Dist: pytest>=8.0; extra == 'dev'
27
28
  Requires-Dist: ruff>=0.1.9; extra == 'dev'
29
+ Requires-Dist: types-deprecated>=1.2.15; extra == 'dev'
28
30
  Description-Content-Type: text/markdown
29
31
 
30
32
  [![Python package](https://github.com/Jylpah/queutils/actions/workflows/python-package.yml/badge.svg)](https://github.com/Jylpah/queutils/actions/workflows/python-package.yml) [![codecov](https://codecov.io/gh/Jylpah/queutils/graph/badge.svg?token=rMKdbfHOFs)](https://codecov.io/gh/Jylpah/queutils)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "queutils"
3
- version = "0.8.4"
3
+ version = "0.9.1"
4
4
  authors = [{ name = "Jylpah", email = "jylpah@gmail.com" }]
5
5
  description = "Handy Python Queue utilies"
6
6
  readme = "README.md"
@@ -13,7 +13,7 @@ classifiers = [
13
13
  "Framework :: AsyncIO",
14
14
  "Topic :: Software Development :: Libraries",
15
15
  ]
16
- dependencies = ["aioconsole>=0.6"]
16
+ dependencies = ["aioconsole>=0.6", "Deprecated>=1.2.18"]
17
17
 
18
18
  [project.optional-dependencies]
19
19
  dev = [
@@ -27,6 +27,7 @@ dev = [
27
27
  "pytest-cov>=4.1",
28
28
  "pytest-timeout>=2.2",
29
29
  "ruff>=0.1.9",
30
+ "types-Deprecated>=1.2.15",
30
31
  ]
31
32
 
32
33
 
@@ -59,7 +60,10 @@ lint.fixable = ["ALL"]
59
60
  minversion = "7.4"
60
61
  addopts = ["-v", "--cov=src"]
61
62
  testpaths = ["tests", "demos"]
62
- pythonpath = "src" # avoid import path append in test files
63
+ pythonpath = "src" # avoid import path append in test files
64
+ asyncio_default_fixture_loop_scope = "function"
63
65
 
64
66
  [tool.pyright]
65
67
  reportGeneralTypeIssues = false
68
+ reportInvalidStringEscapeSequence = false
69
+ typeCheckingMode = "off"
@@ -2,10 +2,15 @@ from .countable import Countable as Countable
2
2
  from .asyncqueue import AsyncQueue as AsyncQueue
3
3
  from .iterablequeue import IterableQueue as IterableQueue, QueueDone as QueueDone
4
4
  from .filequeue import FileQueue as FileQueue
5
+ from .categorycounterqueue import (
6
+ QCounter as QCounter,
7
+ CategoryCounterQueue as CategoryCounterQueue,
8
+ )
5
9
 
6
10
  __all__ = [
7
11
  "asyncqueue",
8
12
  "countable",
13
+ "categorycounterqueue",
9
14
  "filequeue",
10
15
  "iterablequeue",
11
16
  ]
@@ -1,7 +1,7 @@
1
1
  # -----------------------------------------------------------
2
2
  # Class AsyncQueue(asyncio.Queue, Generic[T])
3
3
  #
4
- # asyncio wrapper for non-async queue.Queue. Can be used to create
4
+ # asyncio wrapper for non-async queue.Queue. Can be used to create
5
5
  # an asyncio.Queue out of a (non-async) multiprocessing.Queue.
6
6
  #
7
7
  # -----------------------------------------------------------
@@ -10,7 +10,6 @@ __author__ = "Jylpah"
10
10
  __copyright__ = "Copyright 2024, Jylpah <Jylpah@gmail.com>"
11
11
  __credits__ = ["Jylpah"]
12
12
  __license__ = "MIT"
13
- # __version__ = "1.0"
14
13
  __maintainer__ = "Jylpah"
15
14
  __email__ = "Jylpah@gmail.com"
16
15
  __status__ = "Production"
@@ -35,18 +34,17 @@ error = logger.error
35
34
 
36
35
  class AsyncQueue(asyncio.Queue, Generic[T]):
37
36
  """
38
- Async wrapper for non-async queue.Queue. Can be used to create
37
+ Async wrapper for non-async queue.Queue. Can be used to create
39
38
  an asyncio.Queue out of a (non-async) multiprocessing.Queue.
40
39
  """
41
40
 
42
- def __init__(self, queue: Queue[T], asleep: float = 0.01):
41
+ def __init__(self, queue: Queue[T], asleep: float = 0.001):
43
42
  self._Q: Queue[T] = queue
44
43
  # self._maxsize: int = queue.maxsize
45
44
  self._done: int = 0
46
45
  self._items: int = 0
47
46
  self._sleep: float = asleep
48
47
 
49
-
50
48
  @property
51
49
  def maxsize(self) -> int:
52
50
  """not supported by queue.Queue"""
@@ -0,0 +1,116 @@
1
+ from asyncio import Queue
2
+ from typing import TypeVar
3
+ from deprecated import deprecated
4
+ from .countable import Countable
5
+ from .iterablequeue import IterableQueue, QueueDone
6
+ from collections import defaultdict
7
+ import logging
8
+
9
+ logger = logging.getLogger()
10
+ error = logger.error
11
+ message = logger.warning
12
+ verbose = logger.info
13
+ debug = logger.debug
14
+
15
+ ###########################################
16
+ #
17
+ # class CounterQueue
18
+ #
19
+ ###########################################
20
+ T = TypeVar("T")
21
+
22
+
23
+ @deprecated(version="0.9.1", reason="Use CategoryCounterQueue instead")
24
+ class CounterQueue(Queue[T], Countable):
25
+ """
26
+ CounterQueue is a asyncio.Queue for counting items
27
+ """
28
+
29
+ _counter: int
30
+ _count_items: bool
31
+ _batch: int
32
+
33
+ def __init__(
34
+ self, *args, count_items: bool = True, batch: int = 1, **kwargs
35
+ ) -> None:
36
+ super().__init__(*args, **kwargs)
37
+ self._counter = 0
38
+ self._count_items = count_items
39
+ self._batch = batch
40
+
41
+ def task_done(self) -> None:
42
+ super().task_done()
43
+ if self._count_items:
44
+ self._counter += 1
45
+ return None
46
+
47
+ @property
48
+ def count(self) -> int:
49
+ """Return number of completed tasks"""
50
+ return self._counter * self._batch
51
+
52
+ @property
53
+ def count_items(self) -> bool:
54
+ """Whether or not count items"""
55
+ return self._count_items
56
+
57
+
58
+ class CategoryCounterQueue(IterableQueue[tuple[str, int]]):
59
+ """
60
+ CategorySummerQueue is a asyncio.Queue for summing up values by category
61
+ """
62
+
63
+ _counter: defaultdict[str, int]
64
+ _batch: int
65
+
66
+ def __init__(self, *args, batch: int = 1, **kwargs) -> None:
67
+ super().__init__(*args, **kwargs)
68
+ self._batch = batch
69
+ self._counter = defaultdict(int)
70
+
71
+ async def receive(self) -> tuple[str, int]:
72
+ """Receive a category value from the queue and sum it"""
73
+ category: str
74
+ value: int
75
+ category, value = await super().get()
76
+ self._counter[category] += value
77
+ super().task_done()
78
+ return (category, value)
79
+
80
+ async def send(self, category: str = "count", value: int = 1) -> None:
81
+ """Send count of a category"""
82
+ await super().put((category, value))
83
+ return None
84
+
85
+ def get_count(self, category: str = "count") -> int:
86
+ """Return count of a category"""
87
+ return self._counter[category]
88
+
89
+ def get_counts(self) -> defaultdict[str, int]:
90
+ """Return counts of all categories"""
91
+ return self._counter
92
+
93
+ async def listen(self) -> defaultdict[str, int]:
94
+ """Listen for category values"""
95
+ try:
96
+ while True:
97
+ await self.receive()
98
+ except QueueDone:
99
+ pass
100
+ return self.get_counts()
101
+
102
+
103
+ class QCounter:
104
+ def __init__(self, Q: Queue[int]):
105
+ self._count = 0
106
+ self._Q: Queue[int] = Q
107
+
108
+ @property
109
+ def count(self) -> int:
110
+ return self._count
111
+
112
+ async def start(self) -> None:
113
+ """Read and count items from Q"""
114
+ while True:
115
+ self._count += await self._Q.get()
116
+ self._Q.task_done()
@@ -1,7 +1,7 @@
1
1
  # -----------------------------------------------------------
2
2
  # Class IterableQueue(asyncio.Queue[T], AsyncIterable[T], Countable)
3
3
  #
4
- # IterableQueue is asyncio.Queue subclass that can be iterated asynchronusly.
4
+ # IterableQueue is asyncio.Queue subclass that can be iterated asynchronusly.
5
5
  # IterableQueue terminates automatically when the queue has been
6
6
  # filled and emptied.
7
7
  #
@@ -23,7 +23,7 @@ from .countable import Countable
23
23
  import logging
24
24
 
25
25
  # Setup logging
26
- logger = logging.getLogger()
26
+ logger = logging.getLogger(__name__)
27
27
  error = logger.error
28
28
  message = logger.warning
29
29
  verbose = logger.info
@@ -31,24 +31,26 @@ debug = logger.debug
31
31
 
32
32
  T = TypeVar("T")
33
33
 
34
+
34
35
  class QueueDone(Exception):
35
36
  """
36
37
  Exception to mark an IterableQueue as filled and emptied i.e. done
37
38
  """
39
+
38
40
  pass
39
41
 
40
42
 
41
43
  class IterableQueue(Queue[T], AsyncIterable[T], Countable):
42
44
  """
43
- IterableQueue is asyncio.Queue subclass that can be iterated asynchronusly.
44
-
45
+ IterableQueue is asyncio.Queue subclass that can be iterated asynchronusly.
46
+
45
47
  IterableQueue terminates automatically when the queue has been
46
48
  filled and emptied. Supports:
47
49
  - asyncio.Queue() interface, _nowait() methods are experimental
48
50
  - AsyncIterable(): async for item in queue:
49
51
  - Automatic termination of the consumers when the queue has been emptied with QueueDone exception
50
52
  - Producers must be registered with add_producer() and they must notify the queue
51
- with finish() once they have finished adding items
53
+ with finish() once they have finished adding items
52
54
  - Countable interface to count number of items task_done() through 'count' property
53
55
  - Countable property can be disabled with count_items=False. This is useful when you
54
56
  want to sum the count of multiple IterableQueues
@@ -74,16 +76,16 @@ class IterableQueue(Queue[T], AsyncIterable[T], Countable):
74
76
 
75
77
  @property
76
78
  def is_filled(self) -> bool:
77
- """"
78
- True if all the producers finished filling the queue.
79
+ """ "
80
+ True if all the producers finished filling the queue.
79
81
  New items cannot be added to a filled queue nor can new producers can be added.
80
82
  """
81
83
  return self._filled.is_set()
82
-
84
+
83
85
  @property
84
86
  def is_done(self) -> bool:
85
87
  """
86
- Has the queue been filled, emptied and all the items have been marked with task_done()
88
+ Has the queue been filled, emptied and all the items have been marked with task_done()
87
89
  """
88
90
  return self.is_filled and self.empty() and not self.has_wip
89
91
 
@@ -91,7 +93,6 @@ class IterableQueue(Queue[T], AsyncIterable[T], Countable):
91
93
  def maxsize(self) -> int:
92
94
  return self._Q.maxsize
93
95
 
94
-
95
96
  def full(self) -> bool:
96
97
  """
97
98
  True if the queue is full
@@ -122,8 +123,8 @@ class IterableQueue(Queue[T], AsyncIterable[T], Countable):
122
123
  @property
123
124
  def wip(self) -> int:
124
125
  """
125
- Number of items in progress i.e. items that have been
126
- read from the queue, but not marked with task_done()
126
+ Number of items in progress i.e. items that have been
127
+ read from the queue, but not marked with task_done()
127
128
  """
128
129
  return self._wip
129
130
 
@@ -236,7 +237,7 @@ class IterableQueue(Queue[T], AsyncIterable[T], Countable):
236
237
  def get_nowait(self) -> T:
237
238
  """
238
239
  Experimental asyncio.Queue.get_nowait() implementation
239
- """
240
+ """
240
241
  item: T | None = self._Q.get_nowait()
241
242
  if item is None:
242
243
  self._empty.set()
@@ -0,0 +1,333 @@
1
+ import pytest # type: ignore
2
+ from asyncio.queues import QueueEmpty, QueueFull
3
+ from asyncio import (
4
+ Task,
5
+ create_task,
6
+ sleep,
7
+ gather,
8
+ timeout,
9
+ TimeoutError,
10
+ CancelledError,
11
+ )
12
+ from random import random, choice, randint
13
+ import string
14
+ from collections import defaultdict
15
+
16
+ from queutils import IterableQueue, CategoryCounterQueue, QueueDone
17
+
18
+
19
+ def randomword(length: int) -> str:
20
+ """Generate a random word of fixed length"""
21
+ # https://stackoverflow.com/a/2030081/12946084
22
+ letters: str = string.ascii_lowercase
23
+ return "".join(choice(letters) for i in range(length))
24
+
25
+
26
+ QSIZE: int = 10
27
+ N: int = 100 # N >> QSIZE
28
+ THREADS: int = 4
29
+ # N : int = int(1e10)
30
+
31
+
32
+ @pytest.fixture
33
+ def test_interablequeue_int() -> IterableQueue[int]:
34
+ return IterableQueue[int](maxsize=QSIZE)
35
+
36
+
37
+ async def _producer_int(
38
+ Q: IterableQueue[int], n: int, finish: bool = False, wait: float = 0
39
+ ) -> None:
40
+ await Q.add_producer(N=1)
41
+ await sleep(wait)
42
+ try:
43
+ for i in range(n):
44
+ await sleep(wait * random())
45
+ await Q.put(i)
46
+ except QueueDone:
47
+ pass
48
+ if finish:
49
+ await Q.finish()
50
+ return None
51
+
52
+
53
+ async def _consumer_int(Q: IterableQueue[int], n: int = -1, wait: float = 0) -> bool:
54
+ try:
55
+ while n != 0:
56
+ _ = await Q.get()
57
+ await sleep(wait * random())
58
+ Q.task_done()
59
+ n -= 1
60
+ except QueueDone:
61
+ pass
62
+ except CancelledError:
63
+ raise
64
+ return True
65
+
66
+
67
+ @pytest.mark.timeout(10)
68
+ @pytest.mark.asyncio
69
+ async def test_1_put_get_async(test_interablequeue_int: IterableQueue[int]):
70
+ """Test: put(), get(), join(), qsize(), empty() == True"""
71
+ Q = test_interablequeue_int
72
+ try:
73
+ async with timeout(5):
74
+ await _producer_int(Q, QSIZE - 1, finish=True)
75
+ except TimeoutError:
76
+ assert False, "IterableQueue got stuck"
77
+ assert Q.qsize() == QSIZE - 1, (
78
+ f"qsize() returned {Q.qsize()}, should be {QSIZE - 1}"
79
+ )
80
+ try:
81
+ await Q.put(1)
82
+ assert False, "Queue is filled and put() should raise an exception"
83
+ except QueueDone:
84
+ pass # Queue is done and put() should raise an exception
85
+ assert not Q.is_done, "is_done returned True even queue is not finished"
86
+ consumer: Task = create_task(_consumer_int(Q))
87
+ try:
88
+ async with timeout(5):
89
+ await Q.join()
90
+ await Q.get()
91
+ assert False, "Queue is done and put() should raise an exception"
92
+ except TimeoutError:
93
+ assert False, "IterableQueue.join() took too long"
94
+ except QueueDone:
95
+ pass # should be raised
96
+ assert Q.qsize() == 0, "queue not empty"
97
+ assert Q.empty(), "queue not empty"
98
+ consumer.cancel()
99
+
100
+
101
+ @pytest.mark.timeout(10)
102
+ @pytest.mark.asyncio
103
+ async def test_2_put_get_nowait(test_interablequeue_int: IterableQueue[int]):
104
+ """Test put_nowait() and get_nowait() methods"""
105
+ Q = test_interablequeue_int
106
+ producer: Task = create_task(_producer_int(Q, N))
107
+ await sleep(1)
108
+ # In theory this could fail without a real error
109
+ # if QSIZE is huge and/or system is slow
110
+ assert Q.qsize() == Q.maxsize, "Queue was supposed to be at maxsize"
111
+ assert Q.full(), "Queue should be full"
112
+ assert not Q.empty(), "Queue should not be empty"
113
+
114
+ try:
115
+ Q.put_nowait(1)
116
+ assert False, "Queue was supposed to be full, but was not"
117
+ except QueueFull:
118
+ pass # OK, Queue was supposed to be full
119
+
120
+ try:
121
+ while True:
122
+ _ = Q.get_nowait()
123
+ Q.task_done()
124
+ await sleep(0.01)
125
+ except QueueEmpty:
126
+ assert Q.qsize() == 0, "Queue size should be zero"
127
+
128
+ try:
129
+ async with timeout(5):
130
+ await Q.finish()
131
+ await Q.join()
132
+ except TimeoutError:
133
+ assert False, "Queue.join() took longer than it should"
134
+ assert Q.qsize() == 0, "queue size is > 0 even it should be empty"
135
+ assert Q.empty(), "queue not empty()"
136
+ producer.cancel()
137
+
138
+
139
+ @pytest.mark.timeout(10)
140
+ @pytest.mark.asyncio
141
+ async def test_3_multiple_producers(test_interablequeue_int: IterableQueue[int]):
142
+ Q = test_interablequeue_int
143
+ workers: list[Task] = list()
144
+ for _ in range(THREADS):
145
+ workers.append(create_task(_producer_int(Q, N, finish=True, wait=0.05)))
146
+ try:
147
+ assert not Q.is_done, "is_done returned True even queue is not finished"
148
+ async with timeout(10):
149
+ async for _ in Q:
150
+ pass
151
+ except TimeoutError:
152
+ assert False, "IterableQueue.join() took too long"
153
+ except QueueDone:
154
+ pass # Queue is done
155
+
156
+ assert Q.qsize() == 0, f"queue size is {Q.qsize()} even it should be empty"
157
+ assert Q.empty(), "queue not empty"
158
+ for w in workers:
159
+ w.cancel()
160
+
161
+
162
+ @pytest.mark.timeout(10)
163
+ @pytest.mark.asyncio
164
+ async def test_4_multiple_producers_consumers(
165
+ test_interablequeue_int: IterableQueue[int],
166
+ ):
167
+ Q = test_interablequeue_int
168
+ producers: list[Task] = list()
169
+ consumers: list[Task] = list()
170
+
171
+ for _ in range(THREADS):
172
+ producers.append(create_task(_producer_int(Q, N, finish=False, wait=0.05)))
173
+ consumers.append(create_task(_consumer_int(Q, 2 * N, wait=0.06)))
174
+ try:
175
+ async with timeout(10):
176
+ await gather(*producers)
177
+ await Q.finish(all=True)
178
+ await Q.join()
179
+ assert not Q.has_wip, "Queue should not have any items WIP"
180
+ except TimeoutError:
181
+ assert False, "IterableQueue.join() took too long"
182
+ assert Q.count == THREADS * N, (
183
+ f"count returned wrong value {Q.count}, should be {THREADS * N}"
184
+ )
185
+ assert Q.qsize() == 0, "queue size is > 0 even it should be empty"
186
+ assert Q.empty(), "queue not empty"
187
+ for p in consumers:
188
+ p.cancel()
189
+
190
+
191
+ @pytest.mark.timeout(10)
192
+ @pytest.mark.asyncio
193
+ async def test_5_empty_join(test_interablequeue_int: IterableQueue[int]):
194
+ """Test for await join when an empty queue is finished"""
195
+ Q = test_interablequeue_int
196
+ producer: Task = create_task(_producer_int(Q, n=0, finish=True, wait=2))
197
+ assert not Q.is_done, "is_done returned True even queue is not finished"
198
+ consumer: Task = create_task(_consumer_int(Q))
199
+ try:
200
+ async with timeout(3):
201
+ await Q.join()
202
+ assert Q.empty(), (
203
+ "Queue is done after 3 secs and the join() should finish before timeout(5)"
204
+ )
205
+ except TimeoutError:
206
+ assert False, "await IterableQueue.join() failed with an empty queue finished"
207
+ await sleep(0.1)
208
+
209
+ try:
210
+ consumer.cancel()
211
+ assert not consumer.cancelled(), (
212
+ "consumer task was cancelled and did not complete even it should have"
213
+ )
214
+ except Exception as err:
215
+ assert False, f"Unknown Exception caught: {err}"
216
+ assert producer.done(), "producer has not finished"
217
+
218
+
219
+ @pytest.mark.timeout(10)
220
+ @pytest.mark.asyncio
221
+ async def test_6_finish_full_queue(test_interablequeue_int: IterableQueue[int]):
222
+ """Test for await join when an empty queue is finished"""
223
+ Q = test_interablequeue_int
224
+ producer: Task = create_task(_producer_int(Q, n=QSIZE * 2))
225
+ try:
226
+ await sleep(0.5)
227
+ async with timeout(3):
228
+ await Q.finish(all=True, empty=True)
229
+ assert Q.empty(), (
230
+ f"Queue should be empty: qsize={Q._Q.qsize()}: {Q._Q.get_nowait()}, {Q._Q.get_nowait()}"
231
+ )
232
+ assert Q.is_done, "Queue is not done"
233
+ except TimeoutError:
234
+ assert False, "await IterableQueue.join() failed with an empty queue finished"
235
+ await sleep(0.1)
236
+ assert Q.is_done, "Queue is not done"
237
+ producer.cancel()
238
+
239
+
240
+ @pytest.mark.timeout(10)
241
+ @pytest.mark.asyncio
242
+ async def test_7_aiter(test_interablequeue_int: IterableQueue[int]):
243
+ """Test for await join when an empty queue is finished"""
244
+ Q = test_interablequeue_int
245
+ await _producer_int(Q, n=QSIZE - 1, finish=True)
246
+
247
+ try:
248
+ await sleep(0.5)
249
+ async for i in Q:
250
+ assert i >= 0, "Did not receive an int"
251
+ assert Q.is_done, "Queue is not done"
252
+ # assert (
253
+ # True
254
+ # ), "Queue is done after 3 secs and the join() should finish before timeout(5)"
255
+ except TimeoutError:
256
+ assert False, "await IterableQueue.join() failed with an empty queue finished"
257
+
258
+
259
+ @pytest.mark.timeout(10)
260
+ @pytest.mark.asyncio
261
+ async def test_8_aiter_1_item(test_interablequeue_int: IterableQueue[int]):
262
+ """Test for await join when an empty queue is finished"""
263
+ Q = test_interablequeue_int
264
+ await _producer_int(Q, n=1, finish=True)
265
+
266
+ try:
267
+ assert Q.qsize() == 1, f"incorrect queue length {Q.qsize()} != 1"
268
+ await sleep(0.5)
269
+ count: int = 0
270
+ async for i in Q:
271
+ count += 1
272
+ assert i >= 0, "Did not receive an int"
273
+ assert count == 1, f"Did not receive correct number of elements {count} != 1"
274
+ assert True, (
275
+ "Queue is done after 3 secs and the join() should finish before timeout(5)"
276
+ )
277
+ except TimeoutError:
278
+ assert False, "await IterableQueue.join() failed with an empty queue finished"
279
+
280
+
281
+ @pytest.mark.parametrize(
282
+ "cats,N, producers",
283
+ [
284
+ ([randomword(5) for _ in range(10)], 1000, 1),
285
+ ([randomword(5) for _ in range(20)], 10000, 1),
286
+ ([randomword(5) for _ in range(5)], 1000, 3),
287
+ ],
288
+ )
289
+ @pytest.mark.timeout(10)
290
+ @pytest.mark.asyncio
291
+ async def test_9_category_counter_queue(
292
+ cats: list[str], N: int, producers: int
293
+ ) -> None:
294
+ """Test CategoryCounterQueue"""
295
+ Q = CategoryCounterQueue(maxsize=QSIZE)
296
+
297
+ async def producer(
298
+ Q: CategoryCounterQueue, cats: list[str], N: int = 100
299
+ ) -> defaultdict[str, int]:
300
+ """
301
+ Test Producer for CategoryCounterQueue
302
+ """
303
+ _counter: defaultdict[str, int] = defaultdict(int)
304
+ await Q.add_producer()
305
+ for _ in range(N):
306
+ cat: str = choice(cats)
307
+ count: int = randint(1, 10)
308
+ await Q.send(cat, count)
309
+ _counter[cat] += count
310
+ await Q.finish()
311
+ return _counter
312
+
313
+ senders: list[Task] = list()
314
+
315
+ for _ in range(producers):
316
+ senders.append(create_task(producer(Q, cats, N)))
317
+
318
+ try:
319
+ res_in: defaultdict[str, int] = await Q.listen()
320
+ res_out: defaultdict[str, int] = defaultdict(int)
321
+ for res in await gather(*senders):
322
+ for cat, count in res.items():
323
+ res_out[cat] += count
324
+
325
+ assert res_in == res_out, f"CategoryCounterQueue: {res_in} != {res_out}"
326
+ assert Q.qsize() == 0, "queue size is > 0 even it should be empty"
327
+ assert Q.empty(), "queue not empty"
328
+ assert Q.count == N * producers, (
329
+ f"count returned wrong value {Q.count}, should be {N * producers}"
330
+ )
331
+
332
+ except TimeoutError:
333
+ assert False, "await IterableQueue.join() failed with an empty queue finished"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes