csu 2.1.5__tar.gz → 2.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {csu-2.1.5 → csu-2.2.1}/.bumpversion.cfg +1 -1
  2. {csu-2.1.5 → csu-2.2.1}/.cookiecutterrc +1 -1
  3. {csu-2.1.5 → csu-2.2.1}/PKG-INFO +3 -3
  4. {csu-2.1.5 → csu-2.2.1}/README.rst +2 -2
  5. {csu-2.1.5 → csu-2.2.1}/pyproject.toml +1 -1
  6. {csu-2.1.5 → csu-2.2.1}/src/csu/__init__.py +1 -1
  7. {csu-2.1.5 → csu-2.2.1}/src/csu/worker/engine.py +55 -40
  8. {csu-2.1.5 → csu-2.2.1}/src/csu/worker/enums.py +1 -0
  9. {csu-2.1.5 → csu-2.2.1}/src/csu.egg-info/PKG-INFO +3 -3
  10. csu-2.2.1/tests/test_exampleworker.py +164 -0
  11. csu-2.1.5/tests/test_exampleworker.py +0 -104
  12. {csu-2.1.5 → csu-2.2.1}/.coveragerc +0 -0
  13. {csu-2.1.5 → csu-2.2.1}/.editorconfig +0 -0
  14. {csu-2.1.5 → csu-2.2.1}/.github/workflows/github-actions.yml +0 -0
  15. {csu-2.1.5 → csu-2.2.1}/.pre-commit-config.yaml +0 -0
  16. {csu-2.1.5 → csu-2.2.1}/.taplo.toml +0 -0
  17. {csu-2.1.5 → csu-2.2.1}/AUTHORS.rst +0 -0
  18. {csu-2.1.5 → csu-2.2.1}/CHANGELOG.rst +0 -0
  19. {csu-2.1.5 → csu-2.2.1}/CONTRIBUTING.rst +0 -0
  20. {csu-2.1.5 → csu-2.2.1}/LICENSE +0 -0
  21. {csu-2.1.5 → csu-2.2.1}/MANIFEST.in +0 -0
  22. {csu-2.1.5 → csu-2.2.1}/ci/bootstrap.py +0 -0
  23. {csu-2.1.5 → csu-2.2.1}/ci/requirements.txt +0 -0
  24. {csu-2.1.5 → csu-2.2.1}/ci/templates/.github/workflows/github-actions.yml +0 -0
  25. {csu-2.1.5 → csu-2.2.1}/pytest.ini +0 -0
  26. {csu-2.1.5 → csu-2.2.1}/setup.cfg +0 -0
  27. {csu-2.1.5 → csu-2.2.1}/src/csu/admin.py +0 -0
  28. {csu-2.1.5 → csu-2.2.1}/src/csu/conf.py +0 -0
  29. {csu-2.1.5 → csu-2.2.1}/src/csu/consts.py +0 -0
  30. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/__init__.py +0 -0
  31. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/auth.py +0 -0
  32. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/fields.py +0 -0
  33. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/forms.py +0 -0
  34. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/perms.py +0 -0
  35. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/phonenumber.py +0 -0
  36. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/serializers.py +0 -0
  37. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/test_auth.py +0 -0
  38. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/test_fields.py +0 -0
  39. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/test_forms.py +0 -0
  40. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/test_phonenumber.py +0 -0
  41. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/test_serializers.py +0 -0
  42. {csu-2.1.5 → csu-2.2.1}/src/csu/drf/views.py +0 -0
  43. {csu-2.1.5 → csu-2.2.1}/src/csu/enums.py +0 -0
  44. {csu-2.1.5 → csu-2.2.1}/src/csu/env.py +0 -0
  45. {csu-2.1.5 → csu-2.2.1}/src/csu/exceptions.py +0 -0
  46. {csu-2.1.5 → csu-2.2.1}/src/csu/export.py +0 -0
  47. {csu-2.1.5 → csu-2.2.1}/src/csu/fixups.py +0 -0
  48. {csu-2.1.5 → csu-2.2.1}/src/csu/forms/__init__.py +0 -0
  49. {csu-2.1.5 → csu-2.2.1}/src/csu/forms/crispy.py +0 -0
  50. {csu-2.1.5 → csu-2.2.1}/src/csu/forms/fields.py +0 -0
  51. {csu-2.1.5 → csu-2.2.1}/src/csu/gettext.py +0 -0
  52. {csu-2.1.5 → csu-2.2.1}/src/csu/gettext_lazy.py +0 -0
  53. {csu-2.1.5 → csu-2.2.1}/src/csu/locale/ro/LC_MESSAGES/django.mo +0 -0
  54. {csu-2.1.5 → csu-2.2.1}/src/csu/locale/ro/LC_MESSAGES/django.po +0 -0
  55. {csu-2.1.5 → csu-2.2.1}/src/csu/logging.py +0 -0
  56. {csu-2.1.5 → csu-2.2.1}/src/csu/management.py +0 -0
  57. {csu-2.1.5 → csu-2.2.1}/src/csu/models.py +0 -0
  58. {csu-2.1.5 → csu-2.2.1}/src/csu/query.py +0 -0
  59. {csu-2.1.5 → csu-2.2.1}/src/csu/routers.py +0 -0
  60. {csu-2.1.5 → csu-2.2.1}/src/csu/service.py +0 -0
  61. {csu-2.1.5 → csu-2.2.1}/src/csu/templates/api_exception.html +0 -0
  62. {csu-2.1.5 → csu-2.2.1}/src/csu/templates/forms/widgets/opaquewidget.html +0 -0
  63. {csu-2.1.5 → csu-2.2.1}/src/csu/test_consts.py +0 -0
  64. {csu-2.1.5 → csu-2.2.1}/src/csu/test_env.py +0 -0
  65. {csu-2.1.5 → csu-2.2.1}/src/csu/test_exceptions.py +0 -0
  66. {csu-2.1.5 → csu-2.2.1}/src/csu/test_logging.py +0 -0
  67. {csu-2.1.5 → csu-2.2.1}/src/csu/test_management.py +0 -0
  68. {csu-2.1.5 → csu-2.2.1}/src/csu/test_service.py +0 -0
  69. {csu-2.1.5 → csu-2.2.1}/src/csu/test_timezones.py +0 -0
  70. {csu-2.1.5 → csu-2.2.1}/src/csu/test_utils.py +0 -0
  71. {csu-2.1.5 → csu-2.2.1}/src/csu/test_xml.py +0 -0
  72. {csu-2.1.5 → csu-2.2.1}/src/csu/timezones.py +0 -0
  73. {csu-2.1.5 → csu-2.2.1}/src/csu/utils.py +0 -0
  74. {csu-2.1.5 → csu-2.2.1}/src/csu/views.py +0 -0
  75. {csu-2.1.5 → csu-2.2.1}/src/csu/worker/__init__.py +0 -0
  76. {csu-2.1.5 → csu-2.2.1}/src/csu/worker/admin.py +0 -0
  77. {csu-2.1.5 → csu-2.2.1}/src/csu/worker/asgi.py +0 -0
  78. {csu-2.1.5 → csu-2.2.1}/src/csu/worker/job.py +0 -0
  79. {csu-2.1.5 → csu-2.2.1}/src/csu/worker/models.py +0 -0
  80. {csu-2.1.5 → csu-2.2.1}/src/csu/worker/registry.py +0 -0
  81. {csu-2.1.5 → csu-2.2.1}/src/csu/wsgi.py +0 -0
  82. {csu-2.1.5 → csu-2.2.1}/src/csu/xml.py +0 -0
  83. {csu-2.1.5 → csu-2.2.1}/src/csu.egg-info/SOURCES.txt +0 -0
  84. {csu-2.1.5 → csu-2.2.1}/src/csu.egg-info/dependency_links.txt +0 -0
  85. {csu-2.1.5 → csu-2.2.1}/src/csu.egg-info/requires.txt +0 -0
  86. {csu-2.1.5 → csu-2.2.1}/src/csu.egg-info/top_level.txt +0 -0
  87. {csu-2.1.5 → csu-2.2.1}/tests/cassettes/test_service/test_custom_context.yaml +0 -0
  88. {csu-2.1.5 → csu-2.2.1}/tests/cassettes/test_service/test_custom_context_0_retries.yaml +0 -0
  89. {csu-2.1.5 → csu-2.2.1}/tests/cassettes/test_service/test_decoding.yaml +0 -0
  90. {csu-2.1.5 → csu-2.2.1}/tests/cassettes/test_service/test_error.yaml +0 -0
  91. {csu-2.1.5 → csu-2.2.1}/tests/cassettes/test_service/test_logging.yaml +0 -0
  92. {csu-2.1.5 → csu-2.2.1}/tests/cassettes/test_service/test_redirects.yaml +0 -0
  93. {csu-2.1.5 → csu-2.2.1}/tests/cassettes/test_service_auth/test_async.yaml +0 -0
  94. {csu-2.1.5 → csu-2.2.1}/tests/cassettes/test_service_auth/test_sync.yaml +0 -0
  95. {csu-2.1.5 → csu-2.2.1}/tests/conftest.py +0 -0
  96. {csu-2.1.5 → csu-2.2.1}/tests/exampleworker.py +0 -0
  97. {csu-2.1.5 → csu-2.2.1}/tests/test_models.py +0 -0
  98. {csu-2.1.5 → csu-2.2.1}/tests/test_service.py +0 -0
  99. {csu-2.1.5 → csu-2.2.1}/tests/test_service_auth.py +0 -0
  100. {csu-2.1.5 → csu-2.2.1}/tests/test_views.py +0 -0
  101. {csu-2.1.5 → csu-2.2.1}/tests/test_worker.py +0 -0
  102. {csu-2.1.5 → csu-2.2.1}/tests/testproject/__init__.py +0 -0
  103. {csu-2.1.5 → csu-2.2.1}/tests/testproject/fixtures/testuser.json +0 -0
  104. {csu-2.1.5 → csu-2.2.1}/tests/testproject/models.py +0 -0
  105. {csu-2.1.5 → csu-2.2.1}/tests/testproject/settings.py +0 -0
  106. {csu-2.1.5 → csu-2.2.1}/tests/testproject/urls.py +0 -0
  107. {csu-2.1.5 → csu-2.2.1}/tox.ini +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.1.5
