pyroflow 0.1.6__tar.gz → 0.2.0__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.
- {pyroflow-0.1.6 → pyroflow-0.2.0}/PKG-INFO +12 -8
- {pyroflow-0.1.6 → pyroflow-0.2.0}/README.md +4 -4
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyproject.toml +17 -3
- pyroflow-0.2.0/pyroflow/__meta__.py +1 -0
- pyroflow-0.2.0/pyroflow/enums.py +21 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/listener_coordinator/listener_coordinator.py +1 -8
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/models.py +8 -9
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/typings.py +1 -8
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_coordinated/callback_query_coordinated.py +1 -1
- pyroflow-0.2.0/pyroflow/update_history/message_history.py +42 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/utils/async_tools.py +58 -26
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/utils/classes.py +1 -7
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/utils/typings.py +16 -20
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/utils/validate_tools.py +1 -3
- pyroflow-0.2.0/tests/__init__.py +0 -0
- pyroflow-0.2.0/tests/conftest.py +70 -0
- pyroflow-0.2.0/tests/test_compat_static.py +144 -0
- pyroflow-0.2.0/tests/test_coordinator.py +110 -0
- pyroflow-0.2.0/tests/test_enums.py +31 -0
- pyroflow-0.2.0/tests/test_history.py +143 -0
- pyroflow-0.2.0/tests/test_imports.py +100 -0
- pyroflow-0.2.0/tests/test_listener.py +136 -0
- pyroflow-0.2.0/tests/test_models.py +84 -0
- pyroflow-0.2.0/tests/test_patch_cls.py +118 -0
- pyroflow-0.2.0/tests/test_utils.py +149 -0
- pyroflow-0.1.6/pyroflow/__meta__.py +0 -1
- pyroflow-0.1.6/pyroflow/enums.py +0 -10
- pyroflow-0.1.6/pyroflow/update_history/message_history.py +0 -6
- {pyroflow-0.1.6 → pyroflow-0.2.0}/LICENSE +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/__init__.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/client.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/dispatcher.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/errors.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/listener_coordinator/__init__.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/listener_coordinator/memory_listener_coordinator.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/listener_coordinator/redis_listener_coordinator.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/types.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_coordinated/__init__.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_coordinated/message_coordinated.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_coordinated/update_coordinated.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_coordinator/__init__.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_coordinator/memory_update_coordinator.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_coordinator/redis_update_coordinator.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_coordinator/update_coordinator.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_history/__init__.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_history/callback_query_history.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_history/update_history.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_history_store/__init__.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_history_store/memory_update_history_store.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_history_store/update_history_store.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_listener/__init__.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_listener/callback_query_listener.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_listener/message_listener.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/update_listener/update_listener.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/utils/__init__.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/utils/enums.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/utils/iter_tools.py +0 -0
- {pyroflow-0.1.6 → pyroflow-0.2.0}/pyroflow/utils/misc_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyroflow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Conversation-oriented Pyrogram extension with per-update listeners and multi-session coordination
|
|
5
5
|
Project-URL: Homepage, https://github.com/eeeob/pyroflow
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/eeeob/pyroflow/issues
|
|
@@ -13,7 +13,6 @@ Classifier: Framework :: AsyncIO
|
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -22,16 +21,21 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
22
21
|
Classifier: Topic :: Communications :: Chat
|
|
23
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
23
|
Classifier: Typing :: Typed
|
|
25
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.10
|
|
26
25
|
Requires-Dist: cachetools
|
|
27
26
|
Requires-Dist: kurigram>=2.2.0
|
|
28
27
|
Requires-Dist: sortedcontainers==2.4.0
|
|
29
|
-
Requires-Dist: typing-extensions>=4.6.0; python_version < '3.
|
|
28
|
+
Requires-Dist: typing-extensions>=4.6.0; python_version < '3.13'
|
|
30
29
|
Provides-Extra: dev
|
|
31
30
|
Requires-Dist: hatch<=1.16.5; extra == 'dev'
|
|
32
31
|
Requires-Dist: twine<=6.2.0; extra == 'dev'
|
|
33
32
|
Provides-Extra: redis
|
|
34
33
|
Requires-Dist: redis>=4.2.0; extra == 'redis'
|
|
34
|
+
Provides-Extra: test
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
|
|
36
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
37
|
+
Requires-Dist: redis>=4.2.0; extra == 'test'
|
|
38
|
+
Requires-Dist: tomli; (python_version < '3.11') and extra == 'test'
|
|
35
39
|
Description-Content-Type: text/markdown
|
|
36
40
|
|
|
37
41
|
# pyroflow
|
|
@@ -72,7 +76,7 @@ pyroflow solves all three problems:
|
|
|
72
76
|
|
|
73
77
|
## Installation
|
|
74
78
|
|
|
75
|
-
**Minimum requirements:** Python 3.
|
|
79
|
+
**Minimum requirements:** Python 3.10+
|
|
76
80
|
|
|
77
81
|
```
|
|
78
82
|
pip install pyroflow # core
|
|
@@ -233,10 +237,10 @@ client.run()
|
|
|
233
237
|
A `UpdateHistory` records which handlers ran successfully for each update. This enables features like `back` buttons that replay or inspect previous processing steps.
|
|
234
238
|
|
|
235
239
|
```python
|
|
236
|
-
from pyroflow import Client,
|
|
240
|
+
from pyroflow import Client, CallbackQueryHistory
|
|
237
241
|
|
|
238
242
|
client = Client("my_session")
|
|
239
|
-
client.register_history(
|
|
243
|
+
client.register_history(CallbackQueryHistory())
|
|
240
244
|
|
|
241
245
|
client.run()
|
|
242
246
|
```
|
|
@@ -248,7 +252,7 @@ They can also be removed at runtime:
|
|
|
248
252
|
```python
|
|
249
253
|
await client.unregister_listener(Message)
|
|
250
254
|
await client.unregister_coordinated(Message)
|
|
251
|
-
await client.unregister_history(
|
|
255
|
+
await client.unregister_history(CallbackQueryHistory)
|
|
252
256
|
```
|
|
253
257
|
|
|
254
258
|
---
|
|
@@ -36,7 +36,7 @@ pyroflow solves all three problems:
|
|
|
36
36
|
|
|
37
37
|
## Installation
|
|
38
38
|
|
|
39
|
-
**Minimum requirements:** Python 3.
|
|
39
|
+
**Minimum requirements:** Python 3.10+
|
|
40
40
|
|
|
41
41
|
```
|
|
42
42
|
pip install pyroflow # core
|
|
@@ -197,10 +197,10 @@ client.run()
|
|
|
197
197
|
A `UpdateHistory` records which handlers ran successfully for each update. This enables features like `back` buttons that replay or inspect previous processing steps.
|
|
198
198
|
|
|
199
199
|
```python
|
|
200
|
-
from pyroflow import Client,
|
|
200
|
+
from pyroflow import Client, CallbackQueryHistory
|
|
201
201
|
|
|
202
202
|
client = Client("my_session")
|
|
203
|
-
client.register_history(
|
|
203
|
+
client.register_history(CallbackQueryHistory())
|
|
204
204
|
|
|
205
205
|
client.run()
|
|
206
206
|
```
|
|
@@ -212,7 +212,7 @@ They can also be removed at runtime:
|
|
|
212
212
|
```python
|
|
213
213
|
await client.unregister_listener(Message)
|
|
214
214
|
await client.unregister_coordinated(Message)
|
|
215
|
-
await client.unregister_history(
|
|
215
|
+
await client.unregister_history(CallbackQueryHistory)
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
---
|
|
@@ -7,7 +7,7 @@ build-backend = "hatchling.build"
|
|
|
7
7
|
name = "pyroflow"
|
|
8
8
|
dynamic = ["version"]
|
|
9
9
|
description = "Conversation-oriented Pyrogram extension with per-update listeners and multi-session coordination"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
authors = [{ name = "Abdullah", email = "aldheeb01@gmail.com" }]
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
license = { text = "MIT" }
|
|
@@ -27,7 +27,6 @@ classifiers = [
|
|
|
27
27
|
"Intended Audience :: Developers",
|
|
28
28
|
"License :: OSI Approved :: MIT License",
|
|
29
29
|
"Programming Language :: Python :: 3",
|
|
30
|
-
"Programming Language :: Python :: 3.9",
|
|
31
30
|
"Programming Language :: Python :: 3.10",
|
|
32
31
|
"Programming Language :: Python :: 3.11",
|
|
33
32
|
"Programming Language :: Python :: 3.12",
|
|
@@ -43,7 +42,7 @@ dependencies = [
|
|
|
43
42
|
"cachetools",
|
|
44
43
|
"sortedcontainers==2.4.0",
|
|
45
44
|
"kurigram>=2.2.0",
|
|
46
|
-
"typing_extensions>=4.6.0; python_version < '3.
|
|
45
|
+
"typing_extensions>=4.6.0; python_version < '3.13'",
|
|
47
46
|
]
|
|
48
47
|
|
|
49
48
|
|
|
@@ -62,6 +61,13 @@ redis = [
|
|
|
62
61
|
"redis>=4.2.0",
|
|
63
62
|
]
|
|
64
63
|
|
|
64
|
+
test = [
|
|
65
|
+
"pytest>=7.0",
|
|
66
|
+
"pytest-asyncio>=0.23",
|
|
67
|
+
"redis>=4.2.0",
|
|
68
|
+
"tomli; python_version < '3.11'",
|
|
69
|
+
]
|
|
70
|
+
|
|
65
71
|
|
|
66
72
|
[tool.hatch.build.targets.sdist]
|
|
67
73
|
exclude = [
|
|
@@ -78,3 +84,11 @@ allow-direct-references = true
|
|
|
78
84
|
[tool.hatch.build.targets.wheel]
|
|
79
85
|
ignore-vcs = true
|
|
80
86
|
packages = ["pyroflow"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
[tool.pytest.ini_options]
|
|
90
|
+
asyncio_mode = "auto"
|
|
91
|
+
testpaths = ["tests"]
|
|
92
|
+
filterwarnings = [
|
|
93
|
+
"ignore::DeprecationWarning",
|
|
94
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if sys.version_info >= (3, 11):
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
else:
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
class StrEnum(str, Enum):
|
|
9
|
+
def _generate_next_value_(self, *args):
|
|
10
|
+
return self.lower()
|
|
11
|
+
|
|
12
|
+
from enum import IntEnum, auto
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UpdateLockState(IntEnum):
|
|
16
|
+
PROCESSING = 0
|
|
17
|
+
HANDLED = 1
|
|
18
|
+
|
|
19
|
+
class DuplicatePolicy(StrEnum):
|
|
20
|
+
REJECT = auto()
|
|
21
|
+
REPLACE = auto()
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Optional, Union, Tuple, TYPE_CHECKING
|
|
2
|
+
from typing import Optional, Union, Tuple, TypeAlias, TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
from ..utils.typings import AsyncLockProto
|
|
6
6
|
from ..typings import ListenerCoordinatorIdT
|
|
7
7
|
from ..models import ListenerKey
|
|
8
8
|
|
|
9
|
-
import sys
|
|
10
|
-
|
|
11
|
-
if sys.version_info >= (3, 10):
|
|
12
|
-
from typing import TypeAlias
|
|
13
|
-
else:
|
|
14
|
-
from typing_extensions import TypeAlias
|
|
15
|
-
|
|
16
9
|
if TYPE_CHECKING:
|
|
17
10
|
from ..update_listener import UpdateListener
|
|
18
11
|
|
|
@@ -8,7 +8,6 @@ import asyncio
|
|
|
8
8
|
import time
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
@dataclass(frozen=True, slots=True, eq=False)
|
|
13
12
|
class ListenerKey:
|
|
14
13
|
chat_id: int
|
|
@@ -18,11 +17,11 @@ class ListenerKey:
|
|
|
18
17
|
@property
|
|
19
18
|
def to_tuple(self) -> Tuple[int, Optional[int], Optional[int]]:
|
|
20
19
|
return (
|
|
21
|
-
self.chat_id,
|
|
22
|
-
self.user_id,
|
|
20
|
+
self.chat_id,
|
|
21
|
+
self.user_id,
|
|
23
22
|
self.message_id
|
|
24
23
|
)
|
|
25
|
-
|
|
24
|
+
|
|
26
25
|
|
|
27
26
|
def sub_keys(self, min_dep: int = 1) -> Generator["ListenerKey", None, None]:
|
|
28
27
|
keys = self.to_tuple
|
|
@@ -32,26 +31,26 @@ class ListenerKey:
|
|
|
32
31
|
yield ListenerKey(
|
|
33
32
|
*keys[:i]
|
|
34
33
|
)
|
|
35
|
-
|
|
34
|
+
|
|
36
35
|
def __hash__(self):
|
|
37
36
|
return hash(self.to_tuple)
|
|
38
|
-
|
|
37
|
+
|
|
39
38
|
def __eq__(self, value):
|
|
40
39
|
return isinstance(value, ListenerKey) and self.to_tuple == value.to_tuple
|
|
41
40
|
|
|
42
41
|
@dataclass(slots=True)
|
|
43
42
|
class ListenerModel:
|
|
44
43
|
key: ListenerKey
|
|
45
|
-
meta: Optional[JsonValueT] = None
|
|
44
|
+
meta: Optional[JsonValueT] = None
|
|
46
45
|
future: asyncio.Future = field(default_factory=asyncio.Future)
|
|
47
46
|
|
|
48
47
|
|
|
49
48
|
def resolve(self, update) -> None:
|
|
50
49
|
self.future.set_result(update)
|
|
51
|
-
|
|
50
|
+
|
|
52
51
|
def set_exc(self, exc: Exception) -> None:
|
|
53
52
|
self.future.set_exception(exc)
|
|
54
|
-
|
|
53
|
+
|
|
55
54
|
def done(self) -> bool:
|
|
56
55
|
return self.future.done()
|
|
57
56
|
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
from typing import Union, Tuple
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
|
|
5
|
-
if sys.version_info >= (3, 10):
|
|
6
|
-
from typing import TypeAlias
|
|
7
|
-
else:
|
|
8
|
-
from typing_extensions import TypeAlias
|
|
1
|
+
from typing import Union, Tuple, TypeAlias
|
|
9
2
|
|
|
10
3
|
UpdateCoordinatorKeyT: TypeAlias = Tuple[Union[int, str], ...]
|
|
11
4
|
UpdateHistoryKeyT = UpdateCoordinatorKeyT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from ..types import Message
|
|
4
|
+
from .update_history import UpdateHistory
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MessageHistory(UpdateHistory[Message]):
|
|
8
|
+
__update_type__ = Message
|
|
9
|
+
|
|
10
|
+
if not TYPE_CHECKING:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
store_factory = None,
|
|
14
|
+
is_recordable_func = None,
|
|
15
|
+
extract_key_func = None,
|
|
16
|
+
extract_data_func = None,
|
|
17
|
+
**kw
|
|
18
|
+
):
|
|
19
|
+
|
|
20
|
+
if is_recordable_func is None:
|
|
21
|
+
is_recordable_func = self._is_recordable
|
|
22
|
+
|
|
23
|
+
if extract_key_func is None:
|
|
24
|
+
extract_key_func = self._extract_key
|
|
25
|
+
|
|
26
|
+
if extract_data_func is None:
|
|
27
|
+
extract_data_func = self._extract_data
|
|
28
|
+
|
|
29
|
+
super().__init__(store_factory, is_recordable_func, extract_key_func, extract_data_func, **kw)
|
|
30
|
+
|
|
31
|
+
async def _is_recordable(self, update):
|
|
32
|
+
return update.chat is not None
|
|
33
|
+
|
|
34
|
+
async def _extract_key(self, update):
|
|
35
|
+
return (
|
|
36
|
+
update.chat.id,
|
|
37
|
+
getattr(update.from_user, "id", None),
|
|
38
|
+
update.id,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
async def _extract_data(self, update):
|
|
42
|
+
return update.text
|
|
@@ -10,11 +10,34 @@ import functools
|
|
|
10
10
|
import logging
|
|
11
11
|
import contextvars
|
|
12
12
|
import inspect
|
|
13
|
+
import traceback
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
log = logging.getLogger(__file__)
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
def _log_exc(
|
|
20
|
+
header: Optional[str],
|
|
21
|
+
caller_stack: List[traceback.FrameSummary],
|
|
22
|
+
exc: BaseException,
|
|
23
|
+
index: Optional[int] = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
exc_trace = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
26
|
+
caller_trace = traceback.format_list(caller_stack)
|
|
27
|
+
|
|
28
|
+
if header is None:
|
|
29
|
+
header = ""
|
|
30
|
+
|
|
31
|
+
if index is not None:
|
|
32
|
+
header = f"{header} [awaitable index={index}]" if header else f"[awaitable index={index}]"
|
|
33
|
+
|
|
34
|
+
log.error(
|
|
35
|
+
"%s\n%s",
|
|
36
|
+
header,
|
|
37
|
+
"".join(caller_trace) + "".join(exc_trace)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
18
41
|
@overload
|
|
19
42
|
async def to_thread(
|
|
20
43
|
func: Callable[_P, _T],
|
|
@@ -37,25 +60,26 @@ async def to_thread(
|
|
|
37
60
|
func,
|
|
38
61
|
*args,
|
|
39
62
|
executor = None,
|
|
40
|
-
log_exc = True,
|
|
41
|
-
return_exc = False,
|
|
63
|
+
log_exc = True,
|
|
64
|
+
return_exc = False,
|
|
42
65
|
**kwargs,
|
|
43
66
|
):
|
|
44
|
-
|
|
67
|
+
caller_stack = traceback.extract_stack()[:-1]
|
|
68
|
+
|
|
45
69
|
loop = asyncio.get_running_loop()
|
|
46
70
|
ctx = contextvars.copy_context()
|
|
47
71
|
func = functools.partial(ctx.run, func, *args, **kwargs)
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
|
|
73
|
+
|
|
50
74
|
try:
|
|
51
75
|
return await loop.run_in_executor(executor, func)
|
|
52
76
|
except Exception as e:
|
|
53
77
|
if log_exc:
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
_log_exc("error in to_thread", caller_stack, e)
|
|
79
|
+
|
|
56
80
|
if return_exc:
|
|
57
81
|
return e
|
|
58
|
-
|
|
82
|
+
|
|
59
83
|
raise
|
|
60
84
|
|
|
61
85
|
|
|
@@ -77,12 +101,14 @@ async def gather_helper(
|
|
|
77
101
|
log_exc = True,
|
|
78
102
|
):
|
|
79
103
|
|
|
104
|
+
caller_stack = traceback.extract_stack()[:-1]
|
|
105
|
+
|
|
80
106
|
results = await asyncio.gather(*flat_cont(coros), return_exceptions=return_exc)
|
|
81
107
|
|
|
82
108
|
if log_exc:
|
|
83
|
-
for r in results:
|
|
109
|
+
for i, r in enumerate(results):
|
|
84
110
|
if is_exception(r):
|
|
85
|
-
|
|
111
|
+
_log_exc("error in gather_helper", caller_stack, r, index=i)
|
|
86
112
|
|
|
87
113
|
return results
|
|
88
114
|
|
|
@@ -130,15 +156,24 @@ async def safe_await(
|
|
|
130
156
|
log_exc = True,
|
|
131
157
|
):
|
|
132
158
|
|
|
159
|
+
caller_stack = traceback.extract_stack()[:-1]
|
|
160
|
+
|
|
133
161
|
results = []
|
|
162
|
+
flat_coros = flat_cont(coros)
|
|
163
|
+
is_multi = len(flat_coros) > 1
|
|
134
164
|
|
|
135
|
-
for coro in
|
|
165
|
+
for i, coro in enumerate(flat_coros):
|
|
136
166
|
try:
|
|
137
167
|
result = await coro
|
|
138
168
|
except Exception as e:
|
|
139
169
|
if log_exc:
|
|
140
|
-
|
|
141
|
-
|
|
170
|
+
_log_exc(
|
|
171
|
+
"error in safe_await",
|
|
172
|
+
caller_stack,
|
|
173
|
+
e,
|
|
174
|
+
index=i if is_multi else None,
|
|
175
|
+
)
|
|
176
|
+
|
|
142
177
|
result = e
|
|
143
178
|
|
|
144
179
|
if is_exception(result):
|
|
@@ -172,21 +207,21 @@ async def maybe_awaitable(
|
|
|
172
207
|
async def maybe_awaitable(
|
|
173
208
|
awaitable_or_callable,
|
|
174
209
|
*args,
|
|
175
|
-
executor = None,
|
|
176
|
-
return_exc = False,
|
|
210
|
+
executor = None,
|
|
211
|
+
return_exc = False,
|
|
177
212
|
log_exc = True,
|
|
178
213
|
**kwargs,
|
|
179
214
|
):
|
|
180
|
-
|
|
215
|
+
|
|
181
216
|
if inspect.isawaitable(awaitable_or_callable):
|
|
182
217
|
if args or kwargs:
|
|
183
218
|
raise TypeError(
|
|
184
219
|
"Cannot pass args/kwargs to an already-created awaitable"
|
|
185
220
|
)
|
|
186
221
|
return await safe_await(
|
|
187
|
-
awaitable_or_callable,
|
|
188
|
-
return_exc=return_exc,
|
|
189
|
-
log_exc=log_exc,
|
|
222
|
+
awaitable_or_callable,
|
|
223
|
+
return_exc=return_exc,
|
|
224
|
+
log_exc=log_exc,
|
|
190
225
|
)
|
|
191
226
|
elif iscoroutinefunction_wrapped(awaitable_or_callable):
|
|
192
227
|
return await safe_await(
|
|
@@ -209,12 +244,9 @@ async def maybe_awaitable(
|
|
|
209
244
|
)
|
|
210
245
|
|
|
211
246
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
247
|
__all__ = [
|
|
216
|
-
"to_thread",
|
|
217
|
-
"gather_helper",
|
|
218
|
-
"safe_await",
|
|
248
|
+
"to_thread",
|
|
249
|
+
"gather_helper",
|
|
250
|
+
"safe_await",
|
|
219
251
|
"maybe_awaitable"
|
|
220
|
-
]
|
|
252
|
+
]
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
from abc import ABC
|
|
2
|
-
from typing import Generic, Type
|
|
2
|
+
from typing import Generic, Type, ClassVar
|
|
3
3
|
|
|
4
4
|
from pyrogram.types import Update as PyroUpdate
|
|
5
5
|
from .typings import UpdateType
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
|
-
import sys
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
if sys.version_info >= (3, 10):
|
|
12
|
-
from typing import ClassVar
|
|
13
|
-
else:
|
|
14
|
-
from typing_extensions import ClassVar
|
|
15
|
-
|
|
16
10
|
class AsyncioLock(asyncio.Lock):
|
|
17
11
|
async def release(self):
|
|
18
12
|
return super().release()
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
from typing import (
|
|
2
|
-
Collection as TCollection, Generator, Union, Reversible,
|
|
3
|
-
Sequence, AbstractSet, Mapping, List, Any, Dict,
|
|
4
|
-
|
|
5
|
-
Literal, TYPE_CHECKING,
|
|
2
|
+
Collection as TCollection, Generator, Union, Reversible,
|
|
3
|
+
Sequence, AbstractSet, Mapping, List, Any, Dict,
|
|
4
|
+
Coroutine, Awaitable, TypeVar,
|
|
5
|
+
Literal, TYPE_CHECKING,
|
|
6
|
+
ParamSpec, TypeAlias, Protocol, Callable,
|
|
6
7
|
)
|
|
7
8
|
|
|
8
9
|
from enum import Enum
|
|
9
10
|
|
|
10
11
|
import sys
|
|
11
12
|
|
|
12
|
-
if sys.version_info >= (3, 10):
|
|
13
|
-
from typing import ParamSpec, TypeAlias, Protocol
|
|
14
|
-
else:
|
|
15
|
-
from typing_extensions import ParamSpec, TypeAlias, Protocol
|
|
16
|
-
|
|
17
13
|
if sys.version_info >= (3, 11):
|
|
18
14
|
from enum import EnumType
|
|
19
15
|
else:
|
|
@@ -74,15 +70,15 @@ UpdateType = TypeVar("UpdateType", bound=PyroUpdate)
|
|
|
74
70
|
|
|
75
71
|
|
|
76
72
|
__all__ = (
|
|
77
|
-
"Container",
|
|
78
|
-
"NestedContainer",
|
|
79
|
-
"NotContainer",
|
|
80
|
-
"MaybeAwaitable",
|
|
81
|
-
"Number",
|
|
82
|
-
"MaybeCoroutineCallable",
|
|
83
|
-
"AsyncLockProto",
|
|
84
|
-
"JsonValueT",
|
|
85
|
-
"UpdateType",
|
|
86
|
-
"_P", "_T", "_CT",
|
|
87
|
-
"_True", "_False",
|
|
73
|
+
"Container",
|
|
74
|
+
"NestedContainer",
|
|
75
|
+
"NotContainer",
|
|
76
|
+
"MaybeAwaitable",
|
|
77
|
+
"Number",
|
|
78
|
+
"MaybeCoroutineCallable",
|
|
79
|
+
"AsyncLockProto",
|
|
80
|
+
"JsonValueT",
|
|
81
|
+
"UpdateType",
|
|
82
|
+
"_P", "_T", "_CT",
|
|
83
|
+
"_True", "_False",
|
|
88
84
|
)
|
|
@@ -6,10 +6,8 @@ import inspect
|
|
|
6
6
|
|
|
7
7
|
if sys.version_info >= (3, 13):
|
|
8
8
|
from typing import TypeIs
|
|
9
|
-
elif sys.version_info >= (3, 10):
|
|
10
|
-
from typing_extensions import TypeIs
|
|
11
9
|
else:
|
|
12
|
-
from typing_extensions import
|
|
10
|
+
from typing_extensions import TypeIs
|
|
13
11
|
|
|
14
12
|
from .typings import Container, NotContainer, _T
|
|
15
13
|
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Shared fixtures and object factories for the pyroflow test suite.
|
|
2
|
+
|
|
3
|
+
Importing ``pyroflow`` here is deliberate and must happen before any test
|
|
4
|
+
runs: ``@patch_cls`` monkey-patches Pyrogram's ``Client``, ``Dispatcher``,
|
|
5
|
+
``Message`` and ``CallbackQuery`` in-place at import time, and the whole
|
|
6
|
+
suite relies on that global patch being active.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
import pyroflow # noqa: F401 — activates the @patch_cls global monkey-patch
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from pyrogram.types import Message, CallbackQuery, Chat, User
|
|
15
|
+
from pyrogram.enums import ChatType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def make_message(chat_id=100, user_id=7, message_id=5, **overrides):
|
|
19
|
+
"""Build a minimal, listenable :class:`Message` instance.
|
|
20
|
+
|
|
21
|
+
Defaults produce a private, incoming, non-service message that
|
|
22
|
+
``MessageListener._is_listenable`` accepts. Any field can be
|
|
23
|
+
overridden via keyword to exercise edge cases.
|
|
24
|
+
"""
|
|
25
|
+
msg = Message(
|
|
26
|
+
id=message_id,
|
|
27
|
+
chat=Chat(id=chat_id, type=ChatType.PRIVATE),
|
|
28
|
+
from_user=User(id=user_id, is_self=False),
|
|
29
|
+
)
|
|
30
|
+
for key, value in overrides.items():
|
|
31
|
+
setattr(msg, key, value)
|
|
32
|
+
return msg
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def make_callback_query(chat_id=100, user_id=7, message_id=5, data="cb", **overrides):
|
|
36
|
+
"""Build a minimal :class:`CallbackQuery` whose ``message`` is listenable."""
|
|
37
|
+
cbq = CallbackQuery(
|
|
38
|
+
id="cbq-1",
|
|
39
|
+
from_user=User(id=user_id, is_self=False),
|
|
40
|
+
chat_instance="chat-instance",
|
|
41
|
+
message=make_message(chat_id, user_id, message_id),
|
|
42
|
+
data=data,
|
|
43
|
+
)
|
|
44
|
+
for key, value in overrides.items():
|
|
45
|
+
setattr(cbq, key, value)
|
|
46
|
+
return cbq
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def message_factory():
|
|
51
|
+
return make_message
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def callback_factory():
|
|
56
|
+
return make_callback_query
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def raw_message_factory():
|
|
61
|
+
"""A ``Message`` carrying a ``raw.peer_id`` for coordinator key extraction."""
|
|
62
|
+
|
|
63
|
+
def _factory(chat_id=100, user_id=7, message_id=5, peer_channel_id=100):
|
|
64
|
+
msg = make_message(chat_id, user_id, message_id)
|
|
65
|
+
# get_peer_id() understands raw PeerChannel/PeerUser objects; a
|
|
66
|
+
# SimpleNamespace with the right attribute name is enough here.
|
|
67
|
+
msg.raw = SimpleNamespace(peer_id=SimpleNamespace(channel_id=peer_channel_id))
|
|
68
|
+
return msg
|
|
69
|
+
|
|
70
|
+
return _factory
|