csu 2.1.5__tar.gz → 2.2.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.
- {csu-2.1.5 → csu-2.2.0}/.bumpversion.cfg +1 -1
- {csu-2.1.5 → csu-2.2.0}/.cookiecutterrc +1 -1
- {csu-2.1.5 → csu-2.2.0}/PKG-INFO +3 -3
- {csu-2.1.5 → csu-2.2.0}/README.rst +2 -2
- {csu-2.1.5 → csu-2.2.0}/pyproject.toml +1 -1
- {csu-2.1.5 → csu-2.2.0}/src/csu/__init__.py +1 -1
- {csu-2.1.5 → csu-2.2.0}/src/csu/worker/engine.py +46 -36
- {csu-2.1.5 → csu-2.2.0}/src/csu/worker/enums.py +1 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu.egg-info/PKG-INFO +3 -3
- csu-2.2.0/tests/test_exampleworker.py +143 -0
- csu-2.1.5/tests/test_exampleworker.py +0 -104
- {csu-2.1.5 → csu-2.2.0}/.coveragerc +0 -0
- {csu-2.1.5 → csu-2.2.0}/.editorconfig +0 -0
- {csu-2.1.5 → csu-2.2.0}/.github/workflows/github-actions.yml +0 -0
- {csu-2.1.5 → csu-2.2.0}/.pre-commit-config.yaml +0 -0
- {csu-2.1.5 → csu-2.2.0}/.taplo.toml +0 -0
- {csu-2.1.5 → csu-2.2.0}/AUTHORS.rst +0 -0
- {csu-2.1.5 → csu-2.2.0}/CHANGELOG.rst +0 -0
- {csu-2.1.5 → csu-2.2.0}/CONTRIBUTING.rst +0 -0
- {csu-2.1.5 → csu-2.2.0}/LICENSE +0 -0
- {csu-2.1.5 → csu-2.2.0}/MANIFEST.in +0 -0
- {csu-2.1.5 → csu-2.2.0}/ci/bootstrap.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/ci/requirements.txt +0 -0
- {csu-2.1.5 → csu-2.2.0}/ci/templates/.github/workflows/github-actions.yml +0 -0
- {csu-2.1.5 → csu-2.2.0}/pytest.ini +0 -0
- {csu-2.1.5 → csu-2.2.0}/setup.cfg +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/admin.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/conf.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/consts.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/__init__.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/auth.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/fields.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/forms.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/perms.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/phonenumber.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/serializers.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/test_auth.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/test_fields.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/test_forms.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/test_phonenumber.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/test_serializers.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/drf/views.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/enums.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/env.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/exceptions.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/export.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/fixups.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/forms/__init__.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/forms/crispy.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/forms/fields.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/gettext.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/gettext_lazy.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/locale/ro/LC_MESSAGES/django.mo +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/locale/ro/LC_MESSAGES/django.po +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/logging.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/management.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/models.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/query.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/routers.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/service.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/templates/api_exception.html +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/templates/forms/widgets/opaquewidget.html +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/test_consts.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/test_env.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/test_exceptions.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/test_logging.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/test_management.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/test_service.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/test_timezones.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/test_utils.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/test_xml.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/timezones.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/utils.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/views.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/worker/__init__.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/worker/admin.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/worker/asgi.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/worker/job.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/worker/models.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/worker/registry.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/wsgi.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu/xml.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu.egg-info/SOURCES.txt +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu.egg-info/dependency_links.txt +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu.egg-info/requires.txt +0 -0
- {csu-2.1.5 → csu-2.2.0}/src/csu.egg-info/top_level.txt +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_custom_context.yaml +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_custom_context_0_retries.yaml +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_decoding.yaml +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_error.yaml +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_logging.yaml +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_redirects.yaml +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service_auth/test_async.yaml +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service_auth/test_sync.yaml +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/conftest.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/exampleworker.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/test_models.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/test_service.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/test_service_auth.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/test_views.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/test_worker.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/testproject/__init__.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/testproject/fixtures/testuser.json +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/testproject/models.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/testproject/settings.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tests/testproject/urls.py +0 -0
- {csu-2.1.5 → csu-2.2.0}/tox.ini +0 -0
{csu-2.1.5 → csu-2.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csu
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Clean Slate Utils - bunch of utility code, mostly Django/DRF specific.
|
|
5
5
|
Author-email: Ionel Cristian Mărieș <contact@ionelmc.ro>
|
|
6
6
|
License-Expression: BSD-2-Clause
|
|
@@ -77,9 +77,9 @@ Overview
|
|
|
77
77
|
.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/csu.svg
|
|
78
78
|
:alt: Supported implementations
|
|
79
79
|
:target: https://pypi.org/project/csu
|
|
80
|
-
.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.
|
|
80
|
+
.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.0.svg
|
|
81
81
|
:alt: Commits since latest release
|
|
82
|
-
:target: https://github.com/ionelmc/python-csu/compare/v2.
|
|
82
|
+
:target: https://github.com/ionelmc/python-csu/compare/v2.2.0...main
|
|
83
83
|
.. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/ionelmc/python-csu/main.svg
|
|
84
84
|
:alt: Scrutinizer Status
|
|
85
85
|
:target: https://scrutinizer-ci.com/g/ionelmc/python-csu/
|
|
@@ -34,9 +34,9 @@ Overview
|
|
|
34
34
|
.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/csu.svg
|
|
35
35
|
:alt: Supported implementations
|
|
36
36
|
:target: https://pypi.org/project/csu
|
|
37
|
-
.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.
|
|
37
|
+
.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.0.svg
|
|
38
38
|
:alt: Commits since latest release
|
|
39
|
-
:target: https://github.com/ionelmc/python-csu/compare/v2.
|
|
39
|
+
:target: https://github.com/ionelmc/python-csu/compare/v2.2.0...main
|
|
40
40
|
.. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/ionelmc/python-csu/main.svg
|
|
41
41
|
:alt: Scrutinizer Status
|
|
42
42
|
:target: https://scrutinizer-ci.com/g/ionelmc/python-csu/
|
|
@@ -4,9 +4,9 @@ import traceback
|
|
|
4
4
|
from abc import ABC
|
|
5
5
|
from abc import abstractmethod
|
|
6
6
|
from asyncio import CancelledError
|
|
7
|
-
from asyncio import Event
|
|
8
7
|
from asyncio import Future
|
|
9
8
|
from asyncio import Queue
|
|
9
|
+
from asyncio import Semaphore
|
|
10
10
|
from collections import defaultdict
|
|
11
11
|
from collections.abc import AsyncIterator
|
|
12
12
|
from collections.abc import Iterable
|
|
@@ -18,6 +18,7 @@ from enum import Flag
|
|
|
18
18
|
from enum import auto
|
|
19
19
|
from functools import partial
|
|
20
20
|
from random import randint
|
|
21
|
+
from typing import ClassVar
|
|
21
22
|
from typing import Protocol
|
|
22
23
|
|
|
23
24
|
from datetimerange import DateTimeRange
|
|
@@ -47,17 +48,19 @@ class AbstractWorker(ABC):
|
|
|
47
48
|
state: WorkerState = WorkerState.UNKNOWN
|
|
48
49
|
runner: asyncio.Task | None = None
|
|
49
50
|
shutdown_requested = False
|
|
51
|
+
id_counter: ClassVar = 0
|
|
52
|
+
id = "?"
|
|
50
53
|
|
|
51
54
|
def __str__(self):
|
|
52
|
-
return f"{type(self).__name__}({self.state.name})"
|
|
53
|
-
|
|
54
|
-
def __repr__(self):
|
|
55
|
-
return f"{type(self).__name__}({id(self)}, state={self.state.name})"
|
|
55
|
+
return f"{type(self).__name__}({self.id}/{self.state.name})"
|
|
56
56
|
|
|
57
57
|
def report(self, inspect: defaultdict[str, int]):
|
|
58
58
|
inspect[self.state.name] += 1
|
|
59
59
|
|
|
60
60
|
def start(self):
|
|
61
|
+
cls = type(self)
|
|
62
|
+
cls.id_counter += 1
|
|
63
|
+
self.id = cls.id_counter
|
|
61
64
|
self.runner = asyncio.create_task(self.run())
|
|
62
65
|
# noinspection PyTypeChecker
|
|
63
66
|
self.runner.add_done_callback(self.on_exit)
|
|
@@ -161,9 +164,6 @@ class AbstractProducer(AbstractWorker):
|
|
|
161
164
|
def __init__(self, engine: AbstractEngine):
|
|
162
165
|
self.engine = engine
|
|
163
166
|
|
|
164
|
-
def __str__(self):
|
|
165
|
-
return f"{type(self).__name__}()"
|
|
166
|
-
|
|
167
167
|
async def run(self):
|
|
168
168
|
task: TaskProtocol | None
|
|
169
169
|
logger.info("%s.run() started.", self)
|
|
@@ -211,15 +211,14 @@ class AbstractConsumer(AbstractWorker):
|
|
|
211
211
|
def __init__(self, engine: AbstractEngine):
|
|
212
212
|
self.engine = engine
|
|
213
213
|
|
|
214
|
-
def __str__(self):
|
|
215
|
-
return f"{type(self).__name__}({id(self)})"
|
|
216
|
-
|
|
217
214
|
async def run(self):
|
|
218
215
|
while True:
|
|
219
|
-
if
|
|
216
|
+
if self.shutdown_requested:
|
|
217
|
+
self.state = WorkerState.SHUTDOWN
|
|
218
|
+
else:
|
|
220
219
|
self.state = WorkerState.BEFORE
|
|
221
220
|
await self.before()
|
|
222
|
-
|
|
221
|
+
self.state = WorkerState.READY
|
|
223
222
|
job: Job | None
|
|
224
223
|
async with self.engine.consume() as job:
|
|
225
224
|
if job is None:
|
|
@@ -255,8 +254,9 @@ class AbstractConsumer(AbstractWorker):
|
|
|
255
254
|
|
|
256
255
|
if self.shutdown_requested:
|
|
257
256
|
continue
|
|
258
|
-
|
|
259
|
-
|
|
257
|
+
else:
|
|
258
|
+
self.state = WorkerState.AFTER
|
|
259
|
+
await self.after(idle=idle)
|
|
260
260
|
|
|
261
261
|
@abstractmethod
|
|
262
262
|
async def perform_work(self, job: Job) -> dict:
|
|
@@ -266,15 +266,13 @@ class AbstractConsumer(AbstractWorker):
|
|
|
266
266
|
class AbstractEngine(ABC):
|
|
267
267
|
workers: list[AbstractWorker]
|
|
268
268
|
queue: Queue = None
|
|
269
|
-
|
|
269
|
+
queue_sem: Semaphore
|
|
270
270
|
shutdown_on: Iterable[int] = (signal.SIGINT, signal.SIGTERM)
|
|
271
271
|
shutdown_requested = False
|
|
272
|
-
|
|
272
|
+
shutdown_tasks: ClassVar[dict[bool, asyncio.Task]] = {}
|
|
273
273
|
|
|
274
274
|
def __init__(self):
|
|
275
275
|
self.workers = []
|
|
276
|
-
self.queue_awaited = Event()
|
|
277
|
-
self.queue_maxsize = 0
|
|
278
276
|
|
|
279
277
|
def __str__(self):
|
|
280
278
|
return f"{type(self).__name__}({len(self.workers)}w/{self.queue.qsize() if self.queue else '-'}q)"
|
|
@@ -288,28 +286,27 @@ class AbstractEngine(ABC):
|
|
|
288
286
|
|
|
289
287
|
async def push(self, job: Job | None) -> None:
|
|
290
288
|
logger.info("%s.push(%s)", self, job)
|
|
291
|
-
self.queue_awaited.clear() # clear it here, it's the shortest path to "consumer wakes up and calls .set()"
|
|
292
289
|
await self.queue.put(job)
|
|
293
290
|
|
|
294
291
|
@asynccontextmanager
|
|
295
292
|
async def consume(self) -> AsyncIterator[Job | None]:
|
|
296
|
-
self.
|
|
293
|
+
self.queue_sem.release()
|
|
297
294
|
yield await self.queue.get()
|
|
298
295
|
# if raised then there's no task to mark as done (this won't be reached on exception)
|
|
299
296
|
self.queue.task_done()
|
|
300
297
|
|
|
301
298
|
async def wait_worker_ready(self):
|
|
302
299
|
# event set after consuming, cleared right before there's a task
|
|
303
|
-
await self.
|
|
300
|
+
await self.queue_sem.acquire()
|
|
304
301
|
|
|
305
302
|
def add_worker(self, worker: AbstractWorker):
|
|
306
|
-
assert self.queue is None, f"{self} is already started"
|
|
307
|
-
if isinstance(worker, AbstractConsumer):
|
|
308
|
-
self.queue_maxsize += 1
|
|
303
|
+
assert self.queue is None, f"{self} is already started, {self.queue=}"
|
|
309
304
|
self.workers.append(worker)
|
|
310
305
|
|
|
311
306
|
async def start(self):
|
|
312
|
-
self.queue
|
|
307
|
+
assert self.queue is None, f"{self} is already started, {self.queue=}"
|
|
308
|
+
self.queue = Queue()
|
|
309
|
+
self.queue_sem = Semaphore(value=0)
|
|
313
310
|
for worker in self.workers:
|
|
314
311
|
assert worker.runner is None, f"{worker=} should not be started"
|
|
315
312
|
worker.start()
|
|
@@ -317,8 +314,15 @@ class AbstractEngine(ABC):
|
|
|
317
314
|
signal.signal(sig, self.shutdown_handler)
|
|
318
315
|
|
|
319
316
|
def shutdown_handler(self, sig, frame):
|
|
320
|
-
|
|
321
|
-
|
|
317
|
+
sig_name = signal.Signals(sig).name
|
|
318
|
+
graceful = not self.shutdown_requested
|
|
319
|
+
logger.info("%s.shutdown_handler(%s) %s", self, sig_name, "GRACEFUL" if graceful else "FORCED")
|
|
320
|
+
if not graceful:
|
|
321
|
+
self.shutdown_tasks[True].cancel()
|
|
322
|
+
if graceful in self.shutdown_tasks:
|
|
323
|
+
logger.warn("%s.shutdown_handler(%s) already requested", self, sig_name)
|
|
324
|
+
else:
|
|
325
|
+
self.shutdown_tasks[graceful] = asyncio.create_task(self.stop(graceful=graceful))
|
|
322
326
|
self.shutdown_requested = True
|
|
323
327
|
|
|
324
328
|
async def stop(self, graceful=False):
|
|
@@ -327,16 +331,19 @@ class AbstractEngine(ABC):
|
|
|
327
331
|
for i, worker in enumerate(self.workers, 1):
|
|
328
332
|
logger.info("%s worker %s: %r", self, i, worker)
|
|
329
333
|
if graceful:
|
|
330
|
-
logger.info("%s waiting producers to stop...", self)
|
|
331
|
-
producers = [worker for worker in self.workers if isinstance(worker, AbstractProducer)]
|
|
332
|
-
for producer in producers:
|
|
333
|
-
producer.shutdown_requested = True
|
|
334
|
-
await asyncio.gather(*[producer.runner for producer in producers], return_exceptions=True)
|
|
335
|
-
logger.info("%s requesting all workers to shutdown...", self)
|
|
336
334
|
for worker in self.workers:
|
|
337
335
|
worker.shutdown_requested = True
|
|
336
|
+
|
|
337
|
+
producers = [worker.runner for worker in self.workers if isinstance(worker, AbstractProducer)]
|
|
338
|
+
logger.info("%s requesting workers to stop and awaiting %s producers...", self, len(producers))
|
|
339
|
+
await asyncio.gather(
|
|
340
|
+
*producers,
|
|
341
|
+
return_exceptions=True,
|
|
342
|
+
)
|
|
343
|
+
logger.info("%s flushing queue (%s items) and awaiting consumers...", self, self.queue.qsize())
|
|
344
|
+
for worker in self.workers:
|
|
338
345
|
if isinstance(worker, AbstractConsumer):
|
|
339
|
-
|
|
346
|
+
self.queue.put_nowait(None)
|
|
340
347
|
else:
|
|
341
348
|
while self.queue.qsize() > 0:
|
|
342
349
|
item = self.queue.get_nowait()
|
|
@@ -349,7 +356,10 @@ class AbstractEngine(ABC):
|
|
|
349
356
|
logger.info("%s shutdown complete.", self)
|
|
350
357
|
|
|
351
358
|
def report(self, inspect: defaultdict[str, object]):
|
|
352
|
-
inspect
|
|
359
|
+
inspect.update(
|
|
360
|
+
QSIZE="UNSTARTED" if self.queue is None else self.queue.qsize(),
|
|
361
|
+
SEMVAL=self.queue_sem._value,
|
|
362
|
+
)
|
|
353
363
|
|
|
354
364
|
def inspect(self) -> dict[str, dict[str, int]]:
|
|
355
365
|
result = defaultdict(partial(defaultdict, int))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csu
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Clean Slate Utils - bunch of utility code, mostly Django/DRF specific.
|
|
5
5
|
Author-email: Ionel Cristian Mărieș <contact@ionelmc.ro>
|
|
6
6
|
License-Expression: BSD-2-Clause
|
|
@@ -77,9 +77,9 @@ Overview
|
|
|
77
77
|
.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/csu.svg
|
|
78
78
|
:alt: Supported implementations
|
|
79
79
|
:target: https://pypi.org/project/csu
|
|
80
|
-
.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.
|
|
80
|
+
.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.0.svg
|
|
81
81
|
:alt: Commits since latest release
|
|
82
|
-
:target: https://github.com/ionelmc/python-csu/compare/v2.
|
|
82
|
+
:target: https://github.com/ionelmc/python-csu/compare/v2.2.0...main
|
|
83
83
|
.. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/ionelmc/python-csu/main.svg
|
|
84
84
|
:alt: Scrutinizer Status
|
|
85
85
|
:target: https://scrutinizer-ci.com/g/ionelmc/python-csu/
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import signal
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from process_tests import TestProcess
|
|
6
|
+
from process_tests import dump_on_error
|
|
7
|
+
from process_tests import wait_for_strings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_noskip():
|
|
11
|
+
with TestProcess(sys.executable, "-mexampleworker", "3", "0", "1000", "4") as target, dump_on_error(target.read):
|
|
12
|
+
wait_for_strings(
|
|
13
|
+
target.read,
|
|
14
|
+
5,
|
|
15
|
+
"INFO:worker:ExampleConsumer(1/UNKNOWN) started.",
|
|
16
|
+
"INFO:worker:ExampleConsumer(2/UNKNOWN) started.",
|
|
17
|
+
"INFO:worker:ExampleConsumer(3/UNKNOWN) started.",
|
|
18
|
+
"INFO:worker:ExampleProducer(1/UNKNOWN) started.",
|
|
19
|
+
"INFO:worker:ExampleProducer(1/UNKNOWN).run() started.",
|
|
20
|
+
"PRODUCER fetch_task @ 1 RETURN",
|
|
21
|
+
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
|
|
22
|
+
"INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
|
|
23
|
+
"PRODUCER fetch_task @ 2 RETURN",
|
|
24
|
+
"INFO:worker:ExampleEngine(4w/1q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=2))",
|
|
25
|
+
"INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=2).done of NoOpTask(id=2).",
|
|
26
|
+
"PRODUCER fetch_task @ 3 RETURN",
|
|
27
|
+
"INFO:worker:ExampleEngine(4w/2q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
|
|
28
|
+
"INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
|
|
29
|
+
"INFO:worker:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
|
|
30
|
+
"INFO:worker:ExampleConsumer(1/WORKING).run() - performing...",
|
|
31
|
+
"CONSUMER 0 perform_work JOB 1 START",
|
|
32
|
+
"INFO:worker:ExampleConsumer(2/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=2)",
|
|
33
|
+
"INFO:worker:ExampleConsumer(2/WORKING).run() - performing...",
|
|
34
|
+
"CONSUMER 1 perform_work JOB 2 START",
|
|
35
|
+
"INFO:worker:ExampleConsumer(3/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
|
|
36
|
+
"INFO:worker:ExampleConsumer(3/WORKING).run() - performing...",
|
|
37
|
+
"CONSUMER 2 perform_work JOB 3 START",
|
|
38
|
+
"CONSUMER 0 perform_work JOB 1 DONE",
|
|
39
|
+
"CONSUMER 0 save_result JOB 1",
|
|
40
|
+
"CONSUMER 1 perform_work JOB 2 DONE",
|
|
41
|
+
"CONSUMER 1 save_result JOB 2",
|
|
42
|
+
"CONSUMER 2 perform_work JOB 3 DONE",
|
|
43
|
+
"CONSUMER 2 save_result JOB 3",
|
|
44
|
+
"PRODUCER fetch_task @ 4 RETURN",
|
|
45
|
+
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=4))",
|
|
46
|
+
"INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=4).done of NoOpTask(id=4).",
|
|
47
|
+
"PRODUCER fetch_task @ 5 EXHAUSTED",
|
|
48
|
+
"INFO:worker:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=4)",
|
|
49
|
+
"INFO:worker:ExampleConsumer(1/WORKING).run() - performing...",
|
|
50
|
+
"CONSUMER 0 perform_work JOB 4 START",
|
|
51
|
+
"PRODUCER fetch_task @ 6 EXHAUSTED",
|
|
52
|
+
"CONSUMER 0 perform_work JOB 4 DONE",
|
|
53
|
+
"CONSUMER 0 save_result JOB 4",
|
|
54
|
+
"PRODUCER fetch_task @ 7 EXHAUSTED",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_skip():
|
|
59
|
+
with TestProcess(sys.executable, "-mexampleworker", "3", "2", "100", "4") as target, dump_on_error(target.read):
|
|
60
|
+
wait_for_strings(
|
|
61
|
+
target.read,
|
|
62
|
+
5,
|
|
63
|
+
"INFO:worker:ExampleConsumer(1/UNKNOWN) started.",
|
|
64
|
+
"INFO:worker:ExampleConsumer(2/UNKNOWN) started.",
|
|
65
|
+
"INFO:worker:ExampleConsumer(3/UNKNOWN) started.",
|
|
66
|
+
"INFO:worker:ExampleProducer(1/UNKNOWN) started.",
|
|
67
|
+
"INFO:worker:ExampleProducer(1/UNKNOWN).run() started.",
|
|
68
|
+
"PRODUCER fetch_task @ 1 RETURN",
|
|
69
|
+
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
|
|
70
|
+
"INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
|
|
71
|
+
"PRODUCER fetch_task @ 2 SKIP",
|
|
72
|
+
"INFO:worker:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
|
|
73
|
+
"INFO:worker:ExampleConsumer(1/WORKING).run() - performing...",
|
|
74
|
+
"CONSUMER 0 perform_work JOB 1 START",
|
|
75
|
+
"CONSUMER 0 perform_work JOB 1 DONE",
|
|
76
|
+
"CONSUMER 0 save_result JOB 1",
|
|
77
|
+
"PRODUCER fetch_task @ 3 RETURN",
|
|
78
|
+
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
|
|
79
|
+
"INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
|
|
80
|
+
"PRODUCER fetch_task @ 4 SKIP",
|
|
81
|
+
"INFO:worker:ExampleConsumer(2/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
|
|
82
|
+
"INFO:worker:ExampleConsumer(2/WORKING).run() - performing...",
|
|
83
|
+
"CONSUMER 1 perform_work JOB 3 START",
|
|
84
|
+
"CONSUMER 1 perform_work JOB 3 DONE",
|
|
85
|
+
"CONSUMER 1 save_result JOB 3",
|
|
86
|
+
"PRODUCER fetch_task @ 5 EXHAUSTED",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_graceful_shutdown():
|
|
91
|
+
with TestProcess(sys.executable, "-mexampleworker", "3", "0", "1000", "100") as target, dump_on_error(target.read):
|
|
92
|
+
wait_for_strings(
|
|
93
|
+
target.read,
|
|
94
|
+
15,
|
|
95
|
+
"PRODUCER fetch_task @ 1 RETURN",
|
|
96
|
+
)
|
|
97
|
+
target.proc.send_signal(signal.SIGINT)
|
|
98
|
+
wait_for_strings(
|
|
99
|
+
target.read,
|
|
100
|
+
15,
|
|
101
|
+
"INFO:worker:ExampleEngine(4w/0q).shutdown_handler(SIGINT) GRACEFUL",
|
|
102
|
+
"INFO:worker:ExampleEngine(4w/0q).stop(graceful=True)",
|
|
103
|
+
"INFO:worker:ExampleEngine(4w/0q) stats: {'ExampleConsumer': {'WORKING': 3}, 'ExampleProducer': {'WAITING': 1}, 'ExampleEngine': {'JOBS': 3, 'QSIZE': 0, 'SEMVAL': 0}}",
|
|
104
|
+
"INFO:worker:ExampleEngine(4w/0q) worker 1: <__main__.ExampleConsumer object at ",
|
|
105
|
+
"INFO:worker:ExampleEngine(4w/0q) worker 2: <__main__.ExampleConsumer object at ",
|
|
106
|
+
"INFO:worker:ExampleEngine(4w/0q) worker 3: <__main__.ExampleConsumer object at ",
|
|
107
|
+
"INFO:worker:ExampleEngine(4w/0q) worker 4: <__main__.ExampleProducer object at ",
|
|
108
|
+
"INFO:worker:ExampleEngine(4w/0q) requesting workers to stop and awaiting 1 producers...",
|
|
109
|
+
"CONSUMER 0 perform_work JOB 1 DONE",
|
|
110
|
+
"CONSUMER 0 save_result JOB 1",
|
|
111
|
+
"INFO:worker:ExampleProducer(1/WAITING).on_exit(<Task finished name='Task-5' coro=<AbstractProducer.run() done, defined at /home/ionel/open-source/python-csu/src/csu/worker/engine.py:167> result=None>) exit with result: None",
|
|
112
|
+
"INFO:worker:ExampleEngine(4w/0q) flushing queue (0 items) and awaiting consumers...",
|
|
113
|
+
"INFO:worker:ExampleConsumer(1/SHUTDOWN).on_exit(<Task finished name='Task-2' coro=<AbstractConsumer.run() done, defined at /home/ionel/open-source/python-csu/src/csu/worker/engine.py:214> result=None>) exit with result: None",
|
|
114
|
+
"INFO:worker:ExampleConsumer(2/SHUTDOWN).on_exit(<Task finished name='Task-3' coro=<AbstractConsumer.run() done, defined at /home/ionel/open-source/python-csu/src/csu/worker/engine.py:214> result=None>) exit with result: None",
|
|
115
|
+
"INFO:worker:ExampleConsumer(3/SHUTDOWN).on_exit(<Task finished name='Task-4' coro=<AbstractConsumer.run() done, defined at /home/ionel/open-source/python-csu/src/csu/worker/engine.py:214> result=None>) exit with result: None",
|
|
116
|
+
"INFO:worker:ExampleEngine(0w/0q) shutdown complete.",
|
|
117
|
+
"DONE: {'ExampleEngine':",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_forced_shutdown():
|
|
122
|
+
with TestProcess(sys.executable, "-mexampleworker", "3", "0", "2000", "100") as target, dump_on_error(target.read):
|
|
123
|
+
wait_for_strings(
|
|
124
|
+
target.read,
|
|
125
|
+
15,
|
|
126
|
+
"PRODUCER fetch_task @ 1 RETURN",
|
|
127
|
+
)
|
|
128
|
+
target.proc.send_signal(signal.SIGINT)
|
|
129
|
+
time.sleep(1)
|
|
130
|
+
target.proc.send_signal(signal.SIGTERM)
|
|
131
|
+
wait_for_strings(
|
|
132
|
+
target.read,
|
|
133
|
+
15,
|
|
134
|
+
"INFO:worker:ExampleEngine(4w/0q).shutdown_handler(SIGINT) GRACEFUL",
|
|
135
|
+
"INFO:worker:ExampleEngine(4w/0q).stop(graceful=True)",
|
|
136
|
+
"INFO:worker:ExampleEngine(4w/0q) requesting workers to stop and awaiting 1 producers...",
|
|
137
|
+
"INFO:worker:ExampleEngine(4w/0q).shutdown_handler(SIGTERM) FORCED",
|
|
138
|
+
"CONSUMER 0 perform_work JOB 1 DONE",
|
|
139
|
+
"CONSUMER 1 perform_work JOB 2 DONE",
|
|
140
|
+
"CONSUMER 2 perform_work JOB 3 DONE",
|
|
141
|
+
"DONE: {'ExampleConsumer': {'CANCELED': 3}, 'ExampleProducer': {'CANCELED': 1}, 'ExampleEngine': {'JOBS': 3, 'QSIZE': 0, 'SEMVAL': 0}}",
|
|
142
|
+
"INFO:worker:ExampleEngine(0w/0q) shutdown complete.",
|
|
143
|
+
)
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import signal
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
from process_tests import TestProcess
|
|
5
|
-
from process_tests import dump_on_error
|
|
6
|
-
from process_tests import wait_for_strings
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_noskip():
|
|
10
|
-
with TestProcess(sys.executable, "-mexampleworker", "3", "0", "1000", "4") as target, dump_on_error(target.read):
|
|
11
|
-
wait_for_strings(
|
|
12
|
-
target.read,
|
|
13
|
-
5,
|
|
14
|
-
"PRODUCER fetch_task @ 1 RETURN",
|
|
15
|
-
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
|
|
16
|
-
"INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
|
|
17
|
-
"working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
|
|
18
|
-
"- performing...",
|
|
19
|
-
"CONSUMER 0 perform_work JOB 1 START",
|
|
20
|
-
"CONSUMER 0 perform_work JOB 1 DONE",
|
|
21
|
-
"CONSUMER 0 save_result JOB 1",
|
|
22
|
-
"PRODUCER fetch_task @ 2 RETURN",
|
|
23
|
-
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=2))",
|
|
24
|
-
"INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=2).done of NoOpTask(id=2).",
|
|
25
|
-
").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=2)",
|
|
26
|
-
").run() - performing...",
|
|
27
|
-
"CONSUMER 1 perform_work JOB 2 START",
|
|
28
|
-
"CONSUMER 1 perform_work JOB 2 DONE",
|
|
29
|
-
"CONSUMER 1 save_result JOB 2",
|
|
30
|
-
"PRODUCER fetch_task @ 3 RETURN",
|
|
31
|
-
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
|
|
32
|
-
"INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
|
|
33
|
-
").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
|
|
34
|
-
").run() - performing...",
|
|
35
|
-
"CONSUMER 2 perform_work JOB 3 START",
|
|
36
|
-
"CONSUMER 2 perform_work JOB 3 DONE",
|
|
37
|
-
"CONSUMER 2 save_result JOB 3",
|
|
38
|
-
"PRODUCER fetch_task @ 4 RETURN",
|
|
39
|
-
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=4))",
|
|
40
|
-
"INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=4).done of NoOpTask(id=4).",
|
|
41
|
-
").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=4)",
|
|
42
|
-
").run() - performing...",
|
|
43
|
-
"CONSUMER 0 perform_work JOB 4 START",
|
|
44
|
-
"CONSUMER 0 perform_work JOB 4 DONE",
|
|
45
|
-
"CONSUMER 0 save_result JOB 4",
|
|
46
|
-
"EXHAUSTED",
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_skip():
|
|
51
|
-
with TestProcess(sys.executable, "-mexampleworker", "3", "2", "100", "4") as target, dump_on_error(target.read):
|
|
52
|
-
wait_for_strings(
|
|
53
|
-
target.read,
|
|
54
|
-
5,
|
|
55
|
-
"INFO:worker:ExampleProducer() started.",
|
|
56
|
-
"INFO:worker:ExampleProducer().run() started.",
|
|
57
|
-
"PRODUCER fetch_task @ 1 RETURN",
|
|
58
|
-
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
|
|
59
|
-
"INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
|
|
60
|
-
").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
|
|
61
|
-
").run() - performing...",
|
|
62
|
-
"CONSUMER 0 perform_work JOB 1 START",
|
|
63
|
-
"CONSUMER 0 perform_work JOB 1 DONE",
|
|
64
|
-
"CONSUMER 0 save_result JOB 1",
|
|
65
|
-
"PRODUCER fetch_task @ 2 SKIP",
|
|
66
|
-
"PRODUCER fetch_task @ 3 RETURN",
|
|
67
|
-
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
|
|
68
|
-
"INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
|
|
69
|
-
").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
|
|
70
|
-
").run() - performing...",
|
|
71
|
-
"CONSUMER 1 perform_work JOB 3 START",
|
|
72
|
-
"CONSUMER 1 perform_work JOB 3 DONE",
|
|
73
|
-
"CONSUMER 1 save_result JOB 3",
|
|
74
|
-
"PRODUCER fetch_task @ 4 SKIP",
|
|
75
|
-
"PRODUCER fetch_task @ 5 EXHAUSTED",
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_sigterm():
|
|
80
|
-
with TestProcess(sys.executable, "-mexampleworker", "3", "0", "1000", "100") as target, dump_on_error(target.read):
|
|
81
|
-
wait_for_strings(
|
|
82
|
-
target.read,
|
|
83
|
-
15,
|
|
84
|
-
"PRODUCER fetch_task @ 1 RETURN",
|
|
85
|
-
"INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
|
|
86
|
-
"INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
|
|
87
|
-
"working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
|
|
88
|
-
"- performing...",
|
|
89
|
-
"CONSUMER 0 perform_work JOB 1 START",
|
|
90
|
-
)
|
|
91
|
-
target.proc.send_signal(signal.SIGINT)
|
|
92
|
-
wait_for_strings(
|
|
93
|
-
target.read,
|
|
94
|
-
15,
|
|
95
|
-
"INFO:worker:ExampleEngine(4w/0q).stop(graceful=True)",
|
|
96
|
-
"CONSUMER 0 perform_work JOB 1 DONE",
|
|
97
|
-
"CONSUMER 0 save_result JOB 1",
|
|
98
|
-
"INFO:worker:ExampleEngine(4w/0q) requesting all workers to shutdown...",
|
|
99
|
-
"INFO:worker:ExampleEngine(4w/0q).push(None)",
|
|
100
|
-
"INFO:worker:ExampleEngine(4w/1q).push(None)",
|
|
101
|
-
"INFO:worker:ExampleEngine(4w/2q).push(None)",
|
|
102
|
-
"INFO:worker:ExampleEngine(0w/0q) shutdown complete.",
|
|
103
|
-
"DONE: {'ExampleEngine': {'JOBS': 4, 'QSIZE': 0}}",
|
|
104
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{csu-2.1.5 → csu-2.2.0}/LICENSE
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
{csu-2.1.5 → csu-2.2.0}/tox.ini
RENAMED
|
File without changes
|