2
+ current_version = 2.2.1
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.1
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.1
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.1.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.1...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.1.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.1...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.1"
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.1"
@@ -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)
@@ -173,7 +173,7 @@ class AbstractProducer(AbstractWorker):
173
173
  if self.shutdown_requested:
174
174
  break
175
175
  self.state = WorkerState.WAITING
176
- await self.engine.wait_worker_ready()
176
+ await self.engine.before_produce()
177
177
  if self.shutdown_requested:
178
178
  break
179
179
  try:
@@ -187,11 +187,13 @@ class AbstractProducer(AbstractWorker):
187
187
  task = None
188
188
  if task:
189
189
  # noinspection PyArgumentList
190
- await self.engine.push(job := self.job_class(**task.job_kwargs))
190
+ job = self.job_class(**task.job_kwargs)
191
+ await self.engine.push(job)
191
192
  logger.info("%s.run() awaiting %s.done of %s.", self, job, task)
192
193
  await task.done_watchdog(job)
193
194
  if self.shutdown_requested:
194
195
  break
196
+ self.engine.after_produce(idle=not task)
195
197
  self.state = WorkerState.AFTER
196
198
  await self.after(idle=not task)
197
199
 
@@ -211,15 +213,14 @@ class AbstractConsumer(AbstractWorker):
211
213
  def __init__(self, engine: AbstractEngine):
212
214
  self.engine = engine
213
215
 
214
- def __str__(self):
215
- return f"{type(self).__name__}({id(self)})"
216
-
217
216
  async def run(self):
218
217
  while True:
219
- if not self.shutdown_requested:
218
+ if self.shutdown_requested:
219
+ self.state = WorkerState.SHUTDOWN
220
+ else:
220
221
  self.state = WorkerState.BEFORE
221
222
  await self.before()
222
- self.state = WorkerState.READY
223
+ self.state = WorkerState.READY
223
224
  job: Job | None
224
225
  async with self.engine.consume() as job:
225
226
  if job is None:
@@ -255,8 +256,9 @@ class AbstractConsumer(AbstractWorker):
255
256
 
256
257
  if self.shutdown_requested:
257
258
  continue
258
- self.state = WorkerState.AFTER
259
- await self.after(idle=idle)
259
+ else:
260
+ self.state = WorkerState.AFTER
261
+ await self.after(idle=idle)
260
262
 
261
263
  @abstractmethod
262
264
  async def perform_work(self, job: Job) -> dict:
@@ -266,15 +268,13 @@ class AbstractConsumer(AbstractWorker):
266
268
  class AbstractEngine(ABC):
267
269
  workers: list[AbstractWorker]
268
270
  queue: Queue = None
269
- queue_maxsize: int
271
+ queue_sem: Semaphore
270
272
  shutdown_on: Iterable[int] = (signal.SIGINT, signal.SIGTERM)
271
273
  shutdown_requested = False
272
- shutdown_task = None
274
+ shutdown_tasks: ClassVar[dict[bool, asyncio.Task]] = {}
273
275
 
274
276
  def __init__(self):
275
277
  self.workers = []
276
- self.queue_awaited = Event()
277
- self.queue_maxsize = 0
278
278
 
279
279
  def __str__(self):
280
280
  return f"{type(self).__name__}({len(self.workers)}w/{self.queue.qsize() if self.queue else '-'}q)"
@@ -288,28 +288,31 @@ class AbstractEngine(ABC):
288
288
 
289
289
  async def push(self, job: Job | None) -> None:
290
290
  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
291
  await self.queue.put(job)
293
292
 
294
293
  @asynccontextmanager
295
294
  async def consume(self) -> AsyncIterator[Job | None]:
296
- self.queue_awaited.set()
295
+ self.queue_sem.release()
297
296
  yield await self.queue.get()
298
297
  # if raised then there's no task to mark as done (this won't be reached on exception)
299
298
  self.queue.task_done()
300
299
 
301
- async def wait_worker_ready(self):
300
+ async def before_produce(self):
302
301
  # event set after consuming, cleared right before there's a task
303
- await self.queue_awaited.wait()
302
+ await self.queue_sem.acquire()
303
+
304
+ def after_produce(self, *, idle):
305
+ if idle:
306
+ self.queue_sem.release()
304
307
 
305
308
  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
309
+ assert self.queue is None, f"{self} is already started, {self.queue=}"
309
310
  self.workers.append(worker)
310
311
 
311
312
  async def start(self):
312
- self.queue = Queue(maxsize=self.queue_maxsize)
313
+ assert self.queue is None, f"{self} is already started, {self.queue=}"
314
+ self.queue = Queue()
315
+ self.queue_sem = Semaphore(value=0)
313
316
  for worker in self.workers:
314
317
  assert worker.runner is None, f"{worker=} should not be started"
315
318
  worker.start()
@@ -317,8 +320,15 @@ class AbstractEngine(ABC):
317
320
  signal.signal(sig, self.shutdown_handler)
318
321
 
319
322
  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))
323
+ sig_name = signal.Signals(sig).name
324
+ graceful = not self.shutdown_requested
325
+ logger.info("%s.shutdown_handler(%s) %s", self, sig_name, "GRACEFUL" if graceful else "FORCED")
326
+ if not graceful:
327
+ self.shutdown_tasks[True].cancel()
328
+ if graceful in self.shutdown_tasks:
329
+ logger.warn("%s.shutdown_handler(%s) already requested", self, sig_name)
330
+ else:
331
+ self.shutdown_tasks[graceful] = asyncio.create_task(self.stop(graceful=graceful))
322
332
  self.shutdown_requested = True
323
333
 
324
334
  async def stop(self, graceful=False):
@@ -327,16 +337,19 @@ class AbstractEngine(ABC):
327
337
  for i, worker in enumerate(self.workers, 1):
328
338
  logger.info("%s worker %s: %r", self, i, worker)
329
339
  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
340
  for worker in self.workers:
337
341
  worker.shutdown_requested = True
342
+
343
+ producers = [worker.runner for worker in self.workers if isinstance(worker, AbstractProducer)]
344
+ logger.info("%s requesting workers to stop and awaiting %s producers...", self, len(producers))
345
+ await asyncio.gather(
346
+ *producers,
347
+ return_exceptions=True,
348
+ )
349
+ logger.info("%s flushing queue (%s items) and awaiting consumers...", self, self.queue.qsize())
350
+ for worker in self.workers:
338
351
  if isinstance(worker, AbstractConsumer):
339
- await self.push(None)
352
+ self.queue.put_nowait(None)
340
353
  else:
341
354
  while self.queue.qsize() > 0:
342
355
  item = self.queue.get_nowait()
@@ -345,11 +358,13 @@ class AbstractEngine(ABC):
345
358
  for worker in self.workers:
346
359
  worker.runner.cancel()
347
360
  await asyncio.gather(*[worker.runner for worker in self.workers], return_exceptions=True)
348
- self.workers.clear()
349
361
  logger.info("%s shutdown complete.", self)
350
362
 
351
363
  def report(self, inspect: defaultdict[str, object]):
352
- inspect["QSIZE"] = "UNSTARTED" if self.queue is None else self.queue.qsize()
364
+ inspect.update(
365
+ QSIZE="UNSTARTED" if self.queue is None else self.queue.qsize(),
366
+ SEMVAL=self.queue_sem._value,
367
+ )
353
368
 
354
369
  def inspect(self) -> dict[str, dict[str, int]]:
355
370
  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.1
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.1.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.1...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,164 @@
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(",
112
+ "INFO:worker:ExampleEngine(4w/0q) flushing queue (0 items) and awaiting consumers...",
113
+ "INFO:worker:ExampleConsumer(1/SHUTDOWN).on_exit(",
114
+ "INFO:worker:ExampleConsumer(2/SHUTDOWN).on_exit(",
115
+ "INFO:worker:ExampleConsumer(3/SHUTDOWN).on_exit(",
116
+ "INFO:worker:ExampleEngine(4w/0q) shutdown complete.",
117
+ "DONE: {'ExampleConsumer': {'EXITED': 3}, 'ExampleProducer': {'EXITED': 1}, 'ExampleEngine': {'JOBS': 3, 'QSIZE': 0, 'SEMVAL': 2}}",
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
+ )
143
+
144
+
145
+ def test_sem_rollback():
146
+ with TestProcess(sys.executable, "-mexampleworker", "1", "2", "10", "10") as target, dump_on_error(target.read):
147
+ wait_for_strings(
148
+ target.read,
149
+ 15,
150
+ "PRODUCER fetch_task @ 1 RETURN",
151
+ "CONSUMER 0 perform_work JOB 1 START",
152
+ "CONSUMER 0 perform_work JOB 1 DONE",
153
+ "CONSUMER 0 save_result JOB 1",
154
+ "PRODUCER fetch_task @ 2 SKIP",
155
+ "PRODUCER fetch_task @ 3 RETURN",
156
+ "CONSUMER 0 perform_work JOB 3 START",
157
+ "CONSUMER 0 perform_work JOB 3 DONE",
158
+ "CONSUMER 0 save_result JOB 3",
159
+ "PRODUCER fetch_task @ 4 SKIP",
160
+ "PRODUCER fetch_task @ 5 RETURN",
161
+ "CONSUMER 0 perform_work JOB 5 START",
162
+ "CONSUMER 0 perform_work JOB 5 DONE",
163
+ "CONSUMER 0 save_result JOB 5",
164
+ )
@@ -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