csu 2.1.4__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.4 → csu-2.2.0}/.bumpversion.cfg +1 -1
  2. {csu-2.1.4 → csu-2.2.0}/.cookiecutterrc +1 -1
  3. {csu-2.1.4 → csu-2.2.0}/PKG-INFO +3 -3
  4. {csu-2.1.4 → csu-2.2.0}/README.rst +2 -2
  5. {csu-2.1.4 → csu-2.2.0}/pyproject.toml +1 -1
  6. {csu-2.1.4 → csu-2.2.0}/src/csu/__init__.py +1 -1
  7. {csu-2.1.4 → csu-2.2.0}/src/csu/worker/engine.py +57 -50
  8. {csu-2.1.4 → csu-2.2.0}/src/csu/worker/enums.py +1 -0
  9. {csu-2.1.4 → csu-2.2.0}/src/csu.egg-info/PKG-INFO +3 -3
  10. {csu-2.1.4 → csu-2.2.0}/tests/exampleworker.py +11 -10
  11. csu-2.2.0/tests/test_exampleworker.py +143 -0
  12. csu-2.1.4/tests/test_exampleworker.py +0 -87
  13. {csu-2.1.4 → csu-2.2.0}/.coveragerc +0 -0
  14. {csu-2.1.4 → csu-2.2.0}/.editorconfig +0 -0
  15. {csu-2.1.4 → csu-2.2.0}/.github/workflows/github-actions.yml +0 -0
  16. {csu-2.1.4 → csu-2.2.0}/.pre-commit-config.yaml +0 -0
  17. {csu-2.1.4 → csu-2.2.0}/.taplo.toml +0 -0
  18. {csu-2.1.4 → csu-2.2.0}/AUTHORS.rst +0 -0
  19. {csu-2.1.4 → csu-2.2.0}/CHANGELOG.rst +0 -0
  20. {csu-2.1.4 → csu-2.2.0}/CONTRIBUTING.rst +0 -0
  21. {csu-2.1.4 → csu-2.2.0}/LICENSE +0 -0
  22. {csu-2.1.4 → csu-2.2.0}/MANIFEST.in +0 -0
  23. {csu-2.1.4 → csu-2.2.0}/ci/bootstrap.py +0 -0
  24. {csu-2.1.4 → csu-2.2.0}/ci/requirements.txt +0 -0
  25. {csu-2.1.4 → csu-2.2.0}/ci/templates/.github/workflows/github-actions.yml +0 -0
  26. {csu-2.1.4 → csu-2.2.0}/pytest.ini +0 -0
  27. {csu-2.1.4 → csu-2.2.0}/setup.cfg +0 -0
  28. {csu-2.1.4 → csu-2.2.0}/src/csu/admin.py +0 -0
  29. {csu-2.1.4 → csu-2.2.0}/src/csu/conf.py +0 -0
  30. {csu-2.1.4 → csu-2.2.0}/src/csu/consts.py +0 -0
  31. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/__init__.py +0 -0
  32. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/auth.py +0 -0
  33. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/fields.py +0 -0
  34. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/forms.py +0 -0
  35. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/perms.py +0 -0
  36. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/phonenumber.py +0 -0
  37. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/serializers.py +0 -0
  38. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/test_auth.py +0 -0
  39. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/test_fields.py +0 -0
  40. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/test_forms.py +0 -0
  41. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/test_phonenumber.py +0 -0
  42. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/test_serializers.py +0 -0
  43. {csu-2.1.4 → csu-2.2.0}/src/csu/drf/views.py +0 -0
  44. {csu-2.1.4 → csu-2.2.0}/src/csu/enums.py +0 -0
  45. {csu-2.1.4 → csu-2.2.0}/src/csu/env.py +0 -0
  46. {csu-2.1.4 → csu-2.2.0}/src/csu/exceptions.py +0 -0
  47. {csu-2.1.4 → csu-2.2.0}/src/csu/export.py +0 -0
  48. {csu-2.1.4 → csu-2.2.0}/src/csu/fixups.py +0 -0
  49. {csu-2.1.4 → csu-2.2.0}/src/csu/forms/__init__.py +0 -0
  50. {csu-2.1.4 → csu-2.2.0}/src/csu/forms/crispy.py +0 -0
  51. {csu-2.1.4 → csu-2.2.0}/src/csu/forms/fields.py +0 -0
  52. {csu-2.1.4 → csu-2.2.0}/src/csu/gettext.py +0 -0
  53. {csu-2.1.4 → csu-2.2.0}/src/csu/gettext_lazy.py +0 -0
  54. {csu-2.1.4 → csu-2.2.0}/src/csu/locale/ro/LC_MESSAGES/django.mo +0 -0
  55. {csu-2.1.4 → csu-2.2.0}/src/csu/locale/ro/LC_MESSAGES/django.po +0 -0
  56. {csu-2.1.4 → csu-2.2.0}/src/csu/logging.py +0 -0
  57. {csu-2.1.4 → csu-2.2.0}/src/csu/management.py +0 -0
  58. {csu-2.1.4 → csu-2.2.0}/src/csu/models.py +0 -0
  59. {csu-2.1.4 → csu-2.2.0}/src/csu/query.py +0 -0
  60. {csu-2.1.4 → csu-2.2.0}/src/csu/routers.py +0 -0
  61. {csu-2.1.4 → csu-2.2.0}/src/csu/service.py +0 -0
  62. {csu-2.1.4 → csu-2.2.0}/src/csu/templates/api_exception.html +0 -0
  63. {csu-2.1.4 → csu-2.2.0}/src/csu/templates/forms/widgets/opaquewidget.html +0 -0
  64. {csu-2.1.4 → csu-2.2.0}/src/csu/test_consts.py +0 -0
  65. {csu-2.1.4 → csu-2.2.0}/src/csu/test_env.py +0 -0
  66. {csu-2.1.4 → csu-2.2.0}/src/csu/test_exceptions.py +0 -0
  67. {csu-2.1.4 → csu-2.2.0}/src/csu/test_logging.py +0 -0
  68. {csu-2.1.4 → csu-2.2.0}/src/csu/test_management.py +0 -0
  69. {csu-2.1.4 → csu-2.2.0}/src/csu/test_service.py +0 -0
  70. {csu-2.1.4 → csu-2.2.0}/src/csu/test_timezones.py +0 -0
  71. {csu-2.1.4 → csu-2.2.0}/src/csu/test_utils.py +0 -0
  72. {csu-2.1.4 → csu-2.2.0}/src/csu/test_xml.py +0 -0
  73. {csu-2.1.4 → csu-2.2.0}/src/csu/timezones.py +0 -0
  74. {csu-2.1.4 → csu-2.2.0}/src/csu/utils.py +0 -0
  75. {csu-2.1.4 → csu-2.2.0}/src/csu/views.py +0 -0
  76. {csu-2.1.4 → csu-2.2.0}/src/csu/worker/__init__.py +0 -0
  77. {csu-2.1.4 → csu-2.2.0}/src/csu/worker/admin.py +0 -0
  78. {csu-2.1.4 → csu-2.2.0}/src/csu/worker/asgi.py +0 -0
  79. {csu-2.1.4 → csu-2.2.0}/src/csu/worker/job.py +0 -0
  80. {csu-2.1.4 → csu-2.2.0}/src/csu/worker/models.py +0 -0
  81. {csu-2.1.4 → csu-2.2.0}/src/csu/worker/registry.py +0 -0
  82. {csu-2.1.4 → csu-2.2.0}/src/csu/wsgi.py +0 -0
  83. {csu-2.1.4 → csu-2.2.0}/src/csu/xml.py +0 -0
  84. {csu-2.1.4 → csu-2.2.0}/src/csu.egg-info/SOURCES.txt +0 -0
  85. {csu-2.1.4 → csu-2.2.0}/src/csu.egg-info/dependency_links.txt +0 -0
  86. {csu-2.1.4 → csu-2.2.0}/src/csu.egg-info/requires.txt +0 -0
  87. {csu-2.1.4 → csu-2.2.0}/src/csu.egg-info/top_level.txt +0 -0
  88. {csu-2.1.4 → csu-2.2.0}/tests/cassettes/test_service/test_custom_context.yaml +0 -0
  89. {csu-2.1.4 → csu-2.2.0}/tests/cassettes/test_service/test_custom_context_0_retries.yaml +0 -0
  90. {csu-2.1.4 → csu-2.2.0}/tests/cassettes/test_service/test_decoding.yaml +0 -0
  91. {csu-2.1.4 → csu-2.2.0}/tests/cassettes/test_service/test_error.yaml +0 -0
  92. {csu-2.1.4 → csu-2.2.0}/tests/cassettes/test_service/test_logging.yaml +0 -0
  93. {csu-2.1.4 → csu-2.2.0}/tests/cassettes/test_service/test_redirects.yaml +0 -0
  94. {csu-2.1.4 → csu-2.2.0}/tests/cassettes/test_service_auth/test_async.yaml +0 -0
  95. {csu-2.1.4 → csu-2.2.0}/tests/cassettes/test_service_auth/test_sync.yaml +0 -0
  96. {csu-2.1.4 → csu-2.2.0}/tests/conftest.py +0 -0
  97. {csu-2.1.4 → csu-2.2.0}/tests/test_models.py +0 -0
  98. {csu-2.1.4 → csu-2.2.0}/tests/test_service.py +0 -0
  99. {csu-2.1.4 → csu-2.2.0}/tests/test_service_auth.py +0 -0
  100. {csu-2.1.4 → csu-2.2.0}/tests/test_views.py +0 -0
  101. {csu-2.1.4 → csu-2.2.0}/tests/test_worker.py +0 -0
  102. {csu-2.1.4 → csu-2.2.0}/tests/testproject/__init__.py +0 -0
  103. {csu-2.1.4 → csu-2.2.0}/tests/testproject/fixtures/testuser.json +0 -0
  104. {csu-2.1.4 → csu-2.2.0}/tests/testproject/models.py +0 -0
  105. {csu-2.1.4 → csu-2.2.0}/tests/testproject/settings.py +0 -0
  106. {csu-2.1.4 → csu-2.2.0}/tests/testproject/urls.py +0 -0
  107. {csu-2.1.4 → csu-2.2.0}/tox.ini +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.1.4
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.4
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.4
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.4.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.4...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.4.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.4...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.4"
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.4"
5
+ __version__ = "2.2.0"
@@ -4,11 +4,12 @@ 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
+ from collections.abc import Iterable
12
13
  from contextlib import asynccontextmanager
