retsu 0.0.2__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.
retsu-0.0.2/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Ivan Ogasawara
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without modification,
7
+ are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice, this
13
+ list of conditions and the following disclaimer in the documentation and/or
14
+ other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from this
18
+ software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
23
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
24
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
25
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
27
+ OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
28
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29
+ OF THE POSSIBILITY OF SUCH DAMAGE.
retsu-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.1
2
+ Name: retsu
3
+ Version: 0.0.2
4
+ Summary: Retsu aims to wrap-up Celery in way to facilitate to create parallel and serial tasks
5
+ License: BSD 3 Clause
6
+ Author: Ivan Ogasawara
7
+ Author-email: ivan.ogasawara@gmail.com
8
+ Requires-Python: >=3.8.1,<4
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Provides-Extra: django
16
+ Requires-Dist: atpublic (>=4.0)
17
+ Requires-Dist: celery (>=5)
18
+ Requires-Dist: django (>=3) ; extra == "django"
19
+ Requires-Dist: redis (>=5)
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Retsu
23
+
24
+ Retsu aims to wrap-up Celery in way to facilitate to create parallel and serial
25
+ tasks
26
+
27
+ - Software License: BSD 3 Clause
28
+ - Documentation: https://osl-incubator.github.io/retsu
29
+
30
+ ## Features
31
+
32
+ TBD
33
+
34
+ ## Credits
35
+
36
+ This package was created with
37
+ [scicookie](https://github.com/osl-incubator/scicookie) project template.
38
+
retsu-0.0.2/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # Retsu
2
+
3
+ Retsu aims to wrap-up Celery in way to facilitate to create parallel and serial
4
+ tasks
5
+
6
+ - Software License: BSD 3 Clause
7
+ - Documentation: https://osl-incubator.github.io/retsu
8
+
9
+ ## Features
10
+
11
+ TBD
12
+
13
+ ## Credits
14
+
15
+ This package was created with
16
+ [scicookie](https://github.com/osl-incubator/scicookie) project template.
@@ -0,0 +1,114 @@
1
+ [tool.poetry]
2
+ name = "retsu"
3
+ version = "0.0.2" # semantic-release
4
+ description = "Retsu aims to wrap-up Celery in way to facilitate to create parallel and serial tasks"
5
+ readme = "README.md"
6
+ authors = ["Ivan Ogasawara <ivan.ogasawara@gmail.com>"]
7
+ packages = [
8
+ {include = "retsu", from="src"},
9
+ ]
10
+ license = "BSD 3 Clause"
11
+ exclude = [
12
+ ".git/*",
13
+ ".env*",
14
+ ]
15
+ include = ["src/retsu/py.typed"]
16
+
17
+
18
+ [tool.poetry.dependencies]
19
+ python = ">=3.8.1,<4"
20
+ atpublic = ">=4.0"
21
+ celery = ">=5"
22
+ redis = ">=5"
23
+ django = { version = ">=3", optional = true }
24
+
25
+ [tool.poetry.extras]
26
+ django = [
27
+ "django",
28
+ ]
29
+
30
+
31
+ [tool.poetry.group.dev.dependencies]
32
+ pytest = ">=7.3.2"
33
+ pytest-cov = ">=4.1.0"
34
+ coverage = ">=7.2.7"
35
+ pre-commit = ">=3.3.2"
36
+ ruff = ">=0.2.0"
37
+ mypy = ">=1.5.0"
38
+ bandit = ">=1.7.5"
39
+ vulture = ">=2.7"
40
+ mccabe = ">=0.6.1"
41
+ ipython = "<8"
42
+ ipykernel = ">=6.0.0"
43
+ Jinja2 = ">=3.1.2"
44
+ mkdocs = ">=1.4.3"
45
+ mkdocs-exclude = ">=1.0.2"
46
+ mkdocs-jupyter = ">=0.24.1"
47
+ mkdocs-literate-nav = ">=0.6.0"
48
+ mkdocs-macros-plugin = ">=0.7.0,<1"
49
+ mkdocs-material = ">=9.1.15"
50
+ mkdocstrings = ">=0.21.2"
51
+ mkdocstrings-python = ">=1.1.2"
52
+ mkdocs-gen-files = ">=0.5.0"
53
+ makim = "1.15.1"
54
+ # 'PosixPath' object has no attribute 'endswith'
55
+ virtualenv = "<=20.25.1"
56
+ flask = ">=3"
57
+ containers-sugar = "1.13.0"
58
+ compose-go = "2.27.0"
59
+ django = ">=3"
60
+ django-stubs = ">=3"
61
+
62
+ [tool.pytest.ini_options]
63
+ testpaths = [
64
+ "tests",
65
+ ]
66
+
67
+ [tool.bandit]
68
+ exclude_dirs = ["tests"]
69
+ targets = "./"
70
+
71
+ [tool.vulture]
72
+ exclude = ["tests"]
73
+ ignore_decorators = []
74
+ ignore_names = []
75
+ make_whitelist = true
76
+ min_confidence = 80
77
+ paths = ["./"]
78
+ sort_by_size = true
79
+ verbose = false
80
+
81
+ [tool.ruff]
82
+ line-length = 79
83
+ force-exclude = true
84
+ src = ["./"]
85
+ exclude = [
86
+ 'docs',
87
+ ]
88
+
89
+ [tool.ruff.lint]
90
+ select = [
91
+ "E", # pycodestyle
92
+ "F", # pyflakes
93
+ "D", # pydocstyle
94
+ "YTT", # flake8-2020
95
+ "RUF", # Ruff-specific rules
96
+ "I001", # isort
97
+ ]
98
+
99
+ [tool.ruff.lint.pydocstyle]
100
+ convention = "numpy"
101
+
102
+ [tool.ruff.lint.isort]
103
+ # Use a single line between direct and from import
104
+ lines-between-types = 1
105
+
106
+ [tool.mypy]
107
+ python_version = "3.8"
108
+ check_untyped_defs = true
109
+ strict = true
110
+ ignore_missing_imports = true
111
+ warn_unused_ignores = true
112
+ warn_redundant_casts = true
113
+ warn_unused_configs = true
114
+ exclude = ["example/", "scripts/"]
@@ -0,0 +1,35 @@
1
+ """Retsu."""
2
+
3
+ from importlib import metadata as importlib_metadata
4
+
5
+ from retsu.core import (
6
+ ParallelTask,
7
+ ResultTask,
8
+ SerialTask,
9
+ TaskManager,
10
+ )
11
+
12
+
13
+ def get_version() -> str:
14
+ """Return the program version."""
15
+ try:
16
+ return importlib_metadata.version(__name__)
17
+ except importlib_metadata.PackageNotFoundError: # pragma: no cover
18
+ return "0.0.2" # semantic-release
19
+
20
+
21
+ version = get_version()
22
+
23
+ __version__ = version
24
+ __author__ = "Ivan Ogasawara"
25
+ __email__ = "ivan.ogasawara@gmail.com"
26
+
27
+ __all__ = [
28
+ "__version__",
29
+ "__author__",
30
+ "__email__",
31
+ "ParallelTask",
32
+ "ResultTask",
33
+ "SerialTask",
34
+ "TaskManager",
35
+ ]
@@ -0,0 +1,67 @@
1
+ """Retsu tasks with celery."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import celery
8
+
9
+ from celery import chain, chord
10
+
11
+ from retsu.core import ParallelTask, SerialTask
12
+
13
+
14
+ class CeleryTask:
15
+ """Celery Task class."""
16
+
17
+ def task(self, *args, task_id: str, **kwargs) -> None: # type: ignore
18
+ """Define the task to be executed."""
19
+ chord_tasks, chord_callback = self.get_chord_tasks(
20
+ *args, task_id=task_id, **kwargs
21
+ )
22
+ chain_tasks = self.get_chain_tasks(*args, task_id=task_id, **kwargs)
23
+
24
+ if chord_tasks:
25
+ if chord_callback:
26
+ workflow_chord = chord(chord_tasks, chord_callback)
27
+ else:
28
+ workflow_chord = chord(chord_tasks)
29
+ workflow_chord.apply_async()
30
+
31
+ if chain_tasks:
32
+ workflow_chain = chain(chord_tasks)
33
+ workflow_chain.apply_async()
34
+
35
+ def get_chord_tasks( # type: ignore
36
+ self, *args, **kwargs
37
+ ) -> tuple[list[celery.Signature], Optional[celery.Signature]]:
38
+ """
39
+ Run tasks with chord.
40
+
41
+ Return
42
+ ------
43
+ tuple:
44
+ list of tasks for the chord, and the task to be used as a callback
45
+ """
46
+ chord_tasks: list[celery.Signature] = []
47
+ callback_task = None
48
+ return (chord_tasks, callback_task)
49
+
50
+ def get_chain_tasks( # type: ignore
51
+ self, *args, **kwargs
52
+ ) -> list[celery.Signature]:
53
+ """Run tasks with chain."""
54
+ chain_tasks: list[celery.Signature] = []
55
+ return chain_tasks
56
+
57
+
58
+ class ParallelCeleryTask(CeleryTask, ParallelTask):
59
+ """Parallel Task for Celery."""
60
+
61
+ ...
62
+
63
+
64
+ class SerialCeleryTask(CeleryTask, SerialTask):
65
+ """Serial Task for Celery."""
66
+
67
+ ...
@@ -0,0 +1,198 @@
1
+ """Retsu core classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import multiprocessing as mp
7
+ import warnings
8
+
9
+ from abc import abstractmethod
10
+ from pathlib import Path
11
+ from typing import Any, Optional, cast
12
+ from uuid import uuid4
13
+
14
+ from public import public
15
+
16
+
17
+ class ResultTask:
18
+ """Result from a task."""
19
+
20
+ def __init__(self, result_path: Path) -> None:
21
+ """Initialize ResultTask."""
22
+ self.result_path = result_path
23
+
24
+ @public
25
+ def save(self, task_id: str, result: Any) -> None:
26
+ """Save the result in a file."""
27
+ with open(self.result_path / f"{task_id}.json", "w") as f:
28
+ json.dump(
29
+ {"task_id": task_id, "result": result},
30
+ f,
31
+ indent=2,
32
+ )
33
+
34
+ @public
35
+ def load(self, task_id: str) -> dict[str, Any]:
36
+ """Load the result from a file."""
37
+ result_file = self.result_path / f"{task_id}.json"
38
+ if not result_file.exists():
39
+ raise Exception(f"File {result_file} doesn't exist.")
40
+ with open(result_file, "r") as f:
41
+ return cast(dict[str, Any], json.load(f))
42
+
43
+ @public
44
+ def status(self, task_id: str) -> bool:
45
+ """Return if the result for a given task was already stored."""
46
+ result_file = self.result_path / f"{task_id}.json"
47
+ return result_file.exists()
48
+
49
+ @public
50
+ def get(self, task_id: str) -> Any:
51
+ """Return the result for given task."""
52
+ if not self.status(task_id):
53
+ return {"status": False, "message": "Result not ready."}
54
+
55
+ return self.load(task_id)
56
+
57
+
58
+ @public
59
+ class Task:
60
+ """Main class for handling a task."""
61
+
62
+ def __init__(self, result_path: Path, workers: int = 1) -> None:
63
+ """Initialize a task object."""
64
+ self.active = True
65
+ self.workers = workers
66
+ self.result = ResultTask(result_path)
67
+ self.queue_in: mp.Queue[Any] = mp.Queue()
68
+ self.processes: list[mp.Process] = []
69
+
70
+ @public
71
+ def get_result(self, task_id: str) -> Any:
72
+ """Get the result for given task id."""
73
+ return self.result.get(task_id)
74
+
75
+ @public
76
+ def start(self) -> None:
77
+ """Start processes."""
78
+ for _ in range(self.workers):
79
+ p = mp.Process(target=self.run)
80
+ p.start()
81
+ self.processes.append(p)
82
+
83
+ @public
84
+ def stop(self) -> None:
85
+ """Stop processes."""
86
+ if not self.active:
87
+ return
88
+
89
+ self.active = False
90
+
91
+ for i in range(self.workers):
92
+ self.queue_in.put(None)
93
+
94
+ for i in range(self.workers):
95
+ p = self.processes[i]
96
+ p.join()
97
+
98
+ self.queue_in.close()
99
+ self.queue_in.join_thread()
100
+
101
+ @public
102
+ def request(self, *args, **kwargs) -> str: # type: ignore
103
+ """Feed the queue with data from the request for the task."""
104
+ key = uuid4().hex
105
+ print(
106
+ {
107
+ "task_id": key,
108
+ "args": args,
109
+ "kwargs": kwargs,
110
+ }
111
+ )
112
+ self.queue_in.put(
113
+ {
114
+ "task_id": key,
115
+ "args": args,
116
+ "kwargs": kwargs,
117
+ },
118
+ block=False,
119
+ )
120
+ return key
121
+
122
+ @abstractmethod
123
+ def task(self, *args, task_id: str, **kwargs) -> None: # type: ignore
124
+ """Define the task to be executed."""
125
+ raise Exception("`task` not implemented yet.")
126
+
127
+ def prepare_task(self, data: Any) -> None:
128
+ """Call the task with the necessary arguments."""
129
+ self.task(
130
+ *data["args"],
131
+ task_id=data["task_id"],
132
+ **data["kwargs"],
133
+ )
134
+
135
+ @public
136
+ def run(self) -> None:
137
+ """Run the task with data from the queue."""
138
+ while self.active:
139
+ data = self.queue_in.get()
140
+ if data is None:
141
+ print("Process terminated.")
142
+ self.active = False
143
+ return
144
+ self.prepare_task(data)
145
+
146
+
147
+ class SerialTask(Task):
148
+ """Serial Task class."""
149
+
150
+ def __init__(self, result_path: Path, workers: int = 1) -> None:
151
+ """Initialize a serial task object."""
152
+ if workers != 1:
153
+ warnings.warn(
154
+ "SerialTask should have just 1 worker. "
155
+ "Switching automatically to 1 ..."
156
+ )
157
+ workers = 1
158
+ super().__init__(result_path, workers=workers)
159
+
160
+
161
+ class ParallelTask(Task):
162
+ """Initialize a parallel task object."""
163
+
164
+ def __init__(self, result_path: Path, workers: int = 1) -> None:
165
+ """Initialize ParallelTask."""
166
+ if workers <= 1:
167
+ raise Exception("ParallelTask should have more than 1 worker.")
168
+
169
+ super().__init__(result_path, workers=workers)
170
+
171
+
172
+ class TaskManager:
173
+ """Manage tasks."""
174
+
175
+ tasks: dict[str, Task]
176
+
177
+ def __init__(self) -> None:
178
+ """Create a list of retsu tasks."""
179
+ self.tasks: dict[str, Task] = {}
180
+
181
+ @public
182
+ def get_task(self, name: str) -> Optional[Task]:
183
+ """Get a task with the given name."""
184
+ return self.tasks.get(name)
185
+
186
+ @public
187
+ def start(self) -> None:
188
+ """Start tasks."""
189
+ for task_name, task in self.tasks.items():
190
+ print(f"Task `{task_name}` is starting ...")
191
+ task.start()
192
+
193
+ @public
194
+ def stop(self) -> None:
195
+ """Stop tasks."""
196
+ for task_name, task in self.tasks.items():
197
+ print(f"Task `{task_name}` is stopping ...")
198
+ task.stop()
@@ -0,0 +1 @@
1
+ """Sub-package for plugins."""
@@ -0,0 +1,32 @@
1
+ """Plugin for integrating with django."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Type
6
+
7
+ from django.apps import AppConfig
8
+ from django.core.signals import request_finished
9
+
10
+ from retsu.core import TaskManager
11
+
12
+
13
+ def create_app_config(
14
+ manager: TaskManager, app_name: str = "myapp"
15
+ ) -> Type[AppConfig]:
16
+ """Create a django app config class."""
17
+
18
+ class RetsuAppConfig(AppConfig):
19
+ """RetsuAppConfig class."""
20
+
21
+ name = app_name
22
+
23
+ def ready(self) -> None:
24
+ """Start the task manager when the django app is ready."""
25
+ manager.start()
26
+ request_finished.connect(self.stop_multiprocessing)
27
+
28
+ def stop_multiprocessing(self, **kwargs) -> None: # type: ignore
29
+ assert kwargs is not None
30
+ manager.stop()
31
+
32
+ return RetsuAppConfig
File without changes