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.
- queutils-0.9.1/.github/workflows/codeql.yml +100 -0
- {queutils-0.8.4 → queutils-0.9.1}/.github/workflows/python-package.yml +1 -1
- {queutils-0.8.4 → queutils-0.9.1}/PKG-INFO +4 -2
- {queutils-0.8.4 → queutils-0.9.1}/pyproject.toml +7 -3
- {queutils-0.8.4 → queutils-0.9.1}/src/queutils/__init__.py +5 -0
- {queutils-0.8.4 → queutils-0.9.1}/src/queutils/asyncqueue.py +3 -5
- queutils-0.9.1/src/queutils/categorycounterqueue.py +116 -0
- {queutils-0.8.4 → queutils-0.9.1}/src/queutils/iterablequeue.py +14 -13
- queutils-0.9.1/tests/test_ccategorycounterqueue.py +333 -0
- {queutils-0.8.4 → queutils-0.9.1}/.github/workflows/dependency-review.yml +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/.github/workflows/python-publish.yml +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/.gitignore +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/LICENSE +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/README.md +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/codecov.yml +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/demos/asyncqueue_demo.py +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/demos/filequeue_demo.py +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/demos/iterablequeue_demo.py +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/docs/asyncqueue.md +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/docs/filequeue.md +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/docs/iterablequeue.md +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/src/queutils/countable.py +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/src/queutils/filequeue.py +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/src/queutils/py.typed +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/tests/test_asyncqueue.py +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/tests/test_demos.py +0 -0
- {queutils-0.8.4 → queutils-0.9.1}/tests/test_filequeue.py +0 -0
- {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}}"
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: queutils
|
3
|
-
Version: 0.
|
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
|
[](https://github.com/Jylpah/queutils/actions/workflows/python-package.yml) [](https://codecov.io/gh/Jylpah/queutils)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "queutils"
|
3
|
-
version = "0.
|
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"
|
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.
|
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
|
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
|
File without changes
|