13
14
  from datetime import datetime
14
15
  from datetime import time
@@ -17,6 +18,7 @@ from enum import Flag
17
18
  from enum import auto
18
19
  from functools import partial
19
20
  from random import randint
21
+ from typing import ClassVar
20
22
  from typing import Protocol
21
23
 
22
24
  from datetimerange import DateTimeRange
@@ -46,17 +48,19 @@ class AbstractWorker(ABC):
46
48
  state: WorkerState = WorkerState.UNKNOWN
47
49
  runner: asyncio.Task | None = None
48
50
  shutdown_requested = False
51
+ id_counter: ClassVar = 0
52
+ id = "?"
49
53
 
50
54
  def __str__(self):
51
- return f"{type(self).__name__}({self.state.name})"
52
-
53
- def __repr__(self):
54
- return f"{type(self).__name__}({id(self)}, state={self.state.name})"
55
+ return f"{type(self).__name__}({self.id}/{self.state.name})"
55
56
 
56
57
  def report(self, inspect: defaultdict[str, int]):
57
58
  inspect[self.state.name] += 1
58
59
 
59
60
  def start(self):
61
+ cls = type(self)
62
+ cls.id_counter += 1
63
+ self.id = cls.id_counter
60
64
  self.runner = asyncio.create_task(self.run())
