aiober 0.0.1__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.
- aiober-0.0.1/LICENSE +21 -0
- aiober-0.0.1/PKG-INFO +72 -0
- aiober-0.0.1/README.md +52 -0
- aiober-0.0.1/aiober/__init__.py +6 -0
- aiober-0.0.1/aiober/client/__init__.py +5 -0
- aiober-0.0.1/aiober/client/bot.py +42 -0
- aiober-0.0.1/aiober/client/context_controller.py +21 -0
- aiober-0.0.1/aiober/client/session/base.py +34 -0
- aiober-0.0.1/aiober/client/session/request.py +68 -0
- aiober-0.0.1/aiober/client/session/viber.py +12 -0
- aiober-0.0.1/aiober/filters/__init__.py +3 -0
- aiober-0.0.1/aiober/filters/base.py +13 -0
- aiober-0.0.1/aiober/filters/state.py +30 -0
- aiober-0.0.1/aiober/filters/text.py +22 -0
- aiober-0.0.1/aiober/fsm/__init__.py +0 -0
- aiober-0.0.1/aiober/fsm/context.py +27 -0
- aiober-0.0.1/aiober/fsm/state.py +162 -0
- aiober-0.0.1/aiober/fsm/storage/__init__.py +3 -0
- aiober-0.0.1/aiober/fsm/storage/base.py +73 -0
- aiober-0.0.1/aiober/fsm/storage/memory.py +31 -0
- aiober-0.0.1/aiober/fsm/storage/redis.py +88 -0
- aiober-0.0.1/aiober/methods/__init__.py +7 -0
- aiober-0.0.1/aiober/methods/base.py +31 -0
- aiober-0.0.1/aiober/methods/methods.py +8 -0
- aiober-0.0.1/aiober/methods/send_message.py +57 -0
- aiober-0.0.1/aiober/router/__init__.py +4 -0
- aiober-0.0.1/aiober/router/dispatcher.py +100 -0
- aiober-0.0.1/aiober/router/event/__init__.py +2 -0
- aiober-0.0.1/aiober/router/event/event.py +33 -0
- aiober-0.0.1/aiober/router/event/handler.py +39 -0
- aiober-0.0.1/aiober/router/router.py +52 -0
- aiober-0.0.1/aiober/types/__init__.py +48 -0
- aiober-0.0.1/aiober/types/base.py +37 -0
- aiober-0.0.1/aiober/types/color.py +14 -0
- aiober-0.0.1/aiober/types/conversation_started.py +37 -0
- aiober-0.0.1/aiober/types/keyboard.py +84 -0
- aiober-0.0.1/aiober/types/massage.py +95 -0
- aiober-0.0.1/aiober/types/rich_media.py +30 -0
- aiober-0.0.1/aiober/types/seen.py +7 -0
- aiober-0.0.1/aiober/types/subscribed.py +24 -0
- aiober-0.0.1/aiober/types/user.py +9 -0
- aiober-0.0.1/aiober.egg-info/PKG-INFO +72 -0
- aiober-0.0.1/aiober.egg-info/SOURCES.txt +47 -0
- aiober-0.0.1/aiober.egg-info/dependency_links.txt +1 -0
- aiober-0.0.1/aiober.egg-info/requires.txt +4 -0
- aiober-0.0.1/aiober.egg-info/top_level.txt +1 -0
- aiober-0.0.1/pyproject.toml +39 -0
- aiober-0.0.1/setup.cfg +4 -0
- aiober-0.0.1/setup.py +3 -0
aiober-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Quaron Software
|
|
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.
|
aiober-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiober
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The AioBer library AsyncIO viBER (Aiober) is a python library for creating Viber bots easily, professionally and in a structured way!
|
|
5
|
+
Author: Gucul
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/QuaronSoftware/aiober
|
|
8
|
+
Project-URL: Issues, https://github.com/QuaronSoftware/aiober/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: redis==7.1.1
|
|
16
|
+
Requires-Dist: pydantic==2.12.5
|
|
17
|
+
Requires-Dist: aiohttp==3.13.3
|
|
18
|
+
Requires-Dist: certifi==2026.1.4
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Аiober
|
|
22
|
+
|
|
23
|
+
**A**sync**IO**
|
|
24
|
+
vi**BER** – Python library for easy creation of Viber bots
|
|
25
|
+
*Currently, Viber bots can only be works using webhooks.*
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Introduction
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
## Let's get started !
|
|
32
|
+
|
|
33
|
+
### Installing
|
|
34
|
+
Creating a viber bot is easy !
|
|
35
|
+
1. Install the library with `pip install git+https://github.com/QuaronSoftware/aiober.git` or using git
|
|
36
|
+
2. From aiober import Bot and Dispatcher to your projects
|
|
37
|
+
3. Build a simple script like in **Example**
|
|
38
|
+
4. Make viber bot [here](https://partners.viber.com/account/create-bot-account)
|
|
39
|
+
5. Using ngrok make your address global (for webhooks)
|
|
40
|
+
6. Run your script
|
|
41
|
+
7. Open Postman and make post request to https://chatapi.viber.com/pa/set_webhook with data `{"url":"https://your-domain/update, "auth_token": "auth-token"}` for set webhook to viber bot
|
|
42
|
+
**Good, webhook is registred !**
|
|
43
|
+
|
|
44
|
+
## Example
|
|
45
|
+
|
|
46
|
+
### Simple echo bot
|
|
47
|
+
```python
|
|
48
|
+
import asyncio
|
|
49
|
+
|
|
50
|
+
from aiober import Bot, Dispatcher
|
|
51
|
+
from aiober.types import Message
|
|
52
|
+
|
|
53
|
+
bot = Bot('auth-token')
|
|
54
|
+
dp = Dispatcher(bot=bot)
|
|
55
|
+
|
|
56
|
+
# router
|
|
57
|
+
@dp.messages()
|
|
58
|
+
async def echo(message: Message):
|
|
59
|
+
await message.copy_to(message.sender.id)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def main():
|
|
63
|
+
|
|
64
|
+
# start webhook
|
|
65
|
+
await dp.start_webhook(
|
|
66
|
+
host='127.0.0.1',
|
|
67
|
+
port=8000,
|
|
68
|
+
path='/update'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
asyncio.run(main())
|
|
72
|
+
```
|
aiober-0.0.1/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Аiober
|
|
2
|
+
|
|
3
|
+
**A**sync**IO**
|
|
4
|
+
vi**BER** – Python library for easy creation of Viber bots
|
|
5
|
+
*Currently, Viber bots can only be works using webhooks.*
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Introduction
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
## Let's get started !
|
|
12
|
+
|
|
13
|
+
### Installing
|
|
14
|
+
Creating a viber bot is easy !
|
|
15
|
+
1. Install the library with `pip install git+https://github.com/QuaronSoftware/aiober.git` or using git
|
|
16
|
+
2. From aiober import Bot and Dispatcher to your projects
|
|
17
|
+
3. Build a simple script like in **Example**
|
|
18
|
+
4. Make viber bot [here](https://partners.viber.com/account/create-bot-account)
|
|
19
|
+
5. Using ngrok make your address global (for webhooks)
|
|
20
|
+
6. Run your script
|
|
21
|
+
7. Open Postman and make post request to https://chatapi.viber.com/pa/set_webhook with data `{"url":"https://your-domain/update, "auth_token": "auth-token"}` for set webhook to viber bot
|
|
22
|
+
**Good, webhook is registred !**
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
|
|
26
|
+
### Simple echo bot
|
|
27
|
+
```python
|
|
28
|
+
import asyncio
|
|
29
|
+
|
|
30
|
+
from aiober import Bot, Dispatcher
|
|
31
|
+
from aiober.types import Message
|
|
32
|
+
|
|
33
|
+
bot = Bot('auth-token')
|
|
34
|
+
dp = Dispatcher(bot=bot)
|
|
35
|
+
|
|
36
|
+
# router
|
|
37
|
+
@dp.messages()
|
|
38
|
+
async def echo(message: Message):
|
|
39
|
+
await message.copy_to(message.sender.id)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def main():
|
|
43
|
+
|
|
44
|
+
# start webhook
|
|
45
|
+
await dp.start_webhook(
|
|
46
|
+
host='127.0.0.1',
|
|
47
|
+
port=8000,
|
|
48
|
+
path='/update'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
from .session.base import BaseSession
|
|
3
|
+
from aiober.types import Keyboard, RichMediaKeyboard
|
|
4
|
+
from aiober.methods.base import ViberMethod
|
|
5
|
+
from aiober.client.session.request import AiohttpSession
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Bot:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
token: str,
|
|
12
|
+
session: BaseSession = None
|
|
13
|
+
):
|
|
14
|
+
self._token = token
|
|
15
|
+
self.session = session if isinstance(session, BaseSession) else AiohttpSession()
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def token(self):
|
|
19
|
+
return self._token
|
|
20
|
+
|
|
21
|
+
async def __call__(self, method: ViberMethod, request_runtime: int = None):
|
|
22
|
+
return await self.session(self, method, timeout=request_runtime)
|
|
23
|
+
|
|
24
|
+
def send_message(self, chat_id: str, text: str, keyboard: Keyboard = None):
|
|
25
|
+
from aiober.methods import SendMessage
|
|
26
|
+
|
|
27
|
+
return SendMessage(
|
|
28
|
+
chat_id,
|
|
29
|
+
type='text',
|
|
30
|
+
text=text,
|
|
31
|
+
keyboard=keyboard
|
|
32
|
+
).as_(self)
|
|
33
|
+
|
|
34
|
+
def send_rich_media(self, chat_id: str, rich_media: RichMediaKeyboard):
|
|
35
|
+
from aiober.methods import SendMessage
|
|
36
|
+
|
|
37
|
+
return SendMessage(
|
|
38
|
+
chat_id,
|
|
39
|
+
type='rich_media',
|
|
40
|
+
text=None,
|
|
41
|
+
rich_media=rich_media
|
|
42
|
+
).as_(self)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, PrivateAttr
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from aiober.client.bot import Bot
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BotContextController(BaseModel):
|
|
11
|
+
_bot: Optional["Bot"] = PrivateAttr()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def as_(self, bot: Optional["Bot"]) -> Self:
|
|
15
|
+
self._bot = bot
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def bot(self) -> Optional['Bot']:
|
|
20
|
+
|
|
21
|
+
return self._bot
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pydantic import parse_obj_as
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from aiober.methods.base import Response
|
|
7
|
+
from .viber import ViberAPIServer, PRODUCTION
|
|
8
|
+
|
|
9
|
+
DEFAULT_TIMEOUT: float = 60.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseSession(ABC):
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.api: ViberAPIServer = PRODUCTION
|
|
16
|
+
self.timeout = DEFAULT_TIMEOUT
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def make_request(self, bot, timeout: int = None):
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def check_response(self, bot, status_code: int, content: str) -> Response:
|
|
24
|
+
try:
|
|
25
|
+
json_data = json.loads(content)
|
|
26
|
+
except Exception as E:
|
|
27
|
+
raise UnicodeDecodeError("failed to decode object")
|
|
28
|
+
|
|
29
|
+
response = parse_obj_as(Response, json_data)
|
|
30
|
+
|
|
31
|
+
if 200 <= status_code <= 220 and response.status==0:
|
|
32
|
+
return response
|
|
33
|
+
|
|
34
|
+
raise RuntimeError(f'status code {status_code}; response: {response.dict()}')
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import ssl
|
|
3
|
+
import certifi
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from aiohttp import ClientSession, TCPConnector, FormData
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
|
|
9
|
+
from aiober.methods.base import ViberMethod
|
|
10
|
+
|
|
11
|
+
from .base import BaseSession
|
|
12
|
+
|
|
13
|
+
T = TypeVar('T')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AiohttpSession(BaseSession):
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._session: ClientSession = None
|
|
19
|
+
super().__init__()
|
|
20
|
+
|
|
21
|
+
async def create_session(self, token: str) -> ClientSession:
|
|
22
|
+
if self._session is None:
|
|
23
|
+
self._session = ClientSession(
|
|
24
|
+
connector=TCPConnector(ssl=ssl.create_default_context(cafile=certifi.where())),
|
|
25
|
+
headers={
|
|
26
|
+
"X-Viber-Auth-Token": token
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return self._session
|
|
31
|
+
|
|
32
|
+
def build_form_data(self, data: dict) -> dict:
|
|
33
|
+
result = {
|
|
34
|
+
'min_api_version': 2
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for key, value in data.items():
|
|
38
|
+
if value:
|
|
39
|
+
result[key] = value
|
|
40
|
+
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
async def make_request(self, bot, method: ViberMethod, timeout: int = None):
|
|
44
|
+
session = await self.create_session(bot.token)
|
|
45
|
+
|
|
46
|
+
url = self.api.get_api_url(method.__api_method__)
|
|
47
|
+
form_data = self.build_form_data(method.dict(exclude_none=True))
|
|
48
|
+
logging.debug(form_data)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
async with session.post(
|
|
52
|
+
url, data=json.dumps(form_data), timeout=self.timeout if timeout is None else timeout
|
|
53
|
+
) as resp:
|
|
54
|
+
raw_result = await resp.text()
|
|
55
|
+
logging.debug(raw_result)
|
|
56
|
+
except RuntimeError:
|
|
57
|
+
raise RuntimeError()
|
|
58
|
+
except Exception as E:
|
|
59
|
+
raise E
|
|
60
|
+
|
|
61
|
+
response = self.check_response(bot, resp.status, raw_result)
|
|
62
|
+
|
|
63
|
+
return response.status
|
|
64
|
+
|
|
65
|
+
async def __call__(self, bot, method: ViberMethod[T], timeout: int = None):
|
|
66
|
+
|
|
67
|
+
await self.make_request(bot, method, timeout)
|
|
68
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Any, cast
|
|
2
|
+
from inspect import isclass
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from aiober.fsm.state import State, StatesGroup
|
|
5
|
+
from aiober.fsm.context import FSMcontext, StateType
|
|
6
|
+
|
|
7
|
+
from .base import BaseFilter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StateFilter(BaseFilter):
|
|
11
|
+
def __init__(self, *state: StateType):
|
|
12
|
+
if not state:
|
|
13
|
+
msg = "At list one state is required"
|
|
14
|
+
raise ValueError(msg)
|
|
15
|
+
|
|
16
|
+
self.states = state
|
|
17
|
+
|
|
18
|
+
async def __call__(self, event: Any, state: FSMcontext) -> bool | dict[str, Any]:
|
|
19
|
+
raw_state = await state.get_state()
|
|
20
|
+
allowed_states = cast(Sequence[StateType], self.states)
|
|
21
|
+
for state in allowed_states:
|
|
22
|
+
if isinstance(state, str) or state is None:
|
|
23
|
+
if state in {'*', raw_state}:
|
|
24
|
+
return True
|
|
25
|
+
elif isinstance(state, (State, StatesGroup)):
|
|
26
|
+
if state(event, raw_state):
|
|
27
|
+
return True
|
|
28
|
+
elif isclass(state) and issubclass(state, StatesGroup) and state()(event=event, raw_state=raw_state):
|
|
29
|
+
return True
|
|
30
|
+
return False
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from aiober.types import Message
|
|
2
|
+
|
|
3
|
+
from .base import BaseFilter
|
|
4
|
+
|
|
5
|
+
class TextFilter(BaseFilter):
|
|
6
|
+
def __init__(self, text: str = None):
|
|
7
|
+
self.text = text
|
|
8
|
+
|
|
9
|
+
async def __call__(self, message: Message):
|
|
10
|
+
return message.text == self.text
|
|
11
|
+
|
|
12
|
+
class StartsWithFilter(BaseFilter):
|
|
13
|
+
def __init__(self, text: str = None):
|
|
14
|
+
self.text = text
|
|
15
|
+
|
|
16
|
+
async def __call__(self, message: Message):
|
|
17
|
+
if message.text and message.text.startswith(self.text):
|
|
18
|
+
lens = len(self.text)
|
|
19
|
+
return {
|
|
20
|
+
'text_ends': message.text[lens::]
|
|
21
|
+
}
|
|
22
|
+
return False
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from .storage.base import BaseStorage, StorageKey, StateType
|
|
3
|
+
|
|
4
|
+
class FSMcontext:
|
|
5
|
+
|
|
6
|
+
def __init__(self, key: StorageKey, storage: BaseStorage):
|
|
7
|
+
self._storage_key = key
|
|
8
|
+
self._storage = storage
|
|
9
|
+
|
|
10
|
+
async def get_state(self) -> str:
|
|
11
|
+
return await self._storage.get_state(self._storage_key)
|
|
12
|
+
|
|
13
|
+
async def set_state(self, state: StateType) -> None:
|
|
14
|
+
return await self._storage.set_state(self._storage_key, state=state)
|
|
15
|
+
|
|
16
|
+
async def get_data(self) -> dict[str, Any]:
|
|
17
|
+
return await self._storage.get_data(self._storage_key)
|
|
18
|
+
|
|
19
|
+
async def set_data(self, data: dict[str, Any] = {}, **kwargs):
|
|
20
|
+
return await self._storage.set_data(self._storage_key, data | kwargs)
|
|
21
|
+
|
|
22
|
+
async def update_data(self, data: dict[str, Any] = {}, **kwargs):
|
|
23
|
+
return await self._storage.update_data(self._storage_key, data | kwargs)
|
|
24
|
+
|
|
25
|
+
async def clear(self):
|
|
26
|
+
await self.set_state(None)
|
|
27
|
+
await self.set_data({})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
from typing import Any, no_type_check
|
|
4
|
+
|
|
5
|
+
from aiober.types import ViberObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class State:
|
|
9
|
+
|
|
10
|
+
def __init__(self, state: str | None = None, group_name: str | None = None):
|
|
11
|
+
self._state = state
|
|
12
|
+
self._group_name = group_name
|
|
13
|
+
self._group: type["StatesGroup"] | None = None
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def group(self) -> type["StatesGroup"]:
|
|
17
|
+
if self._group is None:
|
|
18
|
+
msg = "This state is not in any group"
|
|
19
|
+
raise RuntimeError(msg)
|
|
20
|
+
return self._group
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def state(self) -> str | None:
|
|
24
|
+
if self._state is None or self._state == '*':
|
|
25
|
+
return self._state
|
|
26
|
+
|
|
27
|
+
if self._group_name is None and self._group:
|
|
28
|
+
group = self._group.__full_group_name__
|
|
29
|
+
elif self._group_name:
|
|
30
|
+
group = self._group
|
|
31
|
+
else:
|
|
32
|
+
group = "@"
|
|
33
|
+
|
|
34
|
+
return f"{group}:{self._state}"
|
|
35
|
+
|
|
36
|
+
def set_parent(self, group: type["StatesGroup"]) -> None:
|
|
37
|
+
if not issubclass(group, StatesGroup):
|
|
38
|
+
msg = "Group must be subclass of StatesGroup"
|
|
39
|
+
raise ValueError(msg)
|
|
40
|
+
self._group = group
|
|
41
|
+
|
|
42
|
+
def __set_name__(self, owner: type["StatesGroup"], name: str) -> None:
|
|
43
|
+
if self._state is None:
|
|
44
|
+
self._state = name
|
|
45
|
+
self.set_parent(owner)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def __str__(self):
|
|
49
|
+
return f"<State '{self.state or ''}'>"
|
|
50
|
+
|
|
51
|
+
__repr__ = __str__
|
|
52
|
+
|
|
53
|
+
def __call__(self, event: ViberObject, raw_state: str | None = None):
|
|
54
|
+
if self.state=='*':
|
|
55
|
+
return True
|
|
56
|
+
return raw_state == self.state
|
|
57
|
+
|
|
58
|
+
def __eq__(self, other: object) -> bool:
|
|
59
|
+
if isinstance(other, self.__class__):
|
|
60
|
+
return self.state == other.state
|
|
61
|
+
if isinstance(other, str):
|
|
62
|
+
return self.state == other
|
|
63
|
+
return NotImplemented
|
|
64
|
+
|
|
65
|
+
def __hash__(self) -> int:
|
|
66
|
+
return hash(self.state)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class StatesGroupMeta(type):
|
|
71
|
+
__parent__: type["StatesGroup"] | None
|
|
72
|
+
__childs__: tuple[type["StatesGroup"]]
|
|
73
|
+
__states__: tuple[State, ...]
|
|
74
|
+
__state_names__: tuple[str, ...]
|
|
75
|
+
__all_childs__: tuple[type["StatesGroup"], ...]
|
|
76
|
+
__all_states__: tuple[State, ...]
|
|
77
|
+
__all_states_names__: tuple[str, ...]
|
|
78
|
+
|
|
79
|
+
@no_type_check
|
|
80
|
+
def __new__(mcs, name, bases, namespace, **kwargs):
|
|
81
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
82
|
+
|
|
83
|
+
states = []
|
|
84
|
+
childs = []
|
|
85
|
+
|
|
86
|
+
for arg in namespace.values():
|
|
87
|
+
if isinstance(arg, State):
|
|
88
|
+
states.append(arg)
|
|
89
|
+
if inspect.isclass(arg) and issubclass(arg, StatesGroup):
|
|
90
|
+
child = cls._preprare_child(arg)
|
|
91
|
+
childs.append(child)
|
|
92
|
+
|
|
93
|
+
cls.__parent__ = None
|
|
94
|
+
cls.__childs__ = tuple(childs)
|
|
95
|
+
cls.__states__ = tuple(states)
|
|
96
|
+
cls.__state_names__ = tuple(state.state for state in states)
|
|
97
|
+
|
|
98
|
+
cls.__all_childs__ = cls._get_all_childs()
|
|
99
|
+
cls.__all_states__ = cls._get_all_states()
|
|
100
|
+
|
|
101
|
+
cls.__all_states_names__ = cls._get_all_states_names()
|
|
102
|
+
|
|
103
|
+
return cls
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def __full_group_name__(cls) -> str:
|
|
107
|
+
if cls.__parent__:
|
|
108
|
+
return f"{cls.__parent__.__full_group_name__}.{cls.__name__}"
|
|
109
|
+
return cls.__name__
|
|
110
|
+
|
|
111
|
+
def _preprare_child(cls, child: type["StatesGroup"]) -> type["StatesGroup"]:
|
|
112
|
+
child.__parent__ = cls
|
|
113
|
+
child.__all_states_names__ = child._all_states_names()
|
|
114
|
+
return child
|
|
115
|
+
|
|
116
|
+
def _get_all_childs(cls) -> tuple[type["StatesGroup"], ...]:
|
|
117
|
+
result = cls.__childs__
|
|
118
|
+
for child in cls.__childs__:
|
|
119
|
+
result += child.__childs__
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
def _get_all_states(cls) -> tuple[State, ...]:
|
|
123
|
+
states = cls.__states__
|
|
124
|
+
for group in cls.__childs__:
|
|
125
|
+
states += group.__all_states__
|
|
126
|
+
return states
|
|
127
|
+
|
|
128
|
+
def _get_all_states_names(cls) -> tuple[str, ...]:
|
|
129
|
+
return tuple(state.state for state in cls.__all_states__ if state.state)
|
|
130
|
+
|
|
131
|
+
def __contains__(cls, item: Any) -> bool:
|
|
132
|
+
if isinstance(item, str):
|
|
133
|
+
return item in cls.__all_states_names__
|
|
134
|
+
if isinstance(item, State):
|
|
135
|
+
return item in cls.__all_states__
|
|
136
|
+
if isinstance(item, StatesGroupMeta):
|
|
137
|
+
return item in cls.__all_childs__
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
def __str__(self) -> str:
|
|
141
|
+
return f"<StatesGroup '{self.__full_group_name__}'>"
|
|
142
|
+
|
|
143
|
+
def __iter__(self) -> Iterator[State]:
|
|
144
|
+
return iter(self.__all_states__)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class StatesGroup(metaclass=StatesGroupMeta):
|
|
148
|
+
@classmethod
|
|
149
|
+
def get_root(cls):
|
|
150
|
+
if cls.__parent__ is None:
|
|
151
|
+
return cls
|
|
152
|
+
return cls.__parent__.get_root()
|
|
153
|
+
|
|
154
|
+
def __call__(self, event: ViberObject, state_raw: str | None = None) -> bool:
|
|
155
|
+
return state_raw in type(self).__all_states_names__
|
|
156
|
+
|
|
157
|
+
def __str__(self) -> str:
|
|
158
|
+
return f"<StatesGroup '{type(self).__full_group_name__}'>"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
default_state = State(None)
|
|
162
|
+
any_state = State(state='*')
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from aiober.fsm.state import State
|
|
7
|
+
|
|
8
|
+
StateType = str | State | None
|
|
9
|
+
StorageKeyBuildType = Literal['state', 'data']
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class StorageKey:
|
|
14
|
+
user_id: str
|
|
15
|
+
chat_id: str
|
|
16
|
+
|
|
17
|
+
def build(self, type: StorageKeyBuildType):
|
|
18
|
+
return f"{self.chat_id}:{self.user_id}:{type}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseStorage(ABC):
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def set_state(self, key: StorageKey, state: StateType) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Set state for key
|
|
28
|
+
|
|
29
|
+
:key: storage key
|
|
30
|
+
:state: new state
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
async def get_state(self, key: StorageKey) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Get state by storage key
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def set_data(self, key: StorageKey, data: dict[str, Any]):
|
|
44
|
+
"""
|
|
45
|
+
Set data to storage key
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
async def get_data(self, key: StorageKey) -> dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
Get data by key
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
async def update_data(self, key: StorageKey, data: dict[str, Any]):
|
|
59
|
+
"""
|
|
60
|
+
Update data by key
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
current_data = await self.get_data(key=key)
|
|
64
|
+
current_data.update(data)
|
|
65
|
+
await self.set_data(key=key, data=current_data)
|
|
66
|
+
return current_data.copy()
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
async def close(self) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Close storage
|
|
72
|
+
"""
|
|
73
|
+
pass
|