py-alaska 0.1.0__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.
@@ -0,0 +1,238 @@
1
+ """
2
+ ╔══════════════════════════════════════════════════════════════════════════════╗
3
+ ║ ALASK v2.0 Project ║
4
+ ║ Task Signal - Process-Safe Signal Communication Module ║
5
+ ╠══════════════════════════════════════════════════════════════════════════════╣
6
+ ║ Version : 2.0.0 ║
7
+ ║ Date : 2026-01-30 ║
8
+ ╠══════════════════════════════════════════════════════════════════════════════╣
9
+ ║ Classes: ║
10
+ ║ - Signal : Dataclass representing a signal message ║
11
+ ║ - SignalBroker : Central broker managing queues and subscriptions ║
12
+ ║ - SignalClient : Client wrapper for signal emission and reception ║
13
+ ║ ║
14
+ ║ Functions: ║
15
+ ║ - _is_picklable : Check if an object can be pickled for IPC ║
16
+ ║ ║
17
+ ║ Notes (Thread/Process Mixed Mode): ║
18
+ ║ 1. Handlers cannot be passed between processes - register locally ║
19
+ ║ 2. Data must be picklable (no lambdas, sockets, file handles) ║
20
+ ║ 3. Process mode requires SyncManager ║
21
+ ║ 4. Thread-only mode uses mgr=None for lightweight operation ║
22
+ ╚══════════════════════════════════════════════════════════════════════════════╝
23
+ """
24
+
25
+ from __future__ import annotations
26
+ from typing import Any, Dict, List, Callable, Optional, TYPE_CHECKING
27
+ from dataclasses import dataclass, field
28
+ import threading
29
+ import queue
30
+ import time
31
+ import uuid
32
+ import pickle
33
+ import traceback
34
+
35
+ if TYPE_CHECKING:
36
+ from multiprocessing.managers import SyncManager
37
+
38
+
39
+ def _is_picklable(obj: Any) -> bool:
40
+ """객체가 pickle 가능한지 확인"""
41
+ try:
42
+ pickle.dumps(obj)
43
+ return True
44
+ except (pickle.PicklingError, TypeError, AttributeError):
45
+ return False
46
+
47
+
48
+ @dataclass
49
+ class Signal:
50
+ name: str
51
+ source: str
52
+ data: Any = None
53
+ signal_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
54
+ timestamp: float = field(default_factory=time.time)
55
+
56
+ def validate_for_ipc(self) -> bool:
57
+ """IPC(프로세스 간 통신)에 사용 가능한지 확인"""
58
+ return _is_picklable(self.data)
59
+
60
+
61
+ class SignalBroker:
62
+ """시그널 브로커 - Thread/Process 혼용 지원
63
+
64
+ mgr=None: 스레드 전용 모드 (경량, 빠름)
65
+ mgr=Manager(): 프로세스 간 통신 모드 (IPC)
66
+ """
67
+ __slots__ = ('_mgr', '_queues', '_subscribers', '_lock', '_mode')
68
+
69
+ def __init__(self, mgr: Optional[SyncManager] = None):
70
+ self._mgr = mgr
71
+ self._mode = 'process' if mgr else 'thread'
72
+ self._queues: Dict[str, Any] = {}
73
+ self._subscribers: Dict[str, List[str]] = {}
74
+ self._lock = threading.Lock()
75
+
76
+ def register(self, task_id: str) -> Any:
77
+ """태스크 큐 등록 (모드에 따라 적절한 큐 타입 생성)"""
78
+ queues = self._queues
79
+ with self._lock:
80
+ if task_id not in queues:
81
+ if self._mgr:
82
+ queues[task_id] = self._mgr.Queue()
83
+ else:
84
+ queues[task_id] = queue.Queue()
85
+ return queues[task_id]
86
+
87
+ @property
88
+ def mode(self) -> str:
89
+ """현재 모드 반환 ('thread' 또는 'process')"""
90
+ return self._mode
91
+
92
+ def unregister(self, task_id: str):
93
+ with self._lock:
94
+ self._queues.pop(task_id, None)
95
+ for subs in self._subscribers.values():
96
+ if task_id in subs:
97
+ subs.remove(task_id)
98
+
99
+ def subscribe(self, signal_name: str, task_id: str):
100
+ subs = self._subscribers
101
+ with self._lock:
102
+ if signal_name not in subs:
103
+ subs[signal_name] = []
104
+ lst = subs[signal_name]
105
+ if task_id not in lst:
106
+ lst.append(task_id)
107
+
108
+ def unsubscribe(self, signal_name: str, task_id: str):
109
+ with self._lock:
110
+ lst = self._subscribers.get(signal_name)
111
+ if lst and task_id in lst:
112
+ lst.remove(task_id)
113
+
114
+ def emit(self, signal: Signal, validate: bool = False) -> bool:
115
+ """시그널 발행
116
+
117
+ Args:
118
+ signal: 발행할 시그널
119
+ validate: True이면 IPC 모드에서 pickle 가능 여부 검증
120
+
121
+ Returns:
122
+ 성공 여부
123
+ """
124
+ if validate and self._mode == 'process' and not signal.validate_for_ipc():
125
+ return False
126
+
127
+ with self._lock:
128
+ lst = self._subscribers.get(signal.name)
129
+ subscribers = lst[:] if lst else []
130
+ queues = self._queues
131
+ q_map = {tid: queues.get(tid) for tid in subscribers}
132
+ data = signal.__dict__
133
+ sent = False
134
+ for tid, q in q_map.items():
135
+ if q:
136
+ try:
137
+ q.put(data, block=False)
138
+ sent = True
139
+ except queue.Full:
140
+ print(f"[SignalBroker] Queue full for task '{tid}', signal '{signal.name}' dropped")
141
+ except Exception as e:
142
+ print(f"[SignalBroker] emit error to '{tid}': {e}")
143
+ return sent
144
+
145
+ def emit_to(self, task_id: str, signal: Signal, validate: bool = False) -> bool:
146
+ """특정 태스크에 시그널 발행
147
+
148
+ Args:
149
+ task_id: 대상 태스크 ID
150
+ signal: 발행할 시그널
151
+ validate: True이면 IPC 모드에서 pickle 가능 여부 검증
152
+
153
+ Returns:
154
+ 성공 여부
155
+ """
156
+ if validate and self._mode == 'process' and not signal.validate_for_ipc():
157
+ return False
158
+
159
+ with self._lock:
160
+ q = self._queues.get(task_id)
161
+ if q:
162
+ try:
163
+ q.put(signal.__dict__, block=False)
164
+ return True
165
+ except queue.Full:
166
+ print(f"[SignalBroker] Queue full for task '{task_id}', signal '{signal.name}' dropped")
167
+ except Exception as e:
168
+ print(f"[SignalBroker] emit_to error '{task_id}': {e}")
169
+ return False
170
+
171
+ def get_subscribers(self, signal_name: str) -> List[str]:
172
+ with self._lock:
173
+ lst = self._subscribers.get(signal_name)
174
+ return lst[:] if lst else []
175
+
176
+
177
+ class SignalClient:
178
+ __slots__ = ('_broker', '_task_id', '_queue', '_handlers', '_running', '_thread')
179
+
180
+ def __init__(self, broker: SignalBroker, task_id: str):
181
+ self._broker = broker
182
+ self._task_id = task_id
183
+ self._queue = broker.register(task_id)
184
+ self._handlers: Dict[str, List[Callable]] = {}
185
+ self._running = False
186
+ self._thread = None
187
+
188
+ def start(self):
189
+ self._running = True
190
+ self._thread = threading.Thread(target=self._recv_loop, daemon=True)
191
+ self._thread.start()
192
+
193
+ def stop(self):
194
+ self._running = False
195
+ self._broker.unregister(self._task_id)
196
+
197
+ def _recv_loop(self):
198
+ q, handlers = self._queue, self._handlers
199
+ while self._running:
200
+ try:
201
+ data = q.get(timeout=0.5)
202
+ signal = Signal(**data) if isinstance(data, dict) else data
203
+ lst = handlers.get(signal.name)
204
+ if lst:
205
+ for handler in lst:
206
+ try:
207
+ handler(signal)
208
+ except Exception as e:
209
+ tb = traceback.format_exc()
210
+ print(f"[SignalClient:{self._task_id}] Handler error for '{signal.name}':\n{tb}")
211
+ except queue.Empty:
212
+ continue
213
+ except Exception as e:
214
+ print(f"[SignalClient:{self._task_id}] recv_loop error: {e}")
215
+
216
+ def on(self, signal_name: str, handler: Callable[[Signal], None]):
217
+ h = self._handlers
218
+ if signal_name not in h:
219
+ h[signal_name] = []
220
+ h[signal_name].append(handler)
221
+ self._broker.subscribe(signal_name, self._task_id)
222
+
223
+ def off(self, signal_name: str, handler: Callable[[Signal], None] = None):
224
+ lst = self._handlers.get(signal_name)
225
+ if lst:
226
+ if handler:
227
+ if handler in lst:
228
+ lst.remove(handler)
229
+ else:
230
+ lst.clear()
231
+ if not self._handlers.get(signal_name):
232
+ self._broker.unsubscribe(signal_name, self._task_id)
233
+
234
+ def emit(self, signal_name: str, data: Any = None):
235
+ self._broker.emit(Signal(name=signal_name, source=self._task_id, data=data))
236
+
237
+ def emit_to(self, task_id: str, signal_name: str, data: Any = None):
238
+ self._broker.emit_to(task_id, Signal(name=signal_name, source=self._task_id, data=data))
@@ -0,0 +1,263 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-alaska
3
+ Version: 0.1.0
4
+ Summary: ALASKA - Multiprocess Task Management Framework for Python
5
+ Author-email: DivisionVision <info@division.co.kr>
6
+ Maintainer-email: DivisionVision <info@division.co.kr>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/divisionvision/alaska
9
+ Project-URL: Documentation, https://github.com/divisionvision/alaska#readme
10
+ Project-URL: Repository, https://github.com/divisionvision/alaska.git
11
+ Project-URL: Issues, https://github.com/divisionvision/alaska/issues
12
+ Keywords: multiprocess,task,rmi,ipc,monitoring,shared-memory
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Distributed Computing
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: numpy>=1.20.0
28
+ Provides-Extra: monitor
29
+ Requires-Dist: psutil>=5.8.0; extra == "monitor"
30
+ Provides-Extra: camera
31
+ Requires-Dist: PySide6>=6.0.0; extra == "camera"
32
+ Provides-Extra: all
33
+ Requires-Dist: psutil>=5.8.0; extra == "all"
34
+ Requires-Dist: PySide6>=6.0.0; extra == "all"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
37
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
38
+ Requires-Dist: build>=1.0.0; extra == "dev"
39
+ Requires-Dist: twine>=4.0.0; extra == "dev"
40
+ Dynamic: license-file
41
+
42
+ # ALASKA
43
+
44
+ **A**dvanced **L**ightweight **A**synchronous **S**ervice **K**ernel for **A**pplications
45
+
46
+ [![PyPI version](https://badge.fury.io/py/py-alaska.svg)](https://badge.fury.io/py/py-alaska)
47
+ [![Python](https://img.shields.io/pypi/pyversions/alaska.svg)](https://pypi.org/project/py-alaska/)
48
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
49
+
50
+ A Python framework for building multiprocess task management systems with RMI (Remote Method Invocation), shared memory, and real-time monitoring.
51
+
52
+ ## Features
53
+
54
+ - **Multiprocess Task Management**: Run tasks as separate processes or threads
55
+ - **RMI (Remote Method Invocation)**: Call methods across processes seamlessly
56
+ - **Shared Memory (SmBlock)**: Zero-copy image/data sharing between processes
57
+ - **Signal/Broker Pattern**: Pub/sub messaging between tasks
58
+ - **Web Monitoring Dashboard**: Real-time HTTP-based monitoring UI
59
+ - **Performance Metrics**: IPC/FUNC timing statistics with sliding window
60
+ - **Auto-restart**: Automatic task recovery on failure
61
+ - **JSON Configuration**: Flexible configuration with injection support
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ # Basic installation
67
+ pip install py-alaska
68
+
69
+ # With monitoring support (psutil)
70
+ pip install py-alaska[monitor]
71
+
72
+ # With camera/GUI support (PySide6)
73
+ pip install py-alaska[camera]
74
+
75
+ # Full installation
76
+ pip install py-alaska[all]
77
+ ```
78
+
79
+ ## Quick Start
80
+
81
+ ### 1. Define a Task
82
+
83
+ ```python
84
+ from py_alaska import TaskClass
85
+
86
+ @TaskClass(name="my_task", mode="process", restart=True)
87
+ class MyTask:
88
+ def __init__(self):
89
+ self.task = None # Injected by framework
90
+ self.counter = 0
91
+
92
+ def increment(self, value: int) -> int:
93
+ """RMI method: can be called from other tasks"""
94
+ self.counter += value
95
+ return self.counter
96
+
97
+ def get_count(self) -> int:
98
+ """RMI method: query current count"""
99
+ return self.counter
100
+
101
+ def task_loop(self):
102
+ """Main loop: runs continuously"""
103
+ while not self.task.should_stop():
104
+ # Do work here
105
+ pass
106
+ ```
107
+
108
+ ### 2. Create Configuration (config.json)
109
+
110
+ ```json
111
+ {
112
+ "app_info": {
113
+ "name": "MyApp",
114
+ "version": "1.0.0",
115
+ "id": "myapp_001"
116
+ },
117
+ "task_config": {
118
+ "_monitor": {
119
+ "port": 7000,
120
+ "exit_hook": true
121
+ },
122
+ "worker/my_task": {
123
+ "counter": 0
124
+ }
125
+ }
126
+ }
127
+ ```
128
+
129
+ ### 3. Run the Application
130
+
131
+ ```python
132
+ from py_alaska import TaskManager, gconfig
133
+ import my_task # Import to register TaskClass
134
+
135
+ def main():
136
+ gconfig.load("config.json")
137
+
138
+ manager = TaskManager(gconfig)
139
+ manager.start_all()
140
+
141
+ # Access via RMI
142
+ worker = manager.get_client("worker")
143
+ result = worker.increment(10)
144
+ print(f"Counter: {result}")
145
+
146
+ # Web monitor at http://localhost:7000
147
+ import time
148
+ time.sleep(3600)
149
+
150
+ manager.stop_all()
151
+
152
+ if __name__ == "__main__":
153
+ main()
154
+ ```
155
+
156
+ ## Architecture
157
+
158
+ ```
159
+ ┌─────────────────────────────────────────────────────────────┐
160
+ │ TaskManager │
161
+ ├─────────────────────────────────────────────────────────────┤
162
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
163
+ │ │ Task A │ │ Task B │ │ Task C │ │
164
+ │ │ (Process) │ │ (Process) │ │ (Thread) │ │
165
+ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
166
+ │ │ │ │ │
167
+ │ └────────────────┼────────────────┘ │
168
+ │ │ │
169
+ │ ┌─────┴─────┐ │
170
+ │ │ RMI Bus │ │
171
+ │ │ (Queue) │ │
172
+ │ └─────┬─────┘ │
173
+ │ │ │
174
+ │ ┌─────┴─────┐ │
175
+ │ │ SmBlock │ │
176
+ │ │ (Shared) │ │
177
+ │ └───────────┘ │
178
+ ├─────────────────────────────────────────────────────────────┤
179
+ │ TaskMonitor │
180
+ │ HTTP :7000 │
181
+ └─────────────────────────────────────────────────────────────┘
182
+ ```
183
+
184
+ ## Core Components
185
+
186
+ | Component | Description |
187
+ |-----------|-------------|
188
+ | `TaskManager` | Main orchestrator for all tasks |
189
+ | `TaskClass` | Decorator to define a task |
190
+ | `RmiClient` | Client for calling remote methods |
191
+ | `SmBlock` | Shared memory block pool for zero-copy data sharing |
192
+ | `Signal/SignalBroker` | Pub/sub messaging system |
193
+ | `TaskMonitor` | HTTP-based web monitoring dashboard |
194
+ | `GConfig` | Global configuration management |
195
+
196
+ ## API Reference
197
+
198
+ ### TaskClass Decorator
199
+
200
+ ```python
201
+ @TaskClass(
202
+ name="task_name", # Unique task identifier
203
+ mode="process", # "process" or "thread"
204
+ restart=True, # Auto-restart on failure
205
+ restart_delay=3.0, # Delay before restart (seconds)
206
+ )
207
+ ```
208
+
209
+ ### RMI Methods
210
+
211
+ Any public method in a TaskClass becomes an RMI method:
212
+
213
+ ```python
214
+ # In Task A
215
+ def calculate(self, x: int, y: int) -> int:
216
+ return x + y
217
+
218
+ # From Task B or main process
219
+ client = manager.get_client("task_a")
220
+ result = client.calculate(10, 20) # Returns 30
221
+ ```
222
+
223
+ ### SmBlock (Shared Memory)
224
+
225
+ ```python
226
+ # Configuration
227
+ "_smblock": {
228
+ "image_pool": {"shape": [1024, 1024, 3], "maxsize": 100}
229
+ }
230
+
231
+ # In task
232
+ index = self.smblock.malloc() # Allocate block
233
+ image = self.smblock.get(index) # Get numpy array
234
+ image[:] = frame # Write data
235
+ self.smblock.mfree(index) # Release block
236
+ ```
237
+
238
+ ## Monitoring
239
+
240
+ Access the web dashboard at `http://localhost:7000` (configurable port).
241
+
242
+ **Features:**
243
+ - Real-time task status (alive/stopped)
244
+ - RMI call statistics (count, timing)
245
+ - CPU/Memory usage per task
246
+ - SmBlock pool utilization
247
+ - Configuration editor
248
+ - Performance metrics (IPC/FUNC time)
249
+
250
+ ## Requirements
251
+
252
+ - Python >= 3.8
253
+ - numpy >= 1.20.0
254
+ - psutil >= 5.8.0 (optional, for monitoring)
255
+ - PySide6 >= 6.0.0 (optional, for camera/GUI)
256
+
257
+ ## License
258
+
259
+ MIT License - see [LICENSE](LICENSE) for details.
260
+
261
+ ## Contributing
262
+
263
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,15 @@
1
+ py_alaska/SmBlock.py,sha256=pxnKHQmFqcFCZZQF-A__GBBKf4kus0VACdLfsA0Dm7M,10476
2
+ py_alaska/__init__.py,sha256=q4_ibJZWWvtUD7d2vt9RrsmViLTO55X_A_nDf_o3I6c,3355
3
+ py_alaska/div_logo.png,sha256=RKsuiBP7vFWBq1EEMrOEYkwuDzia3nuDMMDfKjzXUVI,26020
4
+ py_alaska/gconfig.py,sha256=6MCnGcjNNEJAjZRMjO06wx67B8D7NBzlJYS9JrXMkJA,43544
5
+ py_alaska/imi_camera.py,sha256=pPt6VTFqPD9U2YPABhalnsq9uFIV6iZkmJCvf8YQauM,15652
6
+ py_alaska/tab_camera.py,sha256=6Rj9n2eMp6LHXTZ8bm0eyoMLkOJDi2eLvPEVYevt5D0,31126
7
+ py_alaska/task_manager.py,sha256=gS6lY3A8g3Qhvk1maYaRPoQRpX-B9iXi_pJyuZP6wl0,28793
8
+ py_alaska/task_monitor.py,sha256=8cFy6i0NxK7hOCePJ4QpuN38hDNL_ey8mtAaClKuyn8,78213
9
+ py_alaska/task_performance.py,sha256=v11o2gOPt_rMKFIXe8qB7qCNRzILxCehvVytem6TWJs,22859
10
+ py_alaska/task_signal.py,sha256=EHlShzxKW6lgpBZ5CSL7W221-Ja9P0o1i5mxaj94-MY,9771
11
+ py_alaska-0.1.0.dist-info/licenses/LICENSE,sha256=wr6kC3HOkdD_s1IPebIBnK6ZnQyf7p-NtJrM_lK9LLk,1092
12
+ py_alaska-0.1.0.dist-info/METADATA,sha256=xD6vBFWynTtgv6nvm6DD8m-YPzgMdZUhfD0FrQNmGOI,9369
13
+ py_alaska-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ py_alaska-0.1.0.dist-info/top_level.txt,sha256=XIUr32XlSdVUarOxE-TaRvlKJitSLRJjqSsWRbw_wOg,10
15
+ py_alaska-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DivisionVision
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ py_alaska