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.
Files changed (49) hide show
  1. aiober-0.0.1/LICENSE +21 -0
  2. aiober-0.0.1/PKG-INFO +72 -0
  3. aiober-0.0.1/README.md +52 -0
  4. aiober-0.0.1/aiober/__init__.py +6 -0
  5. aiober-0.0.1/aiober/client/__init__.py +5 -0
  6. aiober-0.0.1/aiober/client/bot.py +42 -0
  7. aiober-0.0.1/aiober/client/context_controller.py +21 -0
  8. aiober-0.0.1/aiober/client/session/base.py +34 -0
  9. aiober-0.0.1/aiober/client/session/request.py +68 -0
  10. aiober-0.0.1/aiober/client/session/viber.py +12 -0
  11. aiober-0.0.1/aiober/filters/__init__.py +3 -0
  12. aiober-0.0.1/aiober/filters/base.py +13 -0
  13. aiober-0.0.1/aiober/filters/state.py +30 -0
  14. aiober-0.0.1/aiober/filters/text.py +22 -0
  15. aiober-0.0.1/aiober/fsm/__init__.py +0 -0
  16. aiober-0.0.1/aiober/fsm/context.py +27 -0
  17. aiober-0.0.1/aiober/fsm/state.py +162 -0
  18. aiober-0.0.1/aiober/fsm/storage/__init__.py +3 -0
  19. aiober-0.0.1/aiober/fsm/storage/base.py +73 -0
  20. aiober-0.0.1/aiober/fsm/storage/memory.py +31 -0
  21. aiober-0.0.1/aiober/fsm/storage/redis.py +88 -0
  22. aiober-0.0.1/aiober/methods/__init__.py +7 -0
  23. aiober-0.0.1/aiober/methods/base.py +31 -0
  24. aiober-0.0.1/aiober/methods/methods.py +8 -0
  25. aiober-0.0.1/aiober/methods/send_message.py +57 -0
  26. aiober-0.0.1/aiober/router/__init__.py +4 -0
  27. aiober-0.0.1/aiober/router/dispatcher.py +100 -0
  28. aiober-0.0.1/aiober/router/event/__init__.py +2 -0
  29. aiober-0.0.1/aiober/router/event/event.py +33 -0
  30. aiober-0.0.1/aiober/router/event/handler.py +39 -0
  31. aiober-0.0.1/aiober/router/router.py +52 -0
  32. aiober-0.0.1/aiober/types/__init__.py +48 -0
  33. aiober-0.0.1/aiober/types/base.py +37 -0
  34. aiober-0.0.1/aiober/types/color.py +14 -0
  35. aiober-0.0.1/aiober/types/conversation_started.py +37 -0
  36. aiober-0.0.1/aiober/types/keyboard.py +84 -0
  37. aiober-0.0.1/aiober/types/massage.py +95 -0
  38. aiober-0.0.1/aiober/types/rich_media.py +30 -0
  39. aiober-0.0.1/aiober/types/seen.py +7 -0
  40. aiober-0.0.1/aiober/types/subscribed.py +24 -0
  41. aiober-0.0.1/aiober/types/user.py +9 -0
  42. aiober-0.0.1/aiober.egg-info/PKG-INFO +72 -0
  43. aiober-0.0.1/aiober.egg-info/SOURCES.txt +47 -0
  44. aiober-0.0.1/aiober.egg-info/dependency_links.txt +1 -0
  45. aiober-0.0.1/aiober.egg-info/requires.txt +4 -0
  46. aiober-0.0.1/aiober.egg-info/top_level.txt +1 -0
  47. aiober-0.0.1/pyproject.toml +39 -0
  48. aiober-0.0.1/setup.cfg +4 -0
  49. 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,6 @@
1
+ from .router import (
2
+ Router,
3
+ Dispatcher
4
+ )
5
+
6
+ from aiober.client import Bot
@@ -0,0 +1,5 @@
1
+ from .bot import Bot
2
+
3
+ from .session.base import BaseSession
4
+ from .session.request import AiohttpSession
5
+ from .session.viber import ViberAPIServer
@@ -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,12 @@
1
+
2
+ class ViberAPIServer:
3
+ def __init__(self, *, base: str):
4
+ self.base = base
5
+
6
+ def get_api_url(self, method: str):
7
+ return self.base.format(method=method)
8
+
9
+
10
+ PRODUCTION = ViberAPIServer(
11
+ base='https://chatapi.viber.com/pa/{method}'
12
+ )
@@ -0,0 +1,3 @@
1
+ from .base import BaseFilter
2
+ from .text import TextFilter, StartsWithFilter
3
+ from .state import StateFilter
@@ -0,0 +1,13 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class BaseFilter(ABC):
5
+
6
+ def __init__(self):
7
+ ...
8
+
9
+ @abstractmethod
10
+ async def __call__(self, *args, **kwargs):
11
+ pass
12
+
13
+
@@ -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,3 @@
1
+ from .base import BaseStorage, StorageKey
2
+ from .memory import MemoryStorage
3
+ from .redis import RedisStorage
@@ -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