61
65
  # noinspection PyTypeChecker
62
66
  self.runner.add_done_callback(self.on_exit)
@@ -113,18 +117,6 @@ class SleepingTimeRangeMixin:
113
117
  await asyncio.sleep(delta)
114
118
 
115
119
 
116
- class NoPrefetchingProducerMixin:
117
- state: WorkerState
118
- engine: AbstractEngine
119
- sleeping_timerange: tuple[time, time]
120
-
121
- async def before(self):
122
- # noinspection PyUnresolvedReferences
123
- await super().before()
124
- self.state = WorkerState.WAITING
125
- await self.engine.wait_worker_ready()
126
-
127
-
128
120
  class SleepingCooldown(Flag):
129
121
  IDLE = auto()
130
122
  BUSY = auto()
@@ -172,15 +164,16 @@ class AbstractProducer(AbstractWorker):
172
164
  def __init__(self, engine: AbstractEngine):
173
165
  self.engine = engine
174
166
 
175
- def __str__(self):
176
- return f"{type(self).__name__}()"
177
-
178
167
  async def run(self):
179
168
  task: TaskProtocol | None
180
169
  logger.info("%s.run() started.", self)
181
170
  while True:
182
171
  self.state = WorkerState.BEFORE
183
172
  await self.before()
173
+ if self.shutdown_requested:
174
+ break
175
+ self.state = WorkerState.WAITING
176
+ await self.engine.wait_worker_ready()
184
177
  if self.shutdown_requested:
