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.
Files changed (107) hide show
  1. {csu-2.1.5 → csu-2.2.0}/.bumpversion.cfg +1 -1
  2. {csu-2.1.5 → csu-2.2.0}/.cookiecutterrc +1 -1
  3. {csu-2.1.5 → csu-2.2.0}/PKG-INFO +3 -3
  4. {csu-2.1.5 → csu-2.2.0}/README.rst +2 -2
  5. {csu-2.1.5 → csu-2.2.0}/pyproject.toml +1 -1
  6. {csu-2.1.5 → csu-2.2.0}/src/csu/__init__.py +1 -1
  7. {csu-2.1.5 → csu-2.2.0}/src/csu/worker/engine.py +46 -36
  8. {csu-2.1.5 → csu-2.2.0}/src/csu/worker/enums.py +1 -0
  9. {csu-2.1.5 → csu-2.2.0}/src/csu.egg-info/PKG-INFO +3 -3
  10. csu-2.2.0/tests/test_exampleworker.py +143 -0
  11. csu-2.1.5/tests/test_exampleworker.py +0 -104
  12. {csu-2.1.5 → csu-2.2.0}/.coveragerc +0 -0
  13. {csu-2.1.5 → csu-2.2.0}/.editorconfig +0 -0
  14. {csu-2.1.5 → csu-2.2.0}/.github/workflows/github-actions.yml +0 -0
  15. {csu-2.1.5 → csu-2.2.0}/.pre-commit-config.yaml +0 -0
  16. {csu-2.1.5 → csu-2.2.0}/.taplo.toml +0 -0
  17. {csu-2.1.5 → csu-2.2.0}/AUTHORS.rst +0 -0
  18. {csu-2.1.5 → csu-2.2.0}/CHANGELOG.rst +0 -0
  19. {csu-2.1.5 → csu-2.2.0}/CONTRIBUTING.rst +0 -0
  20. {csu-2.1.5 → csu-2.2.0}/LICENSE +0 -0
  21. {csu-2.1.5 → csu-2.2.0}/MANIFEST.in +0 -0
  22. {csu-2.1.5 → csu-2.2.0}/ci/bootstrap.py +0 -0
  23. {csu-2.1.5 → csu-2.2.0}/ci/requirements.txt +0 -0
  24. {csu-2.1.5 → csu-2.2.0}/ci/templates/.github/workflows/github-actions.yml +0 -0
  25. {csu-2.1.5 → csu-2.2.0}/pytest.ini +0 -0
  26. {csu-2.1.5 → csu-2.2.0}/setup.cfg +0 -0
  27. {csu-2.1.5 → csu-2.2.0}/src/csu/admin.py +0 -0
  28. {csu-2.1.5 → csu-2.2.0}/src/csu/conf.py +0 -0
  29. {csu-2.1.5 → csu-2.2.0}/src/csu/consts.py +0 -0
  30. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/__init__.py +0 -0
  31. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/auth.py +0 -0
  32. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/fields.py +0 -0
  33. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/forms.py +0 -0
  34. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/perms.py +0 -0
  35. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/phonenumber.py +0 -0
  36. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/serializers.py +0 -0
  37. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/test_auth.py +0 -0
  38. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/test_fields.py +0 -0
  39. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/test_forms.py +0 -0
  40. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/test_phonenumber.py +0 -0
  41. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/test_serializers.py +0 -0
  42. {csu-2.1.5 → csu-2.2.0}/src/csu/drf/views.py +0 -0
  43. {csu-2.1.5 → csu-2.2.0}/src/csu/enums.py +0 -0
  44. {csu-2.1.5 → csu-2.2.0}/src/csu/env.py +0 -0
  45. {csu-2.1.5 → csu-2.2.0}/src/csu/exceptions.py +0 -0
  46. {csu-2.1.5 → csu-2.2.0}/src/csu/export.py +0 -0
  47. {csu-2.1.5 → csu-2.2.0}/src/csu/fixups.py +0 -0
  48. {csu-2.1.5 → csu-2.2.0}/src/csu/forms/__init__.py +0 -0
  49. {csu-2.1.5 → csu-2.2.0}/src/csu/forms/crispy.py +0 -0
  50. {csu-2.1.5 → csu-2.2.0}/src/csu/forms/fields.py +0 -0
  51. {csu-2.1.5 → csu-2.2.0}/src/csu/gettext.py +0 -0
  52. {csu-2.1.5 → csu-2.2.0}/src/csu/gettext_lazy.py +0 -0
  53. {csu-2.1.5 → csu-2.2.0}/src/csu/locale/ro/LC_MESSAGES/django.mo +0 -0
  54. {csu-2.1.5 → csu-2.2.0}/src/csu/locale/ro/LC_MESSAGES/django.po +0 -0
  55. {csu-2.1.5 → csu-2.2.0}/src/csu/logging.py +0 -0
  56. {csu-2.1.5 → csu-2.2.0}/src/csu/management.py +0 -0
  57. {csu-2.1.5 → csu-2.2.0}/src/csu/models.py +0 -0
  58. {csu-2.1.5 → csu-2.2.0}/src/csu/query.py +0 -0
  59. {csu-2.1.5 → csu-2.2.0}/src/csu/routers.py +0 -0
  60. {csu-2.1.5 → csu-2.2.0}/src/csu/service.py +0 -0
  61. {csu-2.1.5 → csu-2.2.0}/src/csu/templates/api_exception.html +0 -0
  62. {csu-2.1.5 → csu-2.2.0}/src/csu/templates/forms/widgets/opaquewidget.html +0 -0
  63. {csu-2.1.5 → csu-2.2.0}/src/csu/test_consts.py +0 -0
  64. {csu-2.1.5 → csu-2.2.0}/src/csu/test_env.py +0 -0
  65. {csu-2.1.5 → csu-2.2.0}/src/csu/test_exceptions.py +0 -0
  66. {csu-2.1.5 → csu-2.2.0}/src/csu/test_logging.py +0 -0
  67. {csu-2.1.5 → csu-2.2.0}/src/csu/test_management.py +0 -0
  68. {csu-2.1.5 → csu-2.2.0}/src/csu/test_service.py +0 -0
  69. {csu-2.1.5 → csu-2.2.0}/src/csu/test_timezones.py +0 -0
  70. {csu-2.1.5 → csu-2.2.0}/src/csu/test_utils.py +0 -0
  71. {csu-2.1.5 → csu-2.2.0}/src/csu/test_xml.py +0 -0
  72. {csu-2.1.5 → csu-2.2.0}/src/csu/timezones.py +0 -0
  73. {csu-2.1.5 → csu-2.2.0}/src/csu/utils.py +0 -0
  74. {csu-2.1.5 → csu-2.2.0}/src/csu/views.py +0 -0
  75. {csu-2.1.5 → csu-2.2.0}/src/csu/worker/__init__.py +0 -0
  76. {csu-2.1.5 → csu-2.2.0}/src/csu/worker/admin.py +0 -0
  77. {csu-2.1.5 → csu-2.2.0}/src/csu/worker/asgi.py +0 -0
  78. {csu-2.1.5 → csu-2.2.0}/src/csu/worker/job.py +0 -0
  79. {csu-2.1.5 → csu-2.2.0}/src/csu/worker/models.py +0 -0
  80. {csu-2.1.5 → csu-2.2.0}/src/csu/worker/registry.py +0 -0
  81. {csu-2.1.5 → csu-2.2.0}/src/csu/wsgi.py +0 -0
  82. {csu-2.1.5 → csu-2.2.0}/src/csu/xml.py +0 -0
  83. {csu-2.1.5 → csu-2.2.0}/src/csu.egg-info/SOURCES.txt +0 -0
  84. {csu-2.1.5 → csu-2.2.0}/src/csu.egg-info/dependency_links.txt +0 -0
  85. {csu-2.1.5 → csu-2.2.0}/src/csu.egg-info/requires.txt +0 -0
  86. {csu-2.1.5 → csu-2.2.0}/src/csu.egg-info/top_level.txt +0 -0
  87. {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_custom_context.yaml +0 -0
  88. {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_custom_context_0_retries.yaml +0 -0
  89. {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_decoding.yaml +0 -0
  90. {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_error.yaml +0 -0
  91. {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_logging.yaml +0 -0
  92. {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service/test_redirects.yaml +0 -0
  93. {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service_auth/test_async.yaml +0 -0
  94. {csu-2.1.5 → csu-2.2.0}/tests/cassettes/test_service_auth/test_sync.yaml +0 -0
  95. {csu-2.1.5 → csu-2.2.0}/tests/conftest.py +0 -0
  96. {csu-2.1.5 → csu-2.2.0}/tests/exampleworker.py +0 -0
  97. {csu-2.1.5 → csu-2.2.0}/tests/test_models.py +0 -0
  98. {csu-2.1.5 → csu-2.2.0}/tests/test_service.py +0 -0
  99. {csu-2.1.5 → csu-2.2.0}/tests/test_service_auth.py +0 -0
  100. {csu-2.1.5 → csu-2.2.0}/tests/test_views.py +0 -0
  101. {csu-2.1.5 → csu-2.2.0}/tests/test_worker.py +0 -0
  102. {csu-2.1.5 → csu-2.2.0}/tests/testproject/__init__.py +0 -0
  103. {csu-2.1.5 → csu-2.2.0}/tests/testproject/fixtures/testuser.json +0 -0
  104. {csu-2.1.5 → csu-2.2.0}/tests/testproject/models.py +0 -0
  105. {csu-2.1.5 → csu-2.2.0}/tests/testproject/settings.py +0 -0
  106. {csu-2.1.5 → csu-2.2.0}/tests/testproject/urls.py +0 -0
  107. {csu-2.1.5 → csu-2.2.0}/tox.ini +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.1.5
2
+ current_version = 2.2.0
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -40,7 +40,7 @@ default_context:
40
40
  sphinx_theme: furo
41
41
  test_matrix_separate_coverage: 'no'
42
42
  tests_inside_package: 'yes'
43
- version: 2.1.5
43
+ version: 2.2.0
44
44
  version_manager: bump2version
45
45
  website: https://blog.ionelmc.ro
46
46
  year_from: '2024'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csu
3
- Version: 2.1.5
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.1.5.svg
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.1.5...main
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.1.5.svg
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.1.5...main
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/
@@ -12,7 +12,7 @@ dynamic = [
12
12
  "readme",
13
13
  ]
14
14
  name = "csu"
15
- version = "2.1.5"
15
+ version = "2.2.0"
16
16
  license = "BSD-2-Clause"
17
17
  license-files = ["LICENSE"]
18
18
  description = "Clean Slate Utils - bunch of utility code, mostly Django/DRF specific."
@@ -2,4 +2,4 @@
2
2
  a.k.a Clean Slate Utils
3
3
  """
4
4
 
5
- __version__ = "2.1.5"
5
+ __version__ = "2.2.0"
@@ -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 not self.shutdown_requested:
216
+ if self.shutdown_requested:
217
+ self.state = WorkerState.SHUTDOWN
218
+ else:
220
219
  self.state = WorkerState.BEFORE
221
220
  await self.before()
222
- self.state = WorkerState.READY
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
- self.state = WorkerState.AFTER
259
- await self.after(idle=idle)
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
- queue_maxsize: int
269
+ queue_sem: Semaphore
270
270
  shutdown_on: Iterable[int] = (signal.SIGINT, signal.SIGTERM)
271
271
  shutdown_requested = False
272
- shutdown_task = None
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.queue_awaited.set()
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.queue_awaited.wait()
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 = Queue(maxsize=self.queue_maxsize)
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
- logger.info("%s.shutdown_handler(%s)", self, sig)
321
- self.shutdown_task = asyncio.create_task(self.stop(graceful=not self.shutdown_requested))
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
- await self.push(None)
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["QSIZE"] = "UNSTARTED" if self.queue is None else self.queue.qsize()
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))
@@ -16,3 +16,4 @@ class WorkerState(Enum):
16
16
  CANCELED = auto()
17
17
  FAILED = auto()
18
18
  EXITED = auto()
19
+ SHUTDOWN = auto()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csu
3
- Version: 2.1.5
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.1.5.svg
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.1.5...main
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
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