queutils 0.8.1__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from .countable import Countable as Countable
2
+ from .asyncqueue import AsyncQueue as AsyncQueue
3
+ from .iterablequeue import IterableQueue as IterableQueue, QueueDone as QueueDone
4
+ from .filequeue import FileQueue as FileQueue
5
+
6
+ __all__ = [
7
+ "asyncqueue",
8
+ "countable",
9
+ "filequeue",
10
+ "iterablequeue",
11
+ ]
queutils/asyncqueue.py ADDED
@@ -0,0 +1,112 @@
1
+ # -----------------------------------------------------------
2
+ # Class AsyncQueue(asyncio.Queue, Generic[T])
3
+ #
4
+ # asyncio wrapper for non-async queue.Queue. Can be used to create
5
+ # an asyncio.Queue out of a (non-async) multiprocessing.Queue.
6
+ #
7
+ # -----------------------------------------------------------
8
+
9
+ __author__ = "Jylpah"
10
+ __copyright__ = "Copyright 2024, Jylpah <Jylpah@gmail.com>"
11
+ __credits__ = ["Jylpah"]
12
+ __license__ = "MIT"
13
+ # __version__ = "1.0"
14
+ __maintainer__ = "Jylpah"
15
+ __email__ = "Jylpah@gmail.com"
16
+ __status__ = "Production"
17
+
18
+
19
+ from queue import Full, Empty, Queue
20
+ from asyncio.queues import QueueEmpty, QueueFull
21
+ import asyncio
22
+ from typing import Generic, TypeVar
23
+ import logging
24
+ from asyncio import sleep
25
+
26
+ T = TypeVar("T")
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ debug = logger.debug
31
+ message = logger.warning
32
+ verbose = logger.info
33
+ error = logger.error
34
+
35
+
36
+ class AsyncQueue(asyncio.Queue, Generic[T]):
37
+ """
38
+ Async wrapper for non-async queue.Queue. Can be used to create
39
+ an asyncio.Queue out of a (non-async) multiprocessing.Queue.
40
+ """
41
+
42
+ def __init__(self, queue: Queue[T], asleep: float = 0.01):
43
+ self._Q: Queue[T] = queue
44
+ # self._maxsize: int = queue.maxsize
45
+ self._done: int = 0
46
+ self._items: int = 0
47
+ self._sleep: float = asleep
48
+
49
+
50
+ @property
51
+ def maxsize(self) -> int:
52
+ """not supported by queue.Queue"""
53
+ return self._Q.maxsize
54
+
55
+ async def get(self) -> T:
56
+ while True:
57
+ try:
58
+ return self.get_nowait()
59
+ except QueueEmpty:
60
+ await sleep(self._sleep)
61
+
62
+ def get_nowait(self) -> T:
63
+ try:
64
+ return self._Q.get_nowait()
65
+ except Empty:
66
+ raise QueueEmpty
67
+
68
+ async def put(self, item: T) -> None:
69
+ while True:
70
+ try:
71
+ return self.put_nowait(item)
72
+ except QueueFull:
73
+ await sleep(self._sleep)
74
+
75
+ def put_nowait(self, item: T) -> None:
76
+ try:
77
+ self._Q.put_nowait(item)
78
+ self._items += 1
79
+ return None
80
+ except Full:
81
+ raise QueueFull
82
+
83
+ async def join(self) -> None:
84
+ while True:
85
+ if self._Q.empty():
86
+ return None
87
+ else:
88
+ await sleep(self._sleep)
89
+
90
+ def task_done(self) -> None:
91
+ self._Q.task_done()
92
+ self._done += 1
93
+ return None
94
+
95
+ def qsize(self) -> int:
96
+ return self._Q.qsize()
97
+
98
+ @property
99
+ def done(self) -> int:
100
+ """Number of done items"""
101
+ return self._done
102
+
103
+ @property
104
+ def items(self) -> int:
105
+ """Number of items ever put to the queue"""
106
+ return self._items
107
+
108
+ def empty(self) -> bool:
109
+ return self._Q.empty()
110
+
111
+ def full(self) -> bool:
112
+ return self._Q.full()
queutils/countable.py ADDED
@@ -0,0 +1,11 @@
1
+
2
+
3
+ from typing import Protocol
4
+
5
+ class Countable(Protocol):
6
+ """"
7
+ Protocol interface for count @property
8
+ """
9
+ @property
10
+ def count(self) -> int:
11
+ ...
queutils/filequeue.py ADDED
@@ -0,0 +1,156 @@
1
+ # -----------------------------------------------------------
2
+ # Class FileQueue(asyncio.Queue)
3
+ #
4
+ # Class to build asyncio.Queue of filename (pathlib.Path) based on arguments given.
5
+ # - Inherits from queutils.IterableQueue
6
+ # - Supports include/exclude filters
7
+ #
8
+ # -----------------------------------------------------------
9
+
10
+ __author__ = "Jylpah"
11
+ __copyright__ = "Copyright 2024, Jylpah <Jylpah@gmail.com>"
12
+ __credits__ = ["Jylpah"]
13
+ __license__ = "MIT"
14
+ # __version__ = "1.0"
15
+ __maintainer__ = "Jylpah"
16
+ __email__ = "Jylpah@gmail.com"
17
+ __status__ = "Production"
18
+
19
+
20
+ import logging
21
+
22
+ # from asyncio import Queue
23
+ import aioconsole # type: ignore
24
+ from fnmatch import fnmatch, fnmatchcase
25
+ from pathlib import Path
26
+ from typing import Optional, Sequence
27
+
28
+ from .iterablequeue import IterableQueue
29
+
30
+ logger = logging.getLogger(__name__)
31
+ error = logger.error
32
+ message = logger.warning
33
+ verbose = logger.info
34
+ debug = logger.debug
35
+
36
+
37
+ def str2path(filename: str | Path, suffix: str | None = None) -> Path:
38
+ """
39
+ Convert filename (str) to pathlib.Path
40
+ """
41
+ if isinstance(filename, str):
42
+ filename = Path(filename)
43
+ if suffix is not None and not filename.name.lower().endswith(suffix):
44
+ filename = filename.with_suffix(suffix)
45
+ return filename
46
+
47
+
48
+ class FileQueue(IterableQueue[Path]):
49
+ """
50
+ Class to create a IterableQueue(asyncio.Queue) of filenames based on
51
+ given directories and files as arguments.
52
+ Supports include/exclude filters based on filenames.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ base: Optional[Path] = None,
58
+ filter: str = "*",
59
+ exclude: bool = False,
60
+ case_sensitive: bool = True,
61
+ follow_symlinks: bool = False,
62
+ **kwargs,
63
+ ):
64
+ assert base is None or isinstance(base, Path), "base has to be Path or None"
65
+ assert isinstance(filter, str), "filter has to be string"
66
+ assert isinstance(case_sensitive, bool), "case_sensitive has to be bool"
67
+ assert isinstance(follow_symlinks, bool), "follow_symlinks has to be bool"
68
+
69
+ # debug(f"maxsize={str(maxsize)}, filter='{filter}'")
70
+ super().__init__(count_items=True, **kwargs)
71
+ self._base: Optional[Path] = base
72
+ # self._done: bool = False
73
+ self._case_sensitive: bool = False
74
+ self._exclude: bool = False
75
+ self._follow_symlinks : bool = follow_symlinks
76
+ self.set_filter(filter=filter, exclude=exclude, case_sensitive=case_sensitive)
77
+
78
+ def set_filter(
79
+ self,
80
+ filter: str = "*",
81
+ exclude: bool = False,
82
+ case_sensitive: bool = False,
83
+ ):
84
+ """set filtering logic. Only set (!= None) params are changed"""
85
+ assert isinstance(case_sensitive, bool), "case_sensitive must be type of bool"
86
+ assert isinstance(exclude, bool), "exclude must be type of bool"
87
+
88
+ self._case_sensitive = case_sensitive
89
+ self._exclude = exclude
90
+ self._filter = filter
91
+ debug(
92
+ "filter=%s exclude=%s, case_sensitive=%s",
93
+ str(self._filter),
94
+ self._exclude,
95
+ self._case_sensitive,
96
+ )
97
+
98
+ async def mk_queue(self, files: Sequence[str | Path]) -> bool:
99
+ """Create file queue from arguments given
100
+ '-' denotes for STDIN
101
+ """
102
+ assert files is not None and len(files) > 0, "No files given to process"
103
+ await self.add_producer()
104
+ path: Path
105
+ file: str | Path
106
+ try:
107
+ if isinstance(files[0], str) and files[0] == "-":
108
+ stdin, _ = await aioconsole.get_standard_streams()
109
+ while (line := await stdin.readline()) is not None:
110
+ path = Path(line.decode("utf-8").removesuffix("\n"))
111
+ if self._base is not None:
112
+ path = self._base / path
113
+ await self.put(path)
114
+ else:
115
+ for file in files:
116
+ path = str2path(file)
117
+ if self._base is not None:
118
+ path = self._base / path
119
+ await self.put(path)
120
+ except Exception as err:
121
+ error(f"{err}")
122
+ return await self.finish()
123
+
124
+
125
+ async def put(self, path: Path) -> None:
126
+ """Recursive function to build process queue. Sanitize filename"""
127
+ assert isinstance(path, Path), "path has to be type Path()"
128
+ try:
129
+ if path.is_symlink() and not self._follow_symlinks:
130
+ return None
131
+ if path.is_dir():
132
+ for child in path.iterdir():
133
+ await self.put(child)
134
+ elif path.is_file() and self.match(path):
135
+ debug("Adding file to queue: %s", str(path))
136
+ await super().put(path)
137
+ except Exception as err:
138
+ error(f"{err}")
139
+ return None
140
+
141
+ def match(self, path: Path) -> bool:
142
+ """ "Match file name with filter
143
+
144
+ https://docs.python.org/3/library/fnmatch.html
145
+ """
146
+ assert isinstance(path, Path), "path has to be type Path()"
147
+ try:
148
+ m: bool
149
+ if self._case_sensitive:
150
+ m = fnmatch(path.name, self._filter)
151
+ else:
152
+ m = fnmatchcase(path.name, self._filter)
153
+ return m != self._exclude
154
+ except Exception as err:
155
+ error(f"{err}")
156
+ return False
@@ -0,0 +1,283 @@
1
+ # -----------------------------------------------------------
2
+ # Class IterableQueue(asyncio.Queue[T], AsyncIterable[T], Countable)
3
+ #
4
+ # IterableQueue is asyncio.Queue subclass that can be iterated asynchronusly.
5
+ # IterableQueue terminates automatically when the queue has been
6
+ # filled and emptied.
7
+ #
8
+ # -----------------------------------------------------------
9
+
10
+ __author__ = "Jylpah"
11
+ __copyright__ = "Copyright 2024, Jylpah <Jylpah@gmail.com>"
12
+ __credits__ = ["Jylpah"]
13
+ __license__ = "MIT"
14
+ # __version__ = "1.0"
15
+ __maintainer__ = "Jylpah"
16
+ __email__ = "Jylpah@gmail.com"
17
+ __status__ = "Production"
18
+
19
+
20
+ from asyncio import Queue, QueueFull, QueueEmpty, Event, Lock
21
+ from typing import AsyncIterable, TypeVar, Optional
22
+ from .countable import Countable
23
+ import logging
24
+
25
+ # Setup logging
26
+ logger = logging.getLogger()
27
+ error = logger.error
28
+ message = logger.warning
29
+ verbose = logger.info
30
+ debug = logger.debug
31
+
32
+ T = TypeVar("T")
33
+
34
+ class QueueDone(Exception):
35
+ """
36
+ Exception to mark an IterableQueue as filled and emptied i.e. done
37
+ """
38
+ pass
39
+
40
+
41
+ class IterableQueue(Queue[T], AsyncIterable[T], Countable):
42
+ """
43
+ IterableQueue is asyncio.Queue subclass that can be iterated asynchronusly.
44
+
45
+ IterableQueue terminates automatically when the queue has been
46
+ filled and emptied. Supports:
47
+ - asyncio.Queue() interface, _nowait() methods are experimental
48
+ - AsyncIterable(): async for item in queue:
49
+ - Automatic termination of the consumers when the queue has been emptied with QueueDone exception
50
+ - Producers must be registered with add_producer() and they must notify the queue
51
+ with finish() once they have finished adding items
52
+ - Countable interface to count number of items task_done() through 'count' property
53
+ - Countable property can be disabled with count_items=False. This is useful when you
54
+ want to sum the count of multiple IterableQueues
55
+ """
56
+
57
+ def __init__(self, count_items: bool = True, **kwargs):
58
+ # _Q is required instead of inheriting from Queue()
59
+ # using super() since Queue is Optional[T], not [T]
60
+ self._Q: Queue[Optional[T]] = Queue(**kwargs)
61
+ self._producers: int = 0
62
+ self._count_items: bool = count_items
63
+ self._count: int = 0
64
+ self._wip: int = 0
65
+
66
+ self._modify: Lock = Lock()
67
+ self._put_lock: Lock = Lock()
68
+
69
+ self._filled: Event = Event()
70
+ self._empty: Event = Event()
71
+ self._done: Event = Event()
72
+
73
+ self._empty.set()
74
+
75
+ @property
76
+ def is_filled(self) -> bool:
77
+ """"
78
+ True if all the producers finished filling the queue.
79
+ New items cannot be added to a filled queue nor can new producers can be added.
80
+ """
81
+ return self._filled.is_set()
82
+
83
+ @property
84
+ def is_done(self) -> bool:
85
+ """
86
+ Has the queue been filled, emptied and all the items have been marked with task_done()
87
+ """
88
+ return self.is_filled and self.empty() and not self.has_wip
89
+
90
+ @property
91
+ def maxsize(self) -> int:
92
+ return self._Q.maxsize
93
+
94
+
95
+ def full(self) -> bool:
96
+ """
97
+ True if the queue is full
98
+ """
99
+ return self._Q.full()
100
+
101
+ def check_done(self) -> bool:
102
+ if self.is_filled and self.empty() and not self.has_wip:
103
+ self._done.set()
104
+ return True
105
+ return False
106
+
107
+ def empty(self) -> bool:
108
+ """
109
+ Queue has no items except None as a sentinel
110
+ """
111
+ return self._empty.is_set() or self.qsize() == 0
112
+
113
+ def qsize(self) -> int:
114
+ """
115
+ asyncio.Queue.qsize()
116
+ """
117
+ if self.is_filled:
118
+ return self._Q.qsize() - 1
119
+ else:
120
+ return self._Q.qsize()
121
+
122
+ @property
123
+ def wip(self) -> int:
124
+ """
125
+ Number of items in progress i.e. items that have been
126
+ read from the queue, but not marked with task_done()
127
+ """
128
+ return self._wip
129
+
130
+ @property
131
+ def has_wip(self) -> bool:
132
+ """
133
+ True if queue has items in progress
134
+ """
135
+ return self._wip > 0
136
+
137
+ @property
138
+ def count(self) -> int:
139
+ if self._count_items:
140
+ return self._count
141
+ else:
142
+ return 0
143
+
144
+ async def add_producer(self, N: int = 1) -> int:
145
+ """
146
+ Add producer(s) to the queue
147
+ """
148
+ assert N > 0, "N has to be positive"
149
+ async with self._modify:
150
+ if self.is_filled:
151
+ raise QueueDone
152
+ self._producers += N
153
+ return self._producers
154
+
155
+ async def finish(self, all: bool = False, empty: bool = False) -> bool:
156
+ """
157
+ Producer has finished adding items to the queue.
158
+ Once the last producers has finished, the queue is_filled.
159
+ - all: finish() queue for all producers at once
160
+ """
161
+ async with self._modify:
162
+ if self._producers <= 0 or self.is_filled:
163
+ # raise ValueError("finish() called more than the is producers")
164
+ self._producers = 0
165
+ return False
166
+ self._producers -= 1
167
+
168
+ if all or self._producers <= 0:
169
+ self._filled.set()
170
+ self._producers = 0
171
+
172
+ if self._producers == 0:
173
+ if empty:
174
+ try:
175
+ while True:
176
+ _ = self.get_nowait()
177
+ self.task_done()
178
+ except (QueueDone, QueueEmpty):
179
+ pass
180
+
181
+ async with self._put_lock:
182
+ if empty:
183
+ try:
184
+ _ = self.get_nowait()
185
+ self.task_done()
186
+ except (QueueDone, QueueEmpty):
187
+ pass
188
+ self.check_done()
189
+ await self._Q.put(None)
190
+ return True
191
+ return False
192
+
193
+ async def put(self, item: T) -> None:
194
+ async with self._put_lock:
195
+ if self.is_filled: # should this be inside put_lock?
196
+ raise QueueDone
197
+ if self._producers <= 0:
198
+ raise ValueError("No registered producers")
199
+ elif item is None:
200
+ raise ValueError("Cannot add None to IterableQueue")
201
+ await self._Q.put(item=item)
202
+ self._empty.clear()
203
+ return None
204
+
205
+ def put_nowait(self, item: T) -> None:
206
+ """
207
+ Experimental asyncio.Queue.put_nowait() implementation
208
+ """
209
+ # raise NotImplementedError
210
+ if self.is_filled:
211
+ raise QueueDone
212
+ if self._producers <= 0:
213
+ raise ValueError("No registered producers")
214
+ elif item is None:
215
+ raise ValueError("Cannot add None to IterableQueue")
216
+ self._Q.put_nowait(item=item)
217
+ self._empty.clear()
218
+ return None
219
+
220
+ async def get(self) -> T:
221
+ item = await self._Q.get()
222
+ if item is None:
223
+ self._empty.set()
224
+ self._Q.task_done()
225
+ self.check_done()
226
+ async with self._put_lock:
227
+ await self._Q.put(None)
228
+ raise QueueDone
229
+ else:
230
+ if self._Q.qsize() == 0:
231
+ self._empty.set()
232
+ async with self._modify:
233
+ self._wip += 1
234
+ return item
235
+
236
+ def get_nowait(self) -> T:
237
+ """
238
+ Experimental asyncio.Queue.get_nowait() implementation
239
+ """
240
+ item: T | None = self._Q.get_nowait()
241
+ if item is None:
242
+ self._empty.set()
243
+ self._Q.task_done()
244
+ self.check_done()
245
+ try:
246
+ self._Q.put_nowait(None)
247
+ except QueueFull:
248
+ pass
249
+ raise QueueDone
250
+ else:
251
+ if self._Q.qsize() == 0:
252
+ self._empty.set()
253
+ self._wip += 1
254
+ return item
255
+
256
+ def task_done(self) -> None:
257
+ self._Q.task_done()
258
+ self._count += 1
259
+ self._wip -= 1
260
+ if self._wip < 0:
261
+ raise ValueError("task_done() called more than tasks open")
262
+ self.check_done()
263
+
264
+ async def join(self) -> None:
265
+ debug("Waiting queue to be filled")
266
+ await self._filled.wait()
267
+ debug("Queue filled, waiting when queue is done")
268
+ await self._done.wait()
269
+ debug("queue is done")
270
+ return None
271
+
272
+ def __aiter__(self):
273
+ return self
274
+
275
+ async def __anext__(self) -> T:
276
+ async with self._modify:
277
+ if self._wip > 0: # do not mark task_done() at first call
278
+ self.task_done()
279
+ try:
280
+ item = await self.get()
281
+ return item
282
+ except QueueDone:
283
+ raise StopAsyncIteration
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.3
2
+ Name: queutils
3
+ Version: 0.8.1
4
+ Summary: Handy Python Queue utilies
5
+ Project-URL: Homepage, https://github.com/Jylpah/queutils
6
+ Project-URL: Bug Tracker, https://github.com/Jylpah/queutils/issues
7
+ Author-email: Jylpah <jylpah@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: aioconsole>=0.6
17
+ Provides-Extra: dev
18
+ Requires-Dist: build>=0.10; extra == 'dev'
19
+ Requires-Dist: hatchling>=1.22.4; extra == 'dev'
20
+ Requires-Dist: mypy>=1.8; extra == 'dev'
21
+ Requires-Dist: pip-chill>=1.0; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest-cov>=4.1; extra == 'dev'
24
+ Requires-Dist: pytest-datafiles>=3.0; extra == 'dev'
25
+ Requires-Dist: pytest-timeout>=2.2; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.1.9; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ [![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)
31
+
32
+ # Queutils
33
+
34
+ Queutils *[Queue Utils]* is a package if handy Python queue classes:
35
+ - **[AsyncQueue](docs/asyncqueue.md)** - `async` wrapper for `queue.Queue`
36
+ - **[IterableQueue](docs/iterablequeue.md)** - `AsyncIterable` queue
37
+ - **[FileQueue](docs/filequeue.md)** - builds a queue of filenames from input
38
+
39
+
40
+ # AsyncQueue
41
+
42
+ [`AsyncQueue`](docs/asyncqueue.md) is a async wrapper for non-async `queue.Queue`. It can be used to create
43
+ an `asyncio.Queue` compatible out of a (non-async) `multiprocessing.Queue`. This is handy to have `async` code running in `multiprocessing` processes and yet be able to communicate with the parent via (non-async) managed `multiprocessing.Queue` queue.
44
+
45
+ ## Features
46
+
47
+ - `asyncio.Queue` compatible
48
+ - `queue.Queue` support
49
+ - `multiprocessing.Queue` support
50
+
51
+
52
+ # IterableQueue
53
+
54
+ [`IterableQueue`](docs/iterablequeue.md) is an `asyncio.Queue` subclass that is `AsyncIterable[T]` i.e. it can be
55
+ iterated in `async for` loop. `IterableQueue` terminates automatically when the queue has been filled and emptied.
56
+
57
+ ## Features
58
+
59
+ - `asyncio.Queue` interface, `_nowait()` methods are experimental
60
+ - `AsyncIterable` support: `async for item in queue:`
61
+ - Automatic termination of the consumers with `QueueDone` exception when the queue has been emptied
62
+ - Producers must be registered with `add_producer()` and they must notify the queue
63
+ with `finish()` once they have finished adding items
64
+ - Countable interface to count number of items task_done() through `count` property
65
+ - Countable property can be disabled with count_items=False. This is useful when you
66
+ want to sum the count of multiple IterableQueues
67
+
68
+ # FileQueue
69
+
70
+ [`FileQueue`](docs/filequeue.md) builds a queue (`IterableQueue[pathlib.Path]`) of the matching files found based on search parameters given. It can search both list of files or directories or mixed. Async method `FileQueue.mk_queue()` searches subdirectories of given directories.
71
+
72
+ ## Features
73
+
74
+ - Input can be given both as `str` and `pathlib.Path`
75
+ - `exclude: bool` exclusive or inclusive filtering. Default is `False`.
76
+ - `case_sensitive: bool` case sensitive filtering (use of `fnmatch` or `fnmatchcase`). Default is `True`.
77
+ - `follow_symlinks: bool` whether to follow symlinks. Default is `False`.
78
+
@@ -0,0 +1,9 @@
1
+ queutils/__init__.py,sha256=MboAom80iQTQURz0liGv_f2ae2Np8YA5ORX1LwsNa50,311
2
+ queutils/asyncqueue.py,sha256=cZPrXJgRCK0iOtE6k-VTzA9kcDDg818h9v0cwYzm2g0,2813
3
+ queutils/countable.py,sha256=YSi7ILf9CuB5Tm3T4UUMEFlveqzqcmomfqJAlLGHEz8,172
4
+ queutils/filequeue.py,sha256=q2ly9H-lSCq6xuOqT1IlWgyCVyLoZiKbc0NuzmkF4aw,5360
5
+ queutils/iterablequeue.py,sha256=hygOtgRRuqaPebcK27Ixm_KOOW0MnlKmBNcYreQoWOA,8677
6
+ queutils-0.8.1.dist-info/METADATA,sha256=IcC2JgXacCtVK-NWXMP5T1ehHqJVWzDMuaBr93tvS-0,3694
7
+ queutils-0.8.1.dist-info/WHEEL,sha256=uNdcs2TADwSd5pVaP0Z_kcjcvvTUklh2S7bxZMF8Uj0,87
8
+ queutils-0.8.1.dist-info/licenses/LICENSE,sha256=J1zeIKU2JVQmhwO2hHQDK8WR6zjVZ-wX8r7ZlL45AbI,1063
9
+ queutils-0.8.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.22.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jylpah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.