csu 2.2.3__tar.gz → 2.2.5__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 (108) hide show
  1. {csu-2.2.3 → csu-2.2.5}/.bumpversion.cfg +1 -1
  2. {csu-2.2.3 → csu-2.2.5}/.cookiecutterrc +1 -1
  3. {csu-2.2.3 → csu-2.2.5}/PKG-INFO +3 -3
  4. {csu-2.2.3 → csu-2.2.5}/README.rst +2 -2
  5. {csu-2.2.3 → csu-2.2.5}/pyproject.toml +1 -1
  6. {csu-2.2.3 → csu-2.2.5}/src/csu/__init__.py +1 -1
  7. {csu-2.2.3 → csu-2.2.5}/src/csu/worker/__init__.py +0 -4
  8. csu-2.2.5/src/csu/worker/asgi.py +74 -0
  9. {csu-2.2.3 → csu-2.2.5}/src/csu/worker/engine.py +12 -5
  10. {csu-2.2.3 → csu-2.2.5}/src/csu/worker/models.py +3 -1
  11. {csu-2.2.3 → csu-2.2.5}/src/csu.egg-info/PKG-INFO +3 -3
  12. csu-2.2.5/tests/test_exampleworker.py +164 -0
  13. csu-2.2.3/src/csu/worker/asgi.py +0 -36
  14. csu-2.2.3/tests/test_exampleworker.py +0 -164
  15. {csu-2.2.3 → csu-2.2.5}/.coveragerc +0 -0
  16. {csu-2.2.3 → csu-2.2.5}/.editorconfig +0 -0
  17. {csu-2.2.3 → csu-2.2.5}/.github/workflows/github-actions.yml +0 -0
  18. {csu-2.2.3 → csu-2.2.5}/.pre-commit-config.yaml +0 -0
  19. {csu-2.2.3 → csu-2.2.5}/.taplo.toml +0 -0
  20. {csu-2.2.3 → csu-2.2.5}/AUTHORS.rst +0 -0
  21. {csu-2.2.3 → csu-2.2.5}/CHANGELOG.rst +0 -0
  22. {csu-2.2.3 → csu-2.2.5}/CONTRIBUTING.rst +0 -0
  23. {csu-2.2.3 → csu-2.2.5}/LICENSE +0 -0
  24. {csu-2.2.3 → csu-2.2.5}/MANIFEST.in +0 -0
  25. {csu-2.2.3 → csu-2.2.5}/ci/bootstrap.py +0 -0
  26. {csu-2.2.3 → csu-2.2.5}/ci/requirements.txt +0 -0
  27. {csu-2.2.3 → csu-2.2.5}/ci/templates/.github/workflows/github-actions.yml +0 -0
  28. {csu-2.2.3 → csu-2.2.5}/pytest.ini +0 -0
  29. {csu-2.2.3 → csu-2.2.5}/setup.cfg +0 -0
  30. {csu-2.2.3 → csu-2.2.5}/src/csu/admin.py +0 -0
  31. {csu-2.2.3 → csu-2.2.5}/src/csu/conf.py +0 -0
  32. {csu-2.2.3 → csu-2.2.5}/src/csu/consts.py +0 -0
  33. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/__init__.py +0 -0
  34. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/auth.py +0 -0
  35. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/fields.py +0 -0
  36. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/forms.py +0 -0
  37. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/perms.py +0 -0
  38. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/phonenumber.py +0 -0
  39. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/serializers.py +0 -0
  40. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/test_auth.py +0 -0
  41. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/test_fields.py +0 -0
  42. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/test_forms.py +0 -0
  43. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/test_phonenumber.py +0 -0
  44. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/test_serializers.py +0 -0
  45. {csu-2.2.3 → csu-2.2.5}/src/csu/drf/views.py +0 -0
  46. {csu-2.2.3 → csu-2.2.5}/src/csu/enums.py +0 -0
  47. {csu-2.2.3 → csu-2.2.5}/src/csu/env.py +0 -0
  48. {csu-2.2.3 → csu-2.2.5}/src/csu/exceptions.py +0 -0
  49. {csu-2.2.3 → csu-2.2.5}/src/csu/export.py +0 -0
  50. {csu-2.2.3 → csu-2.2.5}/src/csu/fixups.py +0 -0
  51. {csu-2.2.3 → csu-2.2.5}/src/csu/forms/__init__.py +0 -0
  52. {csu-2.2.3 → csu-2.2.5}/src/csu/forms/crispy.py +0 -0
  53. {csu-2.2.3 → csu-2.2.5}/src/csu/forms/fields.py +0 -0
  54. {csu-2.2.3 → csu-2.2.5}/src/csu/gettext.py +0 -0
  55. {csu-2.2.3 → csu-2.2.5}/src/csu/gettext_lazy.py +0 -0
  56. {csu-2.2.3 → csu-2.2.5}/src/csu/locale/ro/LC_MESSAGES/django.mo +0 -0
  57. {csu-2.2.3 → csu-2.2.5}/src/csu/locale/ro/LC_MESSAGES/django.po +0 -0
  58. {csu-2.2.3 → csu-2.2.5}/src/csu/logging.py +0 -0
  59. {csu-2.2.3 → csu-2.2.5}/src/csu/management.py +0 -0
  60. {csu-2.2.3 → csu-2.2.5}/src/csu/models.py +0 -0
  61. {csu-2.2.3 → csu-2.2.5}/src/csu/query.py +0 -0
  62. {csu-2.2.3 → csu-2.2.5}/src/csu/routers.py +0 -0
  63. {csu-2.2.3 → csu-2.2.5}/src/csu/service.py +0 -0
  64. {csu-2.2.3 → csu-2.2.5}/src/csu/templates/api_exception.html +0 -0
  65. {csu-2.2.3 → csu-2.2.5}/src/csu/templates/forms/widgets/opaquewidget.html +0 -0
  66. {csu-2.2.3 → csu-2.2.5}/src/csu/test_consts.py +0 -0
  67. {csu-2.2.3 → csu-2.2.5}/src/csu/test_env.py +0 -0
  68. {csu-2.2.3 → csu-2.2.5}/src/csu/test_exceptions.py +0 -0
  69. {csu-2.2.3 → csu-2.2.5}/src/csu/test_logging.py +0 -0
  70. {csu-2.2.3 → csu-2.2.5}/src/csu/test_management.py +0 -0
  71. {csu-2.2.3 → csu-2.2.5}/src/csu/test_service.py +0 -0
  72. {csu-2.2.3 → csu-2.2.5}/src/csu/test_timezones.py +0 -0
  73. {csu-2.2.3 → csu-2.2.5}/src/csu/test_utils.py +0 -0
  74. {csu-2.2.3 → csu-2.2.5}/src/csu/test_xml.py +0 -0
  75. {csu-2.2.3 → csu-2.2.5}/src/csu/timezones.py +0 -0
  76. {csu-2.2.3 → csu-2.2.5}/src/csu/utils.py +0 -0
  77. {csu-2.2.3 → csu-2.2.5}/src/csu/views.py +0 -0
  78. {csu-2.2.3 → csu-2.2.5}/src/csu/worker/admin.py +0 -0
  79. {csu-2.2.3 → csu-2.2.5}/src/csu/worker/enums.py +0 -0
  80. {csu-2.2.3 → csu-2.2.5}/src/csu/worker/job.py +0 -0
  81. {csu-2.2.3 → csu-2.2.5}/src/csu/worker/registry.py +0 -0
  82. {csu-2.2.3 → csu-2.2.5}/src/csu/wsgi.py +0 -0
  83. {csu-2.2.3 → csu-2.2.5}/src/csu/xml.py +0 -0
  84. {csu-2.2.3 → csu-2.2.5}/src/csu.egg-info/SOURCES.txt +0 -0
  85. {csu-2.2.3 → csu-2.2.5}/src/csu.egg-info/dependency_links.txt +0 -0
  86. {csu-2.2.3 → csu-2.2.5}/src/csu.egg-info/requires.txt +0 -0
  87. {csu-2.2.3 → csu-2.2.5}/src/csu.egg-info/top_level.txt +0 -0
  88. {csu-2.2.3 → csu-2.2.5}/tests/cassettes/test_service/test_custom_context.yaml +0 -0
  89. {csu-2.2.3 → csu-2.2.5}/tests/cassettes/test_service/test_custom_context_0_retries.yaml +0 -0
  90. {csu-2.2.3 → csu-2.2.5}/tests/cassettes/test_service/test_decoding.yaml +0 -0
  91. {csu-2.2.3 → csu-2.2.5}/tests/cassettes/test_service/test_error.yaml +0 -0
  92. {csu-2.2.3 → csu-2.2.5}/tests/cassettes/test_service/test_logging.yaml +0 -0
  93. {csu-2.2.3 → csu-2.2.5}/tests/cassettes/test_service/test_redirects.yaml +0 -0
  94. {csu-2.2.3 → csu-2.2.5}/tests/cassettes/test_service_auth/test_async.yaml +0 -0
  95. {csu-2.2.3 → csu-2.2.5}/tests/cassettes/test_service_auth/test_sync.yaml +0 -0
  96. {csu-2.2.3 → csu-2.2.5}/tests/conftest.py +0 -0
  97. {csu-2.2.3 → csu-2.2.5}/tests/exampleworker.py +0 -0
  98. {csu-2.2.3 → csu-2.2.5}/tests/test_models.py +0 -0
  99. {csu-2.2.3 → csu-2.2.5}/tests/test_service.py +0 -0
  100. {csu-2.2.3 → csu-2.2.5}/tests/test_service_auth.py +0 -0
  101. {csu-2.2.3 → csu-2.2.5}/tests/test_views.py +0 -0
  102. {csu-2.2.3 → csu-2.2.5}/tests/test_worker.py +0 -0
  103. {csu-2.2.3 → csu-2.2.5}/tests/testproject/__init__.py +0 -0
  104. {csu-2.2.3 → csu-2.2.5}/tests/testproject/fixtures/testuser.json +0 -0
  105. {csu-2.2.3 → csu-2.2.5}/tests/testproject/models.py +0 -0
  106. {csu-2.2.3 → csu-2.2.5}/tests/testproject/settings.py +0 -0
  107. {csu-2.2.3 → csu-2.2.5}/tests/testproject/urls.py +0 -0
  108. {csu-2.2.3 → csu-2.2.5}/tox.ini +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.2.3
