actio 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.
actio-0.0.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Semenets V. Pavel
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.
actio-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: actio
3
+ Version: 0.0.2
4
+ Summary: Pure Python actor system for building concurrent and distributed applications with async/await support for Python 3.11+
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: Semenets V. Pavel
8
+ Author-email: p.semenets@gmail.com
9
+ Requires-Python: >=3.11
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Communications
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: System :: Hardware
22
+ Project-URL: Documentation, https://github.com/xDarkmanx/actio#readme
23
+ Project-URL: Homepage, https://github.com/xDarkmanx/actio
24
+ Project-URL: Repository, https://github.com/xDarkmanx/actio
25
+ Description-Content-Type: text/markdown
26
+
27
+ ### Actio Project
28
+
29
+ Pure Python actor system for building concurrent and distributed applications with async/await support.
30
+
31
+ ### Features
32
+
33
+ **Async/await native** - Built on Python 3.11+ asyncio
34
+
35
+ **Hierarchical actors** - Parent-child relationships with supervision
36
+
37
+ **Message routing** - Path-based message delivery between actors
38
+
39
+ **Lifecycle management** - Started/receive/stopped hooks with error handling
40
+
41
+ **Type safe** - Full type annotations with Pylance/MyPy support
42
+
43
+ ### Quick Start
44
+
45
+ ```python
46
+ import asyncio
47
+ from actio import Actor, ActorSystem, actio
48
+
49
+ @actio()
50
+ class MyActor(Actor):
51
+ async def receive(self, sender, message):
52
+ print(f"Received: {message}")
53
+
54
+ async def main():
55
+ system = ActorSystem()
56
+ actor_ref = system.create(MyActor())
57
+
58
+ system.tell(actor_ref, "Hello World!")
59
+ await asyncio.sleep(0.1)
60
+ system.shutdown()
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ ### License
66
+ MIT License - see LICENSE file for details.
67
+
actio-0.0.2/README.md ADDED
@@ -0,0 +1,40 @@
1
+ ### Actio Project
2
+
3
+ Pure Python actor system for building concurrent and distributed applications with async/await support.
4
+
5
+ ### Features
6
+
7
+ **Async/await native** - Built on Python 3.11+ asyncio
8
+
9
+ **Hierarchical actors** - Parent-child relationships with supervision
10
+
11
+ **Message routing** - Path-based message delivery between actors
12
+
13
+ **Lifecycle management** - Started/receive/stopped hooks with error handling
14
+
15
+ **Type safe** - Full type annotations with Pylance/MyPy support
16
+
17
+ ### Quick Start
18
+
19
+ ```python
20
+ import asyncio
21
+ from actio import Actor, ActorSystem, actio
22
+
23
+ @actio()
24
+ class MyActor(Actor):
25
+ async def receive(self, sender, message):
26
+ print(f"Received: {message}")
27
+
28
+ async def main():
29
+ system = ActorSystem()
30
+ actor_ref = system.create(MyActor())
31
+
32
+ system.tell(actor_ref, "Hello World!")
33
+ await asyncio.sleep(0.1)
34
+ system.shutdown()
35
+
36
+ asyncio.run(main())
37
+ ```
38
+
39
+ ### License
40
+ MIT License - see LICENSE file for details.
@@ -0,0 +1,52 @@
1
+ [tool.poetry]
2
+ name = "actio"
3
+ version = "0.0.2"
4
+ description = "Pure Python actor system for building concurrent and distributed applications with async/await support for Python 3.11+"
5
+ authors = ["Semenets V. Pavel <p.semenets@gmail.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ packages = [{ include = "actio", from = "src" }]
10
+ homepage = "https://github.com/xDarkmanx/actio"
11
+ repository = "https://github.com/xDarkmanx/actio"
12
+ documentation = "https://github.com/xDarkmanx/actio#readme"
13
+
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Libraries",
24
+ "Topic :: System :: Hardware",
25
+ "Topic :: Communications",
26
+ ]
27
+
28
+ [tool.poetry.dependencies]
29
+ python = ">=3.11"
30
+
31
+ [tool.poetry.group.lint.dependencies]
32
+ flake8 = "^7.3.0"
33
+
34
+ [tool.poetry.group.security.dependencies]
35
+ bandit = "^1.8.6"
36
+
37
+ [tool.poetry.group.test.dependencies]
38
+ pytest = ">=8.4.2"
39
+ pytest-asyncio = ">=1.2.0"
40
+ pytest-cov = ">=4.1.0"
41
+ pytest-mock = ">=3.11.0"
42
+ anyio = ">=3.7.0"
43
+
44
+ [tool.flake8]
45
+ max-line-length = 127
46
+ max-complexity = 25
47
+ ignore = "E203,W503"
48
+ exclude = [".git", "__pycache__", ".venv", "build", "dist"]
49
+
50
+ [build-system]
51
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
52
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,40 @@
1
+ # actio/__init__.py
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from .ref import ActorRef
5
+ from .ref import ActorDefinition
6
+
7
+ from .messages import PoisonPill
8
+ from .messages import DeadLetter
9
+ from .messages import Terminated
10
+
11
+ from .actor import Actor
12
+ from .actor import ActorSystem
13
+
14
+ from .registry import ActorRegistry
15
+ from .registry import registry
16
+ from .registry import actio
17
+
18
+ __version__ = "0.0.2"
19
+ __author__ = "Semenets V. Pavel"
20
+ __license__ = "MIT"
21
+
22
+ __all__ = [
23
+ # from ref.py
24
+ 'ActorRef',
25
+ 'ActorDefinition',
26
+
27
+ # from messages.py
28
+ 'PoisonPill',
29
+ 'DeadLetter',
30
+ 'Terminated',
31
+
32
+ # from actor.py
33
+ 'Actor',
34
+ 'ActorSystem',
35
+
36
+ # from registry.py
37
+ 'ActorRegistry',
38
+ 'registry',
39
+ 'actio'
40
+ ]
@@ -0,0 +1,522 @@
1
+ # actio/actor.py
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from typing import Union
5
+ from typing import Set
6
+ from typing import Dict
7
+ from typing import List
8
+ from typing import Any
9
+ from typing import Optional
10
+
11
+ import asyncio
12
+ import logging
13
+
14
+ from uuid import uuid4
15
+
16
+ from . import PoisonPill
17
+ from . import DeadLetter
18
+ from . import Terminated
19
+
20
+ from . import ActorRef
21
+
22
+ log = logging.getLogger('actio.actor')
23
+
24
+
25
+ class Actor:
26
+ def __init__(self):
27
+ self._context: Optional['_ActorContext'] = None
28
+
29
+ async def receive(self, sender: ActorRef, message: Any) -> None:
30
+ """
31
+ Обрабатывает входящие сообщения.
32
+
33
+ Этот метод должен быть переопределён в подклассах.
34
+ """
35
+ raise NotImplementedError
36
+
37
+ # --- Вспомогательные методы для работы с детьми и системой ---
38
+
39
+ def create(self, actor: 'Actor', name: Optional[str] = None) -> ActorRef:
40
+ """Создаёт дочерний актор."""
41
+ child_actor_ref = self.system._create(actor=actor, parent=self.actor_ref, name=name)
42
+ self.watch(child_actor_ref)
43
+ return child_actor_ref
44
+
45
+ def tell(self, actor: Union['Actor', ActorRef], message: Any) -> None:
46
+ """Отправляет сообщение другому актору."""
47
+ self.system._tell(actor=actor, message=message, sender=self.actor_ref)
48
+
49
+ def schedule_tell(
50
+ self,
51
+ actor: Union['Actor', ActorRef],
52
+ message: Any,
53
+ *,
54
+ delay: Union[None, int, float] = None,
55
+ period: Union[None, int, float] = None,
56
+ ) -> asyncio.Task:
57
+ """Отправляет сообщение с задержкой или периодически."""
58
+ return self.system._schedule_tell(
59
+ actor=actor,
60
+ message=message,
61
+ sender=self.actor_ref,
62
+ delay=delay,
63
+ period=period
64
+ )
65
+
66
+ def watch(self, actor: ActorRef) -> None:
67
+ """Наблюдает за другим актором."""
68
+ self.system._watch(actor=self.actor_ref, other=actor)
69
+
70
+ def unwatch(self, actor: ActorRef) -> None:
71
+ """Прекращает наблюдение за другим актором."""
72
+ self.system._unwatch(actor=self.actor_ref, other=actor)
73
+
74
+ def stop(self) -> None:
75
+ """Останавливает этот актор."""
76
+ self.system.stop(actor=self.actor_ref)
77
+
78
+ # --- Хуки жизненного цикла ---
79
+ async def started(self) -> None:
80
+ """Вызывается при запуске актора."""
81
+ pass
82
+
83
+ async def restarted(self, sender: ActorRef, message: Any, error: Exception) -> None:
84
+ """Вызывается при ошибке в `receive`."""
85
+ log.exception(
86
+ '%s failed to receive message %s from %s',
87
+ self.actor_ref, message, sender, exc_info=error
88
+ )
89
+
90
+ async def stopped(self) -> None:
91
+ """Вызывается при остановке актора."""
92
+ pass
93
+
94
+ # --- Свойства для доступа к контексту ---
95
+ @property
96
+ def actor_ref(self) -> ActorRef:
97
+ assert self._context is not None, "Actor context not initialized"
98
+ return self._context.actor_ref
99
+
100
+ @property
101
+ def system(self) -> 'ActorSystem':
102
+ assert self._context is not None, "Actor context not initialized"
103
+ return self._context.system
104
+
105
+ @property
106
+ def parent(self) -> Optional[ActorRef]:
107
+ assert self._context is not None, "Actor context not initialized"
108
+ return self._context.parent
109
+
110
+ @property
111
+ def name(self) -> str:
112
+ return self.actor_ref.name
113
+
114
+ @property
115
+ def path(self) -> str:
116
+ return self.actor_ref.path
117
+
118
+ def __str__(self):
119
+ return self.path
120
+
121
+ def __repr__(self):
122
+ return self.path
123
+
124
+ # --- Вспомогательные методы для маршрутизации ---
125
+
126
+ async def _route_message_logic(self, sender: ActorRef, message: Dict[str, Any]) -> bool:
127
+ """
128
+ Встроенная логика маршрутизации для сообщений с действием 'route_message'.
129
+
130
+ :param sender: Отправитель сообщения.
131
+ :param message: Сообщение.
132
+ :return: True, если сообщение было обработано как маршрутное, иначе False.
133
+ """
134
+ action = message.get('action')
135
+ if action != 'route_message':
136
+ return False
137
+
138
+ destination: str = message.get('destination', '')
139
+ data = message.get('data')
140
+
141
+ # 1. Если destination пустой, это сообщение для текущего актора
142
+ if not destination:
143
+ # Передаём данные для обработки
144
+ # Создаём новое сообщение, чтобы не мутировать оригинальное
145
+ final_message = data if isinstance(data, dict) else {'data': data}
146
+ # Добавляем информацию о маршруте в финальное сообщение, если нужно
147
+ final_message['source'] = message.get('source')
148
+ await self.receive(sender, final_message)
149
+ return True
150
+
151
+ # 2. Разбираем путь
152
+ path_parts = [p for p in destination.split('/') if p]
153
+ if not path_parts:
154
+ # destination был "/", что тоже означает "этот актор"
155
+ final_message = data if isinstance(data, dict) else {'data': data}
156
+ await self.receive(sender, final_message)
157
+ return True
158
+
159
+ first_part = path_parts[0]
160
+ remaining_path = "/".join(path_parts[1:]) if len(path_parts) > 1 else None
161
+
162
+ # 3. Проверяем, является ли первая часть именем нашего прямого потомка
163
+ # Предполагаем, что у актора есть словарь потомков self.actors (как в примерах)
164
+ # Если такого атрибута нет, ищем в _context.children
165
+ target_child_ref: Optional[ActorRef] = None
166
+ child_actors_dict = getattr(self, 'actors', None)
167
+ if child_actors_dict and isinstance(child_actors_dict, dict):
168
+ target_child_ref = child_actors_dict.get(first_part)
169
+ else:
170
+ # Поиск по имени в списке детей из контекста
171
+ assert self._context is not None, "Actor context not initialized"
172
+ for child_ref in self._context.children:
173
+ if child_ref.name == first_part:
174
+ target_child_ref = child_ref
175
+ break
176
+
177
+ if target_child_ref:
178
+ # Нашли потомка, пересылаем ему.
179
+ # Формируем новое сообщение с обновлённым destination
180
+ forwarded_message = message.copy()
181
+ forwarded_message['destination'] = remaining_path
182
+ # Обновляем source: добавляем себя
183
+ current_source = forwarded_message.get('source', '')
184
+ # forwarded_message['source'] = f"{self.actor_ref.name}/{current_source}".strip('/')
185
+
186
+ log.debug(
187
+ f"[{self.actor_ref.path}] Routing to child '{first_part}' "
188
+ f"with new destination '{remaining_path}' and updated source "
189
+ f"'{forwarded_message['source']}'.")
190
+ self.tell(target_child_ref, forwarded_message)
191
+ else:
192
+ # Не нашли потомка, пересылаем родителю.
193
+ if self.parent:
194
+ # Формируем новое сообщение с обновлённым source
195
+ forwarded_message = message.copy()
196
+ # Обновляем source: добавляем себя
197
+ current_source = forwarded_message.get('source', '')
198
+ forwarded_message['source'] = f"{self.actor_ref.name}/{current_source}".strip('/')
199
+
200
+ log.debug(
201
+ f"[{self.actor_ref.path}] Forwarding to parent with "
202
+ f"updated source '{forwarded_message['source']}'.")
203
+ self.tell(self.parent, forwarded_message)
204
+ else:
205
+ # Мы корневой актор и не можем маршрутизировать дальше
206
+ log.warning(
207
+ f"[{self.actor_ref.path}] Cannot route message, "
208
+ f"no parent and '{first_part}' not found among children. Message: {message}"
209
+ )
210
+ # Можно отправить DeadLetter отправителю или в специальный актор
211
+ # self.tell(sender, DeadLetter(...))
212
+
213
+ return True # Сообщение было обработано как маршрутное
214
+
215
+
216
+ class ActorSystem:
217
+ """Система акторов."""
218
+
219
+ def __init__(self):
220
+ self._actors: Dict[ActorRef, '_ActorContext'] = {}
221
+ self._is_stopped = asyncio.Event()
222
+ self.children: List[ActorRef] = [] # Корневые акторы
223
+
224
+ def create(self, actor: Actor, name: Optional[str] = None) -> ActorRef:
225
+ """Создаёт корневой актор."""
226
+ return self._create(actor=actor, parent=None, name=name)
227
+
228
+ def tell(self, actor: Union[Actor, ActorRef], message: Any) -> None:
229
+ """Отправляет сообщение актору."""
230
+ self._tell(actor=actor, message=message, sender=None)
231
+
232
+ def schedule_tell(
233
+ self,
234
+ actor: Union[Actor, ActorRef],
235
+ message: Any,
236
+ *,
237
+ delay: Union[None, int, float] = None,
238
+ period: Union[None, int, float] = None
239
+ ) -> asyncio.Task:
240
+ """Отправляет сообщение с задержкой или периодически."""
241
+ return self._schedule_tell(actor=actor, message=message, sender=None, delay=delay, period=period)
242
+
243
+ def stop(self, actor: Union[Actor, ActorRef]) -> None:
244
+ """Останавливает актор."""
245
+ self._tell(actor=actor, message=PoisonPill(), sender=None)
246
+
247
+ def shutdown(self, timeout: Union[None, int, float] = None) -> None:
248
+ """Завершает работу всей системы акторов."""
249
+ asyncio.create_task(self._shutdown(timeout=timeout))
250
+
251
+ def stopped(self):
252
+ """Ожидает завершения работы системы."""
253
+ return self._is_stopped.wait()
254
+
255
+ # --- Внутренние методы ---
256
+ def _create(self, actor: Actor, *, parent: Union[None, Actor, ActorRef], name: Optional[str] = None) -> ActorRef:
257
+ """Внутренний метод создания актора."""
258
+ if not isinstance(actor, Actor):
259
+ raise ValueError(f'Not an actor: {actor}')
260
+
261
+ parent_ctx: Optional[_ActorContext] = None
262
+ if parent:
263
+ parent = self._validate_actor_ref(parent)
264
+ parent_ctx = self._actors[parent]
265
+ child_idx = len(parent_ctx.children) + 1
266
+ else:
267
+ child_idx = len(self.children) + 1
268
+
269
+ if not name:
270
+ name = f'{type(actor).__name__}-{child_idx}'
271
+
272
+ if parent:
273
+ path = f'{parent.path}/{name}'
274
+ else:
275
+ path = name
276
+
277
+ actor_id = str(uuid4().hex)
278
+ actor_ref = ActorRef(actor_id=actor_id, path=path, name=name)
279
+ actor_ctx = _ActorContext(self, actor_ref, parent)
280
+
281
+ actor_ctx.lifecycle = asyncio.get_event_loop().create_task(
282
+ self._actor_lifecycle_loop(actor, actor_ref, actor_ctx)
283
+ )
284
+
285
+ actor._context = actor_ctx
286
+ self._actors[actor_ref] = actor_ctx
287
+
288
+ if parent and parent_ctx:
289
+ parent_ctx.children.append(actor_ref)
290
+
291
+ else:
292
+ self.children.append(actor_ref)
293
+
294
+ return actor_ref
295
+
296
+ def _tell(self, actor: Union[Actor, ActorRef], message: Any, *, sender: Union[None, Actor, ActorRef]) -> None:
297
+ """Внутренний метод отправки сообщения."""
298
+ actor = self._validate_actor_ref(actor)
299
+
300
+ if sender:
301
+ sender = self._validate_actor_ref(sender)
302
+
303
+ if sender not in self._actors:
304
+ raise ValueError(f'Sender does not exist: {sender}')
305
+
306
+ if actor in self._actors:
307
+ actor_ctx = self._actors[actor]
308
+ actor_ctx.letterbox.put_nowait((sender, message))
309
+
310
+ elif sender:
311
+ deadletter = DeadLetter(actor=actor, message=message)
312
+ self._tell(sender, deadletter, sender=None)
313
+
314
+ else:
315
+ log.warning(f'Failed to deliver message {message} to {actor}')
316
+
317
+ def _schedule_tell(
318
+ self,
319
+ actor: Union[Actor, ActorRef],
320
+ message: Any,
321
+ *,
322
+ sender: Union[None, Actor, ActorRef],
323
+ delay: Union[None, int, float] = None,
324
+ period: Union[None, int, float] = None
325
+ ) -> asyncio.Task:
326
+ """Внутренний метод планирования отправки сообщения."""
327
+ if not delay:
328
+ if not period:
329
+ raise ValueError('Cannot schedule message without delay and period')
330
+ self._tell(actor=actor, message=message, sender=sender)
331
+ delay = period
332
+
333
+ ts = asyncio.get_event_loop().time() + delay
334
+ return asyncio.create_task(self._schedule_tell_loop(actor=actor, message=message, sender=sender, ts=ts, period=period))
335
+
336
+ async def _schedule_tell_loop(
337
+ self,
338
+ actor: Union[Actor, ActorRef],
339
+ message: Any,
340
+ *,
341
+ sender: Union[None, Actor, ActorRef],
342
+ ts: Union[None, int, float] = None,
343
+ period: Union[None, int, float] = None
344
+ ) -> None:
345
+ """Цикл для периодической отправки сообщения."""
346
+ actor = self._validate_actor_ref(actor)
347
+ delay = (ts - asyncio.get_event_loop().time()) if ts else 0
348
+ while True:
349
+ if delay > 0:
350
+ await asyncio.sleep(delay)
351
+
352
+ self._tell(actor=actor, message=message, sender=sender)
353
+ if not period:
354
+ break
355
+
356
+ delay = period
357
+
358
+ def _watch(self, actor: Union[Actor, ActorRef], other: Union[Actor, ActorRef]) -> None:
359
+ """Внутренний метод для наблюдения за актором."""
360
+ actor = self._validate_actor_ref(actor)
361
+
362
+ if actor not in self._actors:
363
+ raise ValueError(f'Actor does not exist: {actor}')
364
+
365
+ other = self._validate_actor_ref(other)
366
+ if other not in self._actors:
367
+ raise ValueError(f'Actor does not exist: {other}')
368
+
369
+ if actor == other:
370
+ raise ValueError(f'Actor cannot watch themselves: {actor}')
371
+
372
+ actor_ctx = self._actors[actor]
373
+ other_ctx = self._actors[other]
374
+ actor_ctx.watching.add(other)
375
+ other_ctx.watched_by.add(actor)
376
+
377
+ def _unwatch(self, actor: Union[Actor, ActorRef], other: Union[Actor, ActorRef]) -> None:
378
+ """Внутренний метод для прекращения наблюдения за актором."""
379
+ actor = self._validate_actor_ref(actor)
380
+ if actor not in self._actors:
381
+ raise ValueError(f'Actor does not exist: {actor}')
382
+
383
+ other = self._validate_actor_ref(other)
384
+ if actor == other:
385
+ raise ValueError(f'Actor cannot unwatch themselves: {actor}')
386
+
387
+ actor_ctx = self._actors[actor]
388
+ if other in actor_ctx.watching:
389
+ actor_ctx.watching.remove(other)
390
+
391
+ if other in self._actors:
392
+ other_ctx = self._actors[other]
393
+ if actor in other_ctx.watched_by:
394
+ other_ctx.watched_by.remove(actor)
395
+
396
+ async def _shutdown(self, timeout: Union[None, int, float] = None) -> None:
397
+ """Внутренний метод завершения работы системы."""
398
+ if self._actors:
399
+ for actor_ref in self.children: # children propagate stop messages
400
+ self.stop(actor_ref)
401
+
402
+ lifecycle_tasks = [
403
+ task for task
404
+ in (actor_ctx.lifecycle for actor_ctx in self._actors.values())
405
+ if task is not None
406
+ ]
407
+
408
+ done, pending = await asyncio.wait(lifecycle_tasks, timeout=timeout)
409
+ for lifecycle_task in pending:
410
+ lifecycle_task.cancel()
411
+
412
+ self._is_stopped.set()
413
+
414
+ async def _actor_lifecycle_loop(self, actor: Actor, actor_ref: ActorRef, actor_ctx: '_ActorContext') -> None:
415
+ """Основной цикл жизни актора."""
416
+ try:
417
+ await actor.started()
418
+ actor_ctx.receiving_messages = True
419
+
420
+ except Exception as e:
421
+ log.exception('Exception raised while awaiting start of %s', actor_ref, exc_info=e)
422
+
423
+ while actor_ctx.receiving_messages:
424
+ sender, message = await actor_ctx.letterbox.get()
425
+ if isinstance(message, PoisonPill):
426
+ break
427
+
428
+ # --- Логика маршрутизации ---
429
+ # Проверяем, является ли сообщение маршрутизируемым.
430
+ # Это делается до вызова пользовательского `receive`.
431
+ message_handled_by_routing = False
432
+ if isinstance(message, dict) and message.get('action') == 'route_message':
433
+ try:
434
+ # Даем актору возможность обработать маршрутизацию самостоятельно
435
+ # (например, если у него своя логика для 'route_message')
436
+ # Если метод _route_message_logic возвращает True, значит он обработал.
437
+ # Мы предполагаем, что базовый Actor имеет этот метод.
438
+
439
+ if hasattr(actor, '_route_message_logic'):
440
+ # Вызываем напрямую, так как это вспомогательный метод
441
+ message_handled_by_routing = await actor._route_message_logic(sender, message)
442
+ except Exception as e:
443
+ log.error(f"Error in _route_message_logic for {actor_ref.path}: {e}")
444
+
445
+ # Если маршрутизация не обработала сообщение, передаём его в receive актора
446
+ if not message_handled_by_routing:
447
+ try:
448
+ await actor.receive(sender, message)
449
+ except Exception as e:
450
+ try:
451
+ await actor.restarted(sender, message, e)
452
+ except Exception as e:
453
+ log.exception(f'Exception raised while awaiting restart of {actor_ref}', exc_info=e)
454
+ # --- Конец логики маршрутизации ---
455
+
456
+ actor_ctx.receiving_messages = False
457
+
458
+ children_stopping = []
459
+ for child in actor_ctx.children:
460
+ child_ctx = self._actors[child]
461
+ children_stopping.append(child_ctx.is_stopped.wait())
462
+ self.stop(child)
463
+
464
+ if children_stopping:
465
+ await asyncio.wait(children_stopping)
466
+
467
+ try:
468
+ await actor.stopped()
469
+
470
+ except Exception as e:
471
+ log.exception(f'Exception raised while awaiting stop of {actor_ref}', exc_info=e)
472
+
473
+ for other in actor_ctx.watched_by:
474
+ self._tell(other, Terminated(actor_ref), sender=None)
475
+ other_ctx = self._actors[other]
476
+ other_ctx.watching.remove(actor_ref)
477
+
478
+ for other in actor_ctx.watching:
479
+ other_ctx = self._actors[other]
480
+ other_ctx.watched_by.remove(actor_ref)
481
+
482
+ if actor_ctx.parent:
483
+ parent_ctx = self._actors[actor_ctx.parent]
484
+ parent_ctx.children.remove(actor_ref)
485
+ else:
486
+ self.children.remove(actor_ref)
487
+
488
+ while not actor_ctx.letterbox.empty():
489
+ sender, message = actor_ctx.letterbox.get_nowait()
490
+ if sender and sender != actor_ref:
491
+ deadletter = DeadLetter(actor_ref, message)
492
+ self._tell(sender, deadletter, sender=None)
493
+
494
+ actor_ctx.is_stopped.set()
495
+ del self._actors[actor_ref]
496
+
497
+ @staticmethod
498
+ def _validate_actor_ref(actor: Union[Actor, ActorRef]) -> ActorRef:
499
+ """Проверяет и возвращает ActorRef."""
500
+ if isinstance(actor, Actor):
501
+ actor = actor.actor_ref
502
+
503
+ if not isinstance(actor, ActorRef):
504
+ raise ValueError(f'Not an actor: {actor}')
505
+
506
+ return actor
507
+
508
+
509
+ class _ActorContext:
510
+ """Контекст выполнения актора."""
511
+
512
+ def __init__(self, system: ActorSystem, actor_ref: ActorRef, parent: Optional[ActorRef]):
513
+ self.system: ActorSystem = system
514
+ self.actor_ref: ActorRef = actor_ref
515
+ self.parent: Optional[ActorRef] = parent
516
+ self.letterbox: asyncio.Queue = asyncio.Queue()
517
+ self.lifecycle: Optional[asyncio.Task] = None
518
+ self.watching: Set[ActorRef] = set()
519
+ self.watched_by: Set[ActorRef] = set()
520
+ self.children: List[ActorRef] = []
521
+ self.is_stopped: asyncio.Event = asyncio.Event()
522
+ self.receiving_messages: bool = False
@@ -0,0 +1,23 @@
1
+ # actio/messages.py
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from typing import Any
5
+ from dataclasses import dataclass
6
+
7
+ from . import ActorRef
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class PoisonPill:
12
+ pass
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class DeadLetter:
17
+ actor: ActorRef
18
+ message: Any
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Terminated:
23
+ actor: ActorRef
@@ -0,0 +1,38 @@
1
+ # actio/ref.py
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from typing import Dict
5
+ from typing import Any
6
+ from typing import Optional
7
+
8
+ from dataclasses import dataclass
9
+ from dataclasses import field
10
+
11
+
12
+ @dataclass(repr=False, eq=False, frozen=True)
13
+ class ActorRef:
14
+ actor_id: str
15
+ path: str
16
+ name: str
17
+
18
+ def __str__(self):
19
+ return self.path
20
+
21
+ def __hash__(self):
22
+ return hash(self.actor_id)
23
+
24
+ def __repr__(self):
25
+ return self.path
26
+
27
+ def __eq__(self, other):
28
+ return isinstance(other, ActorRef) and self.actor_id == other.actor_id
29
+
30
+
31
+ @dataclass
32
+ class ActorDefinition:
33
+ name: str
34
+ cls: type
35
+ parent: Optional[str]
36
+ replicas: int = 1
37
+ minimal: int = 1
38
+ config: Dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,130 @@
1
+ # actio/registry.py
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import logging
5
+ import asyncio
6
+
7
+ from typing import Dict
8
+ from typing import Any
9
+ from typing import Optional
10
+ from typing import List
11
+
12
+ from . import ActorRef
13
+ from . import ActorDefinition
14
+
15
+ from . import ActorSystem
16
+
17
+ log = logging.getLogger('actio.registry')
18
+
19
+
20
+ class ActorRegistry:
21
+ def __init__(self):
22
+ self._definitions: Dict[str, ActorDefinition] = {}
23
+
24
+ def actio(
25
+ self,
26
+ name: Optional[str] = None,
27
+ parent: Optional[str] = None,
28
+ replicas: int = 1,
29
+ minimal: int = 1,
30
+ config: Optional[Dict[str, Any]] = None
31
+ ):
32
+
33
+ def decorator(cls):
34
+ actor_name = name or cls.__name__
35
+ self._definitions[actor_name] = ActorDefinition(
36
+ name=actor_name,
37
+ cls=cls,
38
+ parent=parent,
39
+ replicas=replicas,
40
+ minimal=minimal,
41
+ config=config or {}
42
+ )
43
+ return cls
44
+ return decorator
45
+
46
+ async def build_actor_tree(
47
+ self,
48
+ system: ActorSystem,
49
+ root_name: str = 'MainTasks',
50
+ timeout: float = 5.0
51
+ ) -> Dict[str, ActorRef]:
52
+ refs = {}
53
+ actor_instances = {}
54
+
55
+ # Создаем корневые акторы через систему
56
+ for defn in self._definitions.values():
57
+ if defn.parent is None:
58
+ actor_instance = defn.cls()
59
+ refs[defn.name] = system.create(actor_instance, name=defn.name)
60
+ actor_instances[defn.name] = actor_instance
61
+
62
+ # Создаем дочерние акторы через родительские ЭКЗЕМПЛЯРЫ акторов
63
+ created = set(refs.keys())
64
+ while len(created) < len(self._definitions):
65
+ for defn in self._definitions.values():
66
+ if defn.name not in created and defn.parent in created:
67
+ parent_instance = actor_instances[defn.parent] # Экземпляр актора-родителя
68
+ actor_instance = defn.cls()
69
+
70
+ # создаем через parent_instance.create(), а не parent_ref.create()
71
+ child_ref = parent_instance.create(actor_instance, name=defn.name)
72
+ refs[defn.name] = child_ref
73
+ actor_instances[defn.name] = actor_instance
74
+
75
+ # Сохраняем ссылку в родителе
76
+ if hasattr(parent_instance, 'actors') and isinstance(parent_instance.actors, dict):
77
+ parent_instance.actors[defn.name] = child_ref
78
+
79
+ created.add(defn.name)
80
+
81
+ start_time = asyncio.get_event_loop().time()
82
+ while asyncio.get_event_loop().time() - start_time < timeout:
83
+ all_started = True
84
+ for actor_instance in actor_instances.values():
85
+ if (
86
+ hasattr(actor_instance, '_context')
87
+ and actor_instance._context
88
+ and not actor_instance._context.receiving_messages
89
+ ):
90
+ all_started = False
91
+ break
92
+
93
+ if all_started:
94
+ break
95
+ await asyncio.sleep(0.1)
96
+
97
+ return refs
98
+
99
+ def get_actor_graph(self) -> Dict[Optional[str], List[str]]:
100
+ graph = {}
101
+ for defn in self._definitions.values():
102
+ if defn.parent not in graph:
103
+ graph[defn.parent] = []
104
+ graph[defn.parent].append(defn.name)
105
+
106
+ return graph
107
+
108
+ def print_actor_tree(self):
109
+ """Печатает дерево акторов в консоль"""
110
+ graph = self.get_actor_graph()
111
+
112
+ def print_node(parent: Optional[str], level: int = 0):
113
+ indent = " " * level
114
+ if parent in graph:
115
+ for child in graph[parent]:
116
+ defn = self._definitions[child]
117
+ log.warning(f"{indent}├── {child} (replicas={defn.replicas}, minimal={defn.minimal})")
118
+ print_node(child, level + 1)
119
+ elif parent is None:
120
+ for root in graph[None]:
121
+ defn = self._definitions[root]
122
+ log.warning(f"┌── {root} (replicas={defn.replicas}, minimal={defn.minimal})")
123
+ print_node(root, 1)
124
+
125
+ log.warning("Actor System Tree:")
126
+ print_node(None)
127
+
128
+
129
+ registry = ActorRegistry()
130
+ actio = registry.actio