retsu 0.0.2__py3-none-any.whl
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/__init__.py +35 -0
- retsu/celery.py +67 -0
- retsu/core.py +198 -0
- retsu/plugins/__init__.py +1 -0
- retsu/plugins/django.py +32 -0
- retsu/py.typed +0 -0
- retsu-0.0.2.dist-info/LICENSE +29 -0
- retsu-0.0.2.dist-info/METADATA +38 -0
- retsu-0.0.2.dist-info/RECORD +10 -0
- retsu-0.0.2.dist-info/WHEEL +4 -0
retsu/__init__.py
ADDED
|
@@ -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
|
+
]
|
retsu/celery.py
ADDED
|
@@ -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
|
+
...
|
retsu/core.py
ADDED
|
@@ -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."""
|
retsu/plugins/django.py
ADDED
|
@@ -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
|
retsu/py.typed
ADDED
|
File without changes
|
|
@@ -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.
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
retsu/__init__.py,sha256=VadUF26rdNf4WofxcRRMHRLehGXMv9YTs8l21W8h8b4,666
|
|
2
|
+
retsu/celery.py,sha256=qcSaE5qSEn7Ot1LuIT4LF9xcIYBMh1cp0exDFzyh1dU,1755
|
|
3
|
+
retsu/core.py,sha256=5Tgx4OdmCUiJuWUu1cDvIpbMUE3oemlkM3k_aMw13vo,5494
|
|
4
|
+
retsu/plugins/__init__.py,sha256=17Lin0MWK9SmSXUsb78YSfSDDDsJFr668cvo0NRymEw,31
|
|
5
|
+
retsu/plugins/django.py,sha256=Wc3DYae5hWqiWXc1FDu_K5iNSE2hYdiE17WN7ToUlsU,827
|
|
6
|
+
retsu/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
retsu-0.0.2.dist-info/LICENSE,sha256=VEAHetrmHicHpjqcUpjtcmdx7owGyXaJMhDU95IlnU8,1514
|
|
8
|
+
retsu-0.0.2.dist-info/METADATA,sha256=I8TN6L624FKxQnrWAqJlLh3ZVVK9HK2SO84dwbGC1-Q,1075
|
|
9
|
+
retsu-0.0.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
10
|
+
retsu-0.0.2.dist-info/RECORD,,
|