185
178
  break
186
179
  try:
@@ -197,6 +190,8 @@ class AbstractProducer(AbstractWorker):
197
190
  await self.engine.push(job := self.job_class(**task.job_kwargs))
198
191
  logger.info("%s.run() awaiting %s.done of %s.", self, job, task)
199
192
  await task.done_watchdog(job)
193
+ if self.shutdown_requested:
194
+ break
200
195
  self.state = WorkerState.AFTER
201
196
  await self.after(idle=not task)
202
197
 
@@ -216,15 +211,14 @@ class AbstractConsumer(AbstractWorker):
216
211
  def __init__(self, engine: AbstractEngine):
217
212
  self.engine = engine
218
213
 
219
- def __str__(self):
220
- return f"{type(self).__name__}({id(self)})"
221
-
222
214
  async def run(self):
223
215
  while True:
224
- if not self.shutdown_requested:
216
+ if self.shutdown_requested:
217
+ self.state = WorkerState.SHUTDOWN
218
+ else:
225
219
  self.state = WorkerState.BEFORE
226
220
  await self.before()
227
- self.state = WorkerState.READY
221
+ self.state = WorkerState.READY
228
222
  job: Job | None
229
223
  async with self.engine.consume() as job:
230
224
  if job is None:
@@ -260,8 +254,9 @@ class AbstractConsumer(AbstractWorker):
260
254
 
261
255
  if self.shutdown_requested:
262
256
  continue
263
- self.state = WorkerState.AFTER
264
- await self.after(idle=idle)
257
+ else:
258
+ self.state = WorkerState.AFTER
259
+ await self.after(idle=idle)
265
260
 
266
261
  @abstractmethod
267
262
  async def perform_work(self, job: Job) -> dict:
@@ -271,15 +266,13 @@ class AbstractConsumer(AbstractWorker):
271
266
  class AbstractEngine(ABC):
272
267
  workers: list[AbstractWorker]
273
268
  queue: Queue = None
274
- queue_maxsize: int
275
- shutdown_signal = signal.SIGINT
269
+ queue_sem: Semaphore
270
+ shutdown_on: Iterable[int] = (signal.SIGINT, signal.SIGTERM)
276
271
  shutdown_requested = False
277
- shutdown_task = None
272
+ shutdown_tasks: ClassVar[dict[bool, asyncio.Task]] = {}
278
273
 
279
274
  def __init__(self):
280
275
  self.workers = []
281
- self.queue_awaited = Event()
282
- self.queue_maxsize = 0
283
276
 
284
277
  def __str__(self):
285
278
  return f"{type(self).__name__}({len(self.workers)}w/{self.queue.qsize() if self.queue else '-'}q)"
@@ -293,35 +286,43 @@ class AbstractEngine(ABC):
293
286
 
294
287
  async def push(self, job: Job | None) -> None:
295
288
  logger.info("%s.push(%s)", self, job)
296
- self.queue_awaited.clear() # clear it here, it's the shortest path to "consumer wakes up and calls .set()"
297
289
  await self.queue.put(job)
298
290
 
299
291
  @asynccontextmanager
300
292
  async def consume(self) -> AsyncIterator[Job | None]:
301
- self.queue_awaited.set()
293
+ self.queue_sem.release()
302
294
  yield await self.queue.get()
303
- # if raised then there's no task to mark as done
295
+ # if raised then there's no task to mark as done (this won't be reached on exception)
304
296
  self.queue.task_done()
305
297
 
306
298
  async def wait_worker_ready(self):
307
299
  # event set after consuming, cleared right before there's a task
308
- await self.queue_awaited.wait()
300
+ await self.queue_sem.acquire()
309
301
 
310
302
  def add_worker(self, worker: AbstractWorker):
311
- assert self.queue is None, f"{self} is already started"
312
- if isinstance(worker, AbstractConsumer):
313
- self.queue_maxsize += 1
303
+ assert self.queue is None, f"{self} is already started, {self.queue=}"
314
304
  self.workers.append(worker)
315
305
 
316
306
  async def start(self):
317
- 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)
318
310
  for worker in self.workers:
319
311
  assert worker.runner is None, f"{worker=} should not be started"
320
312
  worker.start()
321
- signal.signal(self.shutdown_signal, self.shutdown_handler)
313
+ for sig in self.shutdown_on:
314
+ signal.signal(sig, self.shutdown_handler)
322
315
 
323
316
  def shutdown_handler(self, sig, frame):
324
- 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))
325
326
  self.shutdown_requested = True
326
327
 
327
328
  async def stop(self, graceful=False):
@@ -330,16 +331,19 @@ class AbstractEngine(ABC):
330
331
  for i, worker in enumerate(self.workers, 1):
331
332
  logger.info("%s worker %s: %r", self, i, worker)
332
333
  if graceful:
333
- logger.info("%s waiting producers to stop...", self)
334
- producers = [worker for worker in self.workers if isinstance(worker, AbstractProducer)]
335
- for producer in producers:
336
- producer.shutdown_requested = True
337
- await asyncio.gather(*[producer.runner for producer in producers], return_exceptions=True)
338
- logger.info("%s requesting all workers to shutdown...", self)
339
334
  for worker in self.workers:
340
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:
341
345
  if isinstance(worker, AbstractConsumer):
342
- await self.push(None)
346
+ self.queue.put_nowait(None)
343
347
  else:
344
348
  while self.queue.qsize() > 0:
345
349
  item = self.queue.get_nowait()
@@ -352,7 +356,10 @@ class AbstractEngine(ABC):
352
356
  logger.info("%s shutdown complete.", self)
353
357
 
354
358
  def report(self, inspect: defaultdict[str, object]):
355
- 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
+ )
356
363
 
357
364
  def inspect(self) -> dict[str, dict[str, int]]:
358
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.4
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.4.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.4...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/
@@ -9,7 +9,6 @@ from logging import getLogger
9
9
  from csu.worker.engine import AbstractConsumer
10
10
  from csu.worker.engine import AbstractEngine
11
11
  from csu.worker.engine import AbstractProducer
12
- from csu.worker.engine import NoPrefetchingProducerMixin
13
12
  from csu.worker.engine import SleepingCooldown
14
13
  from csu.worker.engine import SleepingCooldownMixin
15
14
  from csu.worker.engine import TaskProtocol
@@ -48,14 +47,7 @@ class ExampleResultType(StrEnum):
48
47
  ABSENT = "ABSENT"
49
48
 
50
49
 
51
- if sys.argv[1] == "prefetch":
52
-
53
- class NoPrefetchingProducerMixin:
54
- pass
55
-
56
-
57
50
  class ExampleProducer(
58
- NoPrefetchingProducerMixin,
59
51
  SleepingCooldownMixin,
60
52
  AbstractProducer,
61
53
  ):
@@ -104,7 +96,7 @@ class ExampleEngine(AbstractEngine):
104
96
  self.producer_skip = int(producer_skip)
105
97
  self.consumer_sleep = int(consumer_sleep) / 1000
106
98
  self.max_jobs = int(max_jobs)
107
- print(self.__dict__)
99
+ self.job_counter = 0
108
100
 
109
101
  async def start(self):
110
102
  for i in range(self.consumers):
@@ -119,8 +111,17 @@ class ExampleEngine(AbstractEngine):
119
111
  print(self.inspect())
120
112
  print(f"DONE: {self.inspect()}")
121
113
 
114
+ async def push(self, job: Job | None) -> None:
115
+ self.job_counter += 1
116
+ await super().push(job)
117
+
118
+ def report(self, inspect):
119
+ super().report(inspect)
120
+ inspect["JOBS"] = self.job_counter
121
+
122
122
 
123
123
  if __name__ == "__main__":
124
+ print("+", " ".join(sys.argv))
124
125
  logging.basicConfig(level=logging.DEBUG)
125
- engine = ExampleEngine(*sys.argv[2:])
126
+ engine = ExampleEngine(*sys.argv[1:])
126
127
  asyncio.run(engine.start())
@@ -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,87 +0,0 @@
1
- import sys
2
-
3
- from process_tests import TestProcess
4
- from process_tests import dump_on_error
5
- from process_tests import wait_for_strings
6
-
7
-
8
- def test_noprefetch_noskip():
9
- with TestProcess(sys.executable, "-mexampleworker", "nope", "3", "0", "1000", "4") as target, dump_on_error(target.read):
10
- wait_for_strings(
11
- target.read,
12
- 5,
13
- "PRODUCER fetch_task @ 1 RETURN",
14
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
15
- "INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
16
- "working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
17
- "- performing...",
18
- "CONSUMER 0 perform_work JOB 1 START",
19
- "CONSUMER 0 perform_work JOB 1 DONE",
20
- "CONSUMER 0 save_result JOB 1",
21
- "PRODUCER fetch_task @ 2 RETURN",
22
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=2))",
23
- "INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=2).done of NoOpTask(id=2).",
24
- ").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=2)",
25
- ").run() - performing...",
26
- "CONSUMER 1 perform_work JOB 2 START",
27
- "CONSUMER 1 perform_work JOB 2 DONE",
28
- "CONSUMER 1 save_result JOB 2",
29
- "PRODUCER fetch_task @ 3 RETURN",
30
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
31
- "INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
32
- ").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
33
- ").run() - performing...",
34
- "CONSUMER 2 perform_work JOB 3 START",
35
- "CONSUMER 2 perform_work JOB 3 DONE",
36
- "CONSUMER 2 save_result JOB 3",
37
- "PRODUCER fetch_task @ 4 RETURN",
38
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=4))",
39
- "INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=4).done of NoOpTask(id=4).",
40
- ").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=4)",
41
- ").run() - performing...",
42
- "CONSUMER 0 perform_work JOB 4 START",
43
- "CONSUMER 0 perform_work JOB 4 DONE",
44
- "CONSUMER 0 save_result JOB 4",
45
- "EXHAUSTED",
46
- )
47
-
48
-
49
- def test_prefetch_skip():
50
- with TestProcess(sys.executable, "-mexampleworker", "nope", "3", "0", "100", "4") as target, dump_on_error(target.read):
51
- wait_for_strings(
52
- target.read,
53
- 5,
54
- "PRODUCER fetch_task @ 1 RETURN",
55
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
56
- "INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
57
- ").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
58
- ").run() - performing...",
59
- "CONSUMER 0 perform_work JOB 1 START",
60
- "CONSUMER 0 perform_work JOB 1 DONE",
61
- "CONSUMER 0 save_result JOB 1",
62
- "PRODUCER fetch_task @ 2 RETURN",
63
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=2))",
64
- "INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=2).done of NoOpTask(id=2).",
65
- ").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=2)",
66
- ").run() - performing...",
67
- "CONSUMER 1 perform_work JOB 2 START",
68
- "CONSUMER 1 perform_work JOB 2 DONE",
69
- "CONSUMER 1 save_result JOB 2",
70
- "PRODUCER fetch_task @ 3 RETURN",
71
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
72
- "INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
73
- ").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
74
- ").run() - performing...",
75
- "CONSUMER 2 perform_work JOB 3 START",
76
- "CONSUMER 2 perform_work JOB 3 DONE",
77
- "CONSUMER 2 save_result JOB 3",
78
- "PRODUCER fetch_task @ 4 RETURN",
79
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=4))",
80
- "INFO:worker:ExampleProducer().run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=4).done of NoOpTask(id=4).",
81
- ").run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=4)",
82
- ").run() - performing...",
83
- "CONSUMER 0 perform_work JOB 4 START",
84
- "CONSUMER 0 perform_work JOB 4 DONE",
85
- "CONSUMER 0 save_result JOB 4",
86
- "PRODUCER fetch_task @ 5 EXHAUSTED",
87
- )
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