2
+ current_version = 2.2.5
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.2.3
43
+ version: 2.2.5
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.2.3
3
+ Version: 2.2.5
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.2.3.svg
80
+ .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.5.svg
81
81
  :alt: Commits since latest release
82
- :target: https://github.com/ionelmc/python-csu/compare/v2.2.3...main
82
+ :target: https://github.com/ionelmc/python-csu/compare/v2.2.5...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.2.3.svg
37
+ .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.5.svg
38
38
  :alt: Commits since latest release
39
- :target: https://github.com/ionelmc/python-csu/compare/v2.2.3...main
39
+ :target: https://github.com/ionelmc/python-csu/compare/v2.2.5...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.2.3"
15
+ version = "2.2.5"
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.2.3"
5
+ __version__ = "2.2.5"
@@ -17,7 +17,3 @@ Typical batch processing workload::
17
17
 
18
18
  The database is completely optional.
19
19
  """
20
-
21
- from logging import getLogger
22
-
23
- logger = getLogger("worker")
@@ -0,0 +1,74 @@
1
+ from logging import getLogger
2
+
3
+ import django
4
+ from asgiref.typing import ASGIReceiveCallable
5
+ from asgiref.typing import ASGIReceiveEvent
6
+ from asgiref.typing import ASGISendCallable
7
+ from asgiref.typing import LifespanShutdownCompleteEvent
8
+ from asgiref.typing import LifespanShutdownFailedEvent
9
+ from asgiref.typing import LifespanStartupCompleteEvent
10
+ from asgiref.typing import LifespanStartupFailedEvent
11
+ from django.core.handlers.asgi import ASGIHandler
12
+
13
+ from ..utils import lazy_import_classproperty
14
+
15
+ logger = getLogger(__name__)
16
+
17
+
18
+ class WorkerLifespanASGIHandler(ASGIHandler):
19
+ async def __call__(self, scope, receive: ASGIReceiveCallable, send) -> None:
20
+ if scope["type"] == "lifespan":
21
+ await self.handle_lifespan(receive, send)
22
+ else:
23
+ await super().__call__(scope, receive, send)
24
+
25
+ engine_registry = lazy_import_classproperty("csu.worker.registry.REGISTRY")
26
+
27
+ async def handle_start_engines(self):
28
+ registry = self.engine_registry
29
+ logger.info("Found %s registered engines.", len(registry))
30
+ for module_name, engine in registry.items():
31
+ logger.info("Starting engine for %s: %s...", module_name, engine)
32
+ if engine.shutdown_on:
33
+ raise RuntimeError(f"Engine {type(engine)} shutdown_on should be empty, not {engine.shutdown_on}!")
34
+ await engine.start()
35
+ logger.info(
36
+ "Started %s engines: \n %s",
37
+ len(registry),
38
+ "\n ".join(f"{module_name}: {engine}" for module_name, engine in registry.items()),
39
+ )
40
+
41
+ async def handle_stop_engines(self):
42
+ registry = self.engine_registry
43
+ logger.info("Stopping %s registered engines.", len(registry))
44
+ for engine in registry.values():
45
+ await engine.stop(graceful=True)
46
+
47
+ async def handle_lifespan(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
48
+ while True:
49
+ message: ASGIReceiveEvent = await receive()
50
+ match message["type"]:
51
+ case "lifespan.startup":
52
+ try:
53
+ await self.handle_start_engines()
54
+ except Exception as exc:
55
+ await send(LifespanStartupFailedEvent(type="lifespan.startup.failed", message=str(exc)))
56
+ raise
57
+ else:
58
+ await send(LifespanStartupCompleteEvent(type="lifespan.startup.complete"))
59
+ case "lifespan.shutdown":
60
+ try:
61
+ await self.handle_stop_engines()
62
+ except Exception as exc:
63
+ await send(LifespanShutdownFailedEvent(type="lifespan.shutdown.failed", message=str(exc)))
64
+ raise
65
+ else:
66
+ await send(LifespanShutdownCompleteEvent(type="lifespan.shutdown.complete"))
67
+ return
68
+ case _:
69
+ raise ValueError(f"Unexpected message type: {message['type']!r}", message)
70
+
71
+
72
+ def get_worker_lifespan_application() -> WorkerLifespanASGIHandler:
73
+ django.setup(set_prefix=False)
74
+ return WorkerLifespanASGIHandler()
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import signal
3
+ import sys
3
4
  import traceback
4
5
  from abc import ABC
5
6
  from abc import abstractmethod
@@ -17,6 +18,7 @@ from datetime import timedelta
17
18
  from enum import Flag
18
19
  from enum import auto
19
20
  from functools import partial
21
+ from logging import getLogger
20
22
  from random import randint
21
23
  from typing import ClassVar
22
24
  from typing import Protocol
@@ -25,10 +27,11 @@ from datetimerange import DateTimeRange
25
27
 
26
28
  from ..exceptions import InternalServiceError
27
29
  from ..timezones import naivenow
28
- from . import logger
29
30
  from .enums import WorkerState
30
31
  from .job import Job
31
32
 
33
+ logger = getLogger(__name__)
34
+
32
35
 
33
36
  class TaskProtocol(Protocol):
34
37
  job_kwargs: dict
@@ -269,9 +272,11 @@ class AbstractEngine(ABC):
269
272
  workers: list[AbstractWorker]
270
273
  queue: Queue = None
271
274
  queue_sem: Semaphore
272
- shutdown_on: Iterable[int] = (signal.SIGINT, signal.SIGTERM)
275
+ shutdown_on: Iterable[signal.Signals] = (signal.SIGINT, signal.SIGTERM)
273
276
  shutdown_requested = False
274
277
  shutdown_tasks: ClassVar[dict[bool, asyncio.Task]] = {}
278
+ if sys.platform == "win32":
279
+ shutdown_on += (signal.SIGBREAK,)
275
280
 
276
281
  def __init__(self):
277
282
  self.workers = []
@@ -314,11 +319,13 @@ class AbstractEngine(ABC):
314
319
  for worker in self.workers:
315
320
  assert worker.runner is None, f"{worker=} should not be started"
316
321
  worker.start()
322
+
323
+ loop = asyncio.get_event_loop()
317
324
  for sig in self.shutdown_on:
318
- signal.signal(sig, self.shutdown_handler)
325
+ loop.add_signal_handler(sig, self.shutdown_handler, sig)
319
326
 
320
- def shutdown_handler(self, sig, frame):
321
- sig_name = signal.Signals(sig).name
327
+ def shutdown_handler(self, sig: signal.Signals) -> None:
328
+ sig_name = sig.name
322
329
  graceful = not self.shutdown_requested
323
330
  logger.info("%s.shutdown_handler(%s) %s", self, sig_name, "GRACEFUL" if graceful else "FORCED")
324
331
  if not graceful:
@@ -1,6 +1,7 @@
1
1
  from abc import abstractmethod
2
2
  from asyncio import CancelledError
3
3
  from datetime import timedelta
4
+ from logging import getLogger
4
5
  from random import SystemRandom
5
6
 
6
7
  from asgiref.sync import sync_to_async
@@ -11,11 +12,12 @@ from django.db.models import F
11
12
  from django.db.models import Q
12
13
 
13
14
  from ..timezones import utcnow
14
- from . import logger
15
15
  from .engine import AbstractConsumer
16
16
  from .engine import AbstractProducer
17
17
  from .job import Job
18
18
 
19
+ logger = getLogger(__name__)
20
+
19
21
 
20
22
  def get_random_float(system_random=SystemRandom()): # noqa: B008
21
23
  return system_random.random()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csu
3
- Version: 2.2.3
3
+ Version: 2.2.5
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.2.3.svg
80
+ .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.5.svg
81
81
  :alt: Commits since latest release
82
- :target: https://github.com/ionelmc/python-csu/compare/v2.2.3...main
82
+ :target: https://github.com/ionelmc/python-csu/compare/v2.2.5...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:csu.worker.engine:ExampleConsumer(1/UNKNOWN) started.",
16
+ "INFO:csu.worker.engine:ExampleConsumer(2/UNKNOWN) started.",
17
+ "INFO:csu.worker.engine:ExampleConsumer(3/UNKNOWN) started.",
18
+ "INFO:csu.worker.engine:ExampleProducer(1/UNKNOWN) started.",
19
+ "INFO:csu.worker.engine:ExampleProducer(1/UNKNOWN).run() started.",
20
+ "PRODUCER fetch_task @ 1 RETURN",
21
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
22
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleEngine(4w/1q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=2))",
25
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleEngine(4w/2q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
28
+ "INFO:csu.worker.engine:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
29
+ "INFO:csu.worker.engine:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
30
+ "INFO:csu.worker.engine:ExampleConsumer(1/WORKING).run() - performing...",
31
+ "CONSUMER 0 perform_work JOB 1 START",
32
+ "INFO:csu.worker.engine:ExampleConsumer(2/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=2)",
33
+ "INFO:csu.worker.engine:ExampleConsumer(2/WORKING).run() - performing...",
34
+ "CONSUMER 1 perform_work JOB 2 START",
35
+ "INFO:csu.worker.engine:ExampleConsumer(3/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
36
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=4))",
46
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=4)",
49
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleConsumer(1/UNKNOWN) started.",
64
+ "INFO:csu.worker.engine:ExampleConsumer(2/UNKNOWN) started.",
65
+ "INFO:csu.worker.engine:ExampleConsumer(3/UNKNOWN) started.",
66
+ "INFO:csu.worker.engine:ExampleProducer(1/UNKNOWN) started.",
67
+ "INFO:csu.worker.engine:ExampleProducer(1/UNKNOWN).run() started.",
68
+ "PRODUCER fetch_task @ 1 RETURN",
69
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
70
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
73
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
79
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleConsumer(2/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
82
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleEngine(4w/0q).shutdown_handler(SIGINT) GRACEFUL",
102
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).stop(graceful=True)",
103
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) stats: {'ExampleConsumer': {'WORKING': 3}, 'ExampleProducer': {'WAITING': 1}, 'ExampleEngine': {'JOBS': 3, 'QSIZE': 0, 'SEMVAL': 0}}",
104
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) worker 1: <__main__.ExampleConsumer object at ",
105
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) worker 2: <__main__.ExampleConsumer object at ",
106
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) worker 3: <__main__.ExampleConsumer object at ",
107
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) worker 4: <__main__.ExampleProducer object at ",
108
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleProducer(1/WAITING).on_exit(",
112
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) flushing queue (0 items) and awaiting consumers...",
113
+ "INFO:csu.worker.engine:ExampleConsumer(1/SHUTDOWN).on_exit(",
114
+ "INFO:csu.worker.engine:ExampleConsumer(2/SHUTDOWN).on_exit(",
115
+ "INFO:csu.worker.engine:ExampleConsumer(3/SHUTDOWN).on_exit(",
116
+ "INFO:csu.worker.engine: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:csu.worker.engine:ExampleEngine(4w/0q).shutdown_handler(SIGINT) GRACEFUL",
135
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).stop(graceful=True)",
136
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) requesting workers to stop and awaiting 1 producers...",
137
+ "INFO:csu.worker.engine: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,36 +0,0 @@
1
- from asgiref.typing import ASGIReceiveCallable
2
- from asgiref.typing import ASGISendCallable
3
- from asgiref.typing import Scope
4
- from django.core.asgi import get_asgi_application
5
-
6
- from . import logger
7
-
8
-
9
- def get_worker_lifespan_application():
10
- django_application = get_asgi_application()
11
-
12
- async def application_with_worker_lifespan(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
13
- if scope["type"] == "lifespan":
14
- try:
15
- from .registry import REGISTRY # noqa:PLC0415
16
-
17
- while True:
18
- message = await receive()
19
- message_type = message["type"]
20
- if message_type == "lifespan.startup":
21
- logger.info("Found %s registered engines.", len(REGISTRY))
22
- for module_name, engine in REGISTRY.items():
23
- logger.info("Starting engine for %s: %s...", module_name, engine)
24
- await engine.start()
25
- await send({"type": "lifespan.startup.complete"})
26
- if message_type == "lifespan.shutdown":
27
- for engine in REGISTRY.values():
28
- await engine.stop()
29
- await send({"type": "lifespan.shutdown.complete"})
30
- return
31
- except Exception as exc:
32
- logger.exception("Failed setting up lifespan: %r", exc)
33
- else:
34
- await django_application(scope, receive, send)
35
-
36
- return application_with_worker_lifespan
@@ -1,164 +0,0 @@
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
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes