Repository: timoniq/telegrinder (language: python)

Issue:
improve templates



Discussion / hints:

Fix patch:
diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
new file mode 100644
index 00000000..9f70c09e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -0,0 +1,32 @@
+---
+name: "\U0001F41B Bug report \U0001F41B"
+about: "Reporting a bug."
+title: ""
+labels: ["bug", "fix"]
+assignees: "luwqz1"
+---
+
+### ⚪️ Checklist ⚪️
+* [ ] ❕ I'm sure this is a **bug** in the **framework** ❕
+* [ ] 🔎 I have searched for **similar** *bugs* in `issues/PRs` 🔍
+* [ ] 🟢 I have the **latest** version of the **framework** installed 🟢
+
+### ✏️ Description ✏️
+<!-- Describe the bug -->
+`When I..., it...`
+
+### 🪄 Code example 🪄
+<!--  Provide a minimal reproducible example -->
+```python
+from telegrinder import API, Telegrinder, Token
+
+...
+```
+
+### 📝 Logs 📝
+<!-- Provide logs/errors to review the issue -->
+```
+Traceback (most recent call last):
+File "main.py", line 1, in <module>
+...
+```
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..f9874976
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+  - name: 💬 Telegram forum 💬
+    url: https://t.me/botoforum/148235
+    about: ✏️ You can ask questions about the framework. ✏️
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/new_feat.md b/.github/ISSUE_TEMPLATE/new_feat.md
new file mode 100644
index 00000000..a71701e4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/new_feat.md
@@ -0,0 +1,23 @@
+---
+name: "\U0001F680 New feature \U0001F680"
+about: "I have a suggest and may want to implement it."
+title: ""
+labels: ["new_feature"]
+assignees: "luwqz1"
+---
+
+## 🚀 Feature Request 🚀
+
+**✏️ Is your feature request related to a problem? Please describe ✏️**
+A clear and concise description of what the problem is. Ex.
+```
+I have an issue when...
+```
+
+**🪄 Provide a minimal reproducible example 🪄**
+A small code sample that will demonstrate your feature.
+```python
+```
+
+**✨ Teachability, Documentation, Adoption ✨**
+If you can, explain how users will be able to use this and possibly write out a version the docs.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..e5960eb5
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,20 @@
+### ✏️ Description ✏️
+<!-- 
+Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
+-->
+
+`🐛 Fixes # (issue)`
+
+### 📍 Type of change 📍
+
+<!-- Please delete options that are not relevant.-->
+
+- [ ] 🐛 Bug fix (non-breaking change which fixes an issue) 🐛
+- [ ] 🚀 New feature (non-breaking change which adds functionality) 🚀
+- [ ] ⚡ Breaking change (fix or feature that would cause existing functionality not to work as expected) ⚡
+- [ ] 📝 This change requires a documentation update 📝
+
+### ⚪️ Checklist ⚪️
+* [ ] 📝 Your code has documentation 📝
+* [ ] 🔧 Your code has tests 🔧
+* [ ] 🪄 Your code has example usage in `/examples` 🪄
\ No newline at end of file
diff --git a/.github/workflows/typegen.yml b/.github/workflows/typegen.yml
new file mode 100644
index 00000000..6d64b431
--- /dev/null
+++ b/.github/workflows/typegen.yml
@@ -0,0 +1,44 @@
+name: Generate Telegram Bot API types
+
+on:
+  workflow_dispatch: {}
+  schedule:
+    - cron: "0 13 * * *"
+
+jobs:
+  build:
+    name: Generate API types
+    runs-on: ubuntu-latest
+    steps:
+        - uses: actions/checkout@v4
+        
+        - name: Set up CPython 3.11
+          uses: actions/setup-python@v5
+          with:
+            python-version: "3.11"
+        
+        - name: Install poetry
+          run: >
+            pip install --upgrade pip setuptools wheel pipx
+            pipx install poetry
+      
+        - name: Install depends
+          run: poetry install
+      
+        - name: Run typegen
+          run: poetry run python typegen
+
+        - name: Open PR
+          uses: peter-evans/create-pull-request@v6
+          with:
+            commit-message: >
+              Update Bot API types based on the latest schema changes
+            title: >
+              Update Bot API types based on the latest schema changes
+            body: >
+              This is an automated PR. Please check the diff, and the action logs, to check for any funky behaviour.
+            branch: automated/types
+            reviewers: luwqz1
+            assignees: luwqz1
+            labels: automated, types
+            delete-branch: true
diff --git a/examples/single_choice.py b/examples/single_choice.py
index 91bdd930..b7aceb87 100755
--- a/examples/single_choice.py
+++ b/examples/single_choice.py
@@ -12,7 +12,7 @@
 @bot.on.message(Text("/choice"))
 async def action(m: Message):
     chosen, m_id = await (
-        SingleChoice(wm, m.chat.id, "Choose something", max_in_row=2)
+        SingleChoice(wm, m.chat.id, "Choose something", max_in_row=1)
         .add_option("apple", "Apple 🔴", "Apple 🟢")
         .add_option("banana", "Banana 🔴", "Banana 🟢", is_picked=True)
         .add_option("pear", "Pear 🔴", "Pear 🟢")
diff --git a/pyproject.toml b/pyproject.toml
index 5e500f96..4007da84 100755
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "telegrinder"
-version = "0.1.dev167"
+version = "0.1.dev168"
 description = "Framework for effective and reliable async telegram bot building."
 authors = ["timoniq <tesseradecades@mail.ru>"]
 maintainers = ["luwqz1 <howluwqz1@gmail.com>"]
@@ -46,6 +46,11 @@ typing-extensions = "^4.10.0"
 fntypes = "^0.1.2.post1"
 
 [tool.poetry.group.dev.dependencies]
+ruff = "^0.4.4"
+basedpyright = "^1.12.1"
+isort = "5.13.2"
+sort-all = "1.2.0"
+black = "^24.4.2"
 pytest = "^8.0.0"
 pytest-asyncio = "^0.23.5"
 pytest-cov = "^4.0.0"
@@ -58,7 +63,7 @@ requires = ["poetry-core>=1.0.0"]
 build-backend = "poetry.core.masonry.api"
 
 [tool.ruff]
-line-length = 100
+line-length = 110
 target-version = "py311"
 lint.select = ["I", "N", "ANN", "COM", "YTT", "TCH", "SIM"]
 lint.ignore = [
@@ -67,8 +72,8 @@ lint.ignore = [
 ]
 
 [tool.black]
-line-length = 90
-target-version = ["py311"]
+line-length = 110
+target-version = ["py311", "py312"]
 
 [tool.pytest.ini_options]
 asyncio_mode = "auto"
@@ -77,4 +82,7 @@ asyncio_mode = "auto"
 "__init__.py" = ["F401", "F403"]
 
 [tool.ruff.lint.flake8-quotes]
-docstring-quotes = "double"
\ No newline at end of file
+docstring-quotes = "double"
+
+[tool.basedpyright]
+typeCheckingMode = "basic"
\ No newline at end of file
diff --git a/readme.md b/readme.md
index 71a113d1..d9e15bfa 100755
--- a/readme.md
+++ b/readme.md
@@ -14,23 +14,23 @@ Still in development.
 
 Install using pip:
 
-```
+```console
 pip install telegrinder
 ```
 
 Using poetry:
 
-```
+```console
 poetry add telegrinder
 ```
 
 Install from github:
 
-```
+```console
 pip install -U https://github.com/timoniq/telegrinder/archive/dev.zip
 ```
 
-```
+```console
 poetry add git+https://github.com/timoniq/telegrinder.git#dev
 ```
 
diff --git a/telegrinder/bot/cute_types/callback_query.py b/telegrinder/bot/cute_types/callback_query.py
index 1acf27dc..ee5e9d58 100755
--- a/telegrinder/bot/cute_types/callback_query.py
+++ b/telegrinder/bot/cute_types/callback_query.py
@@ -217,7 +217,7 @@ async def delete(
     )
     async def edit_text(
         self,
-        text: str | None,
+        text: str,
         inline_message_id: int | None = None,
         chat_id: int | str | None = None,
         message_id: int | None = None,
diff --git a/telegrinder/bot/dispatch/dispatch.py b/telegrinder/bot/dispatch/dispatch.py
index ec94dc2a..54fc7bd1 100755
--- a/telegrinder/bot/dispatch/dispatch.py
+++ b/telegrinder/bot/dispatch/dispatch.py
@@ -135,7 +135,9 @@ def wrapper(func: typing.Callable):
 
     async def feed(self, event: Update, api: ABCAPI) -> bool:
         logger.debug("Processing update (update_id={})", event.update_id)
-        await self.raw_event.process(event, api)
+        loop = asyncio.get_running_loop()
+        loop.create_task(self.raw_event.process(event, api))
+        
         for view in self.get_views().values():
             if await view.check(event):
                 logger.debug(
@@ -143,10 +145,9 @@ async def feed(self, event: Update, api: ABCAPI) -> bool:
                     event.update_id,
                     view.__class__.__name__,
                 )
-                await view.process(event, api)
+                loop.create_task(view.process(event, api))
                 return True
 
-        loop = asyncio.get_running_loop()
         ctx = Context()
         found = False
         for handler in self.default_handlers:
diff --git a/telegrinder/bot/dispatch/handler/func.py b/telegrinder/bot/dispatch/handler/func.py
index 8b18d5d3..490b952c 100755
--- a/telegrinder/bot/dispatch/handler/func.py
+++ b/telegrinder/bot/dispatch/handler/func.py
@@ -46,7 +46,7 @@ def __repr__(self) -> str:
             self.dataclass,
             self.error_handler,
         )
-
+    
     async def check(self, api: ABCAPI, event: Update, ctx: Context | None = None) -> bool:
         if self.update_type is not None and self.update_type != event.update_type.unwrap_or_none():
             return False
diff --git a/telegrinder/bot/dispatch/view/abc.py b/telegrinder/bot/dispatch/view/abc.py
index 9c7c3eff..6d8bf8e2 100755
--- a/telegrinder/bot/dispatch/view/abc.py
+++ b/telegrinder/bot/dispatch/view/abc.py
@@ -67,13 +67,64 @@ def get_event_type(cls) -> Option[type[EventType]]:
                         return Some(generic_type)
         return Nothing()
 
-    @classmethod
-    def get_raw_event(cls, update: Update) -> Option[Model]:
+    @staticmethod
+    def get_raw_event(update: Update) -> Option[Model]:
         match update.update_type:
             case Some(update_type):
                 return getattr(update, update_type.value)
             case _:
                 return Nothing()
+    
+    @typing.overload
+    @classmethod
+    def to_handler(
+        cls,
+        *rules: ABCRule[EventType],
+    ) -> typing.Callable[
+        [FuncType[EventType]],
+        FuncHandler[EventType, FuncType[EventType], ErrorHandler[EventType]],
+    ]: ...
+
+    @typing.overload
+    @classmethod
+    def to_handler(
+        cls,
+        *rules: ABCRule[EventType],
+        error_handler: ErrorHandlerT,
+        is_blocking: bool = True,
+    ) -> typing.Callable[
+        [FuncType[EventType]], FuncHandler[EventType, FuncType[EventType], ErrorHandlerT]
+    ]: ...
+
+    @typing.overload
+    @classmethod
+    def to_handler(
+        cls,
+        *rules: ABCRule[EventType],
+        error_handler: typing.Literal[None] = None,
+        is_blocking: bool = True,
+    ) -> typing.Callable[
+        [FuncType[EventType]],
+        FuncHandler[EventType, FuncType[EventType], ErrorHandler[EventType]],
+    ]: ...
+
+    @classmethod
+    def to_handler(  # type: ignore
+        cls,
+        *rules: ABCRule[EventType],
+        error_handler: ABCErrorHandler | None = None,
+        is_blocking: bool = True,
+    ):
+        def wrapper(func: FuncType[EventType]):
+            return FuncHandler(
+                func,
+                list(rules),
+                is_blocking=is_blocking,
+                dataclass=None,
+                error_handler=error_handler or ErrorHandler(),
+            )
+
+        return wrapper
 
     @typing.overload
     def __call__(
diff --git a/telegrinder/bot/dispatch/waiter_machine/machine.py b/telegrinder/bot/dispatch/waiter_machine/machine.py
index 403fb369..b8971843 100755
--- a/telegrinder/bot/dispatch/waiter_machine/machine.py
+++ b/telegrinder/bot/dispatch/waiter_machine/machine.py
@@ -39,7 +39,7 @@ async def drop(
     ) -> None:
         view_name = state_view.__class__.__name__
         if view_name not in self.storage:
-            raise LookupError("No record of view {!r} found".format(view_name))
+            raise LookupError("No record of view {!r} found.".format(view_name))
 
         short_state = self.storage[view_name].pop(id, None)
         if short_state is None:
@@ -49,13 +49,7 @@ async def drop(
                 )
             )
 
-        waiters = typing.cast(
-            typing.Iterable[asyncio.Future[typing.Any]],
-            short_state.event._waiters,  # type: ignore
-        )
-        for future in waiters:
-            future.cancel()
-
+        short_state.cancel()
         await self.call_behaviour(
             state_view,
             short_state.event,
@@ -69,9 +63,9 @@ async def wait(
         state_view: "BaseStateView[EventModel]",
         linked: EventModel | tuple[ABCAPI, Identificator],
         *rules: ABCRule[EventModel],
-        default: Behaviour = None,
-        on_drop: Behaviour = None,
-        expiration: datetime.timedelta | int | None = None,
+        default: Behaviour[EventModel] | None = None,
+        on_drop: Behaviour[EventModel] | None = None,
+        expiration: datetime.timedelta | float | None = None,
     ) -> tuple[EventModel, Context]:
         if isinstance(expiration, int | float):
             expiration = datetime.timedelta(seconds=expiration)
@@ -86,6 +80,7 @@ async def wait(
         if not key:
             raise RuntimeError("Unable to get state key.")
 
+        view_name = state_view.__class__.__name__
         event = asyncio.Event()
         short_state = ShortState(
             key,
@@ -96,17 +91,17 @@ async def wait(
             default_behaviour=default,
             on_drop_behaviour=on_drop,
         )
-        view_name = state_view.__class__.__name__
+        
         if view_name not in self.storage:
             state_view.middlewares.insert(0, WaiterMiddleware(self, state_view))
             self.storage[view_name] = LimitedDict()
 
-        self.storage[view_name][key] = short_state
+        if (deleted_short_state := self.storage[view_name].set(key, short_state)) is not None:
+            deleted_short_state.cancel()
+        
         await event.wait()
-
-        e, ctx = getattr(event, "context")
         self.storage[view_name].pop(key, None)
-        return e, ctx
+        return getattr(event, "context")
 
     async def call_behaviour(
         self,
@@ -118,8 +113,10 @@ async def call_behaviour(
     ) -> None:
         # TODO: support param view as a behaviour
 
-        ctx = Context(**context)
+        if behaviour is None:
+            return
 
+        ctx = Context(**context)
         if isinstance(event, asyncio.Event):
             event, preset_ctx = typing.cast(
                 tuple[EventModel, Context],
@@ -127,9 +124,6 @@ async def call_behaviour(
             )
             ctx.update(preset_ctx)
 
-        if behaviour is None:
-            return
-
         if await behaviour.check(event.api, update, ctx):
             await behaviour.run(event, ctx)
         
diff --git a/telegrinder/bot/dispatch/waiter_machine/short_state.py b/telegrinder/bot/dispatch/waiter_machine/short_state.py
index cdf54646..bebffef1 100755
--- a/telegrinder/bot/dispatch/waiter_machine/short_state.py
+++ b/telegrinder/bot/dispatch/waiter_machine/short_state.py
@@ -34,6 +34,16 @@ class ShortState(typing.Generic[EventModel]):
 
     def __post_init__(self, expiration: datetime.timedelta | None = None) -> None:
         self.expiration_date = (datetime.datetime.now() - expiration) if expiration is not None else None
+    
+    def cancel(self) -> None:
+        """Cancel schedule waiters."""
+
+        waiters = typing.cast(
+            typing.Iterable[asyncio.Future[typing.Any]],
+            self.event._waiters,  # type: ignore
+        )
+        for future in waiters:
+            future.cancel()
 
 
 __all__ = ("ShortState",)
diff --git a/telegrinder/bot/rules/callback_data.py b/telegrinder/bot/rules/callback_data.py
index b3af60e1..790102ec 100755
--- a/telegrinder/bot/rules/callback_data.py
+++ b/telegrinder/bot/rules/callback_data.py
@@ -16,10 +16,8 @@
 
 CallbackQuery: typing.TypeAlias = CallbackQueryCute
 Validator: typing.TypeAlias = typing.Callable[[typing.Any], bool | typing.Awaitable[bool]]
-MapDict: typing.TypeAlias = dict[
-    str, "typing.Any | type[typing.Any] | Validator | list[MapDict] | MapDict"
-]
-CallbackMap: typing.TypeAlias = list[tuple[str, "typing.Any | type | Validator | CallbackMap"]]
+MapDict: typing.TypeAlias = dict[str, "typing.Any | type[typing.Any] | Validator | list[MapDict] | MapDict"]
+CallbackMap: typing.TypeAlias = list[tuple[str, "typing.Any | type[typing.Any] | Validator | CallbackMap"]]
 CallbackMapStrict: typing.TypeAlias = list[tuple[str, "Validator | CallbackMapStrict"]]
 
 
@@ -41,7 +39,7 @@ class CallbackQueryDataRule(CallbackQueryRule, abc.ABC, requires=[HasData()]):
 
 
 class CallbackDataMap(CallbackQueryDataRule):
-    def __init__(self, mapping: MapDict) -> None:
+    def __init__(self, mapping: MapDict, /) -> None:
         self.mapping = self.transform_to_callbacks(
             self.transform_to_map(mapping),
         )
@@ -86,12 +84,12 @@ async def run_validator(value: typing.Any, validator: Validator) -> bool:
             result = validator(value)
             if inspect.isawaitable(result):
                 result = await result
-            return result  # type: ignore
+            return result
 
         return False
 
     @classmethod
-    async def match(cls, callback_data: dict, callback_map: CallbackMapStrict) -> bool:
+    async def match(cls, callback_data: dict[str, typing.Any], callback_map: CallbackMapStrict) -> bool:
         """Matches callback_data with callback_map recursively."""
 
         for key, validator in callback_map:
@@ -121,7 +119,7 @@ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
 
 
 class CallbackDataEq(CallbackQueryDataRule):
-    def __init__(self, value: str):
+    def __init__(self, value: str, /) -> None:
         self.value = value
 
     async def check(self, event: CallbackQuery, ctx: Context) -> bool:
@@ -129,7 +127,7 @@ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
 
 
 class CallbackDataJsonEq(CallbackQueryDataRule):
-    def __init__(self, d: dict[str, typing.Any]):
+    def __init__(self, d: dict[str, typing.Any], /) -> None:
         self.d = d
 
     async def check(self, event: CallbackQuery, ctx: Context) -> bool:
@@ -137,7 +135,7 @@ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
 
 
 class CallbackDataJsonModel(CallbackQueryDataRule):
-    def __init__(self, model: type[msgspec.Struct] | type[DataclassInstance]):
+    def __init__(self, model: type[msgspec.Struct] | type[DataclassInstance], /) -> None:
         self.model = model
 
     async def check(self, event: CallbackQuery, ctx: Context) -> bool:
@@ -148,7 +146,7 @@ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
 
 
 class CallbackDataMarkup(CallbackQueryDataRule):
-    def __init__(self, patterns: PatternLike | list[PatternLike]):
+    def __init__(self, patterns: PatternLike | list[PatternLike], /) -> None:
         self.patterns = Markup(patterns).patterns
 
     async def check(self, event: CallbackQuery, ctx: Context) -> bool:
diff --git a/telegrinder/bot/scenario/checkbox.py b/telegrinder/bot/scenario/checkbox.py
index 322141ee..2095d2f7 100755
--- a/telegrinder/bot/scenario/checkbox.py
+++ b/telegrinder/bot/scenario/checkbox.py
@@ -33,13 +33,13 @@ def __init__(
         self,
         waiter_machine: WaiterMachine,
         chat_id: int,
-        msg: str,
+        message: str,
         *,
         ready_text: str = "Ready",
         max_in_row: int = 3,
     ) -> None:
         self.chat_id = chat_id
-        self.msg = msg
+        self.message = message
         self.choices: list[Choice] = []
         self.ready = ready_text
         self.max_in_row = max_in_row
@@ -58,7 +58,7 @@ def __repr__(self) -> str:
             self.waiter_machine,
             self.ready,
             self.chat_id,
-            self.msg,
+            self.message,
         )
 
     def get_markup(self) -> InlineKeyboardMarkup:
@@ -101,7 +101,7 @@ async def handle(self, cb: CallbackQueryCute) -> bool:
                 # Toggle choice
                 self.choices[i].is_picked = not self.choices[i].is_picked
                 await cb.edit_text(
-                    text=self.msg,
+                    text=self.message,
                     parse_mode=self.PARSE_MODE,
                     reply_markup=self.get_markup(),
                 )
@@ -118,7 +118,7 @@ async def wait(
         message = (
             await api.send_message(
                 chat_id=self.chat_id,
-                text=self.msg,
+                text=self.message,
                 parse_mode=self.PARSE_MODE,
                 reply_markup=self.get_markup(),
             )
diff --git a/telegrinder/bot/scenario/choice.py b/telegrinder/bot/scenario/choice.py
index 38547051..d4f50a23 100755
--- a/telegrinder/bot/scenario/choice.py
+++ b/telegrinder/bot/scenario/choice.py
@@ -1,6 +1,7 @@
 import typing
 
 from telegrinder.bot.cute_types import CallbackQueryCute
+from telegrinder.bot.dispatch.waiter_machine import WaiterMachine
 
 from .checkbox import Checkbox
 
@@ -22,9 +23,9 @@ async def handle(self, cb: CallbackQueryCute) -> bool:
             if choice.code == code:
                 self.choices[i].is_picked = True
                 await cb.ctx_api.edit_message_text(
+                    text=self.message,
                     chat_id=cb.message.unwrap().v.chat.id,
                     message_id=cb.message.unwrap().v.message_id,
-                    text=self.msg,
                     parse_mode=self.PARSE_MODE,
                     reply_markup=self.get_markup(),
                 )
@@ -36,10 +37,10 @@ async def wait(
         api: "API",
         view: "BaseStateView[CallbackQueryCute]",
     ) -> tuple[str, int]:
-        if len([choice for choice in self.choices if choice.is_picked]) != 1:
+        if len(tuple(choice for choice in self.choices if choice.is_picked)) != 1:
             raise ValueError("Exactly one choice must be picked")
         choices, m_id = await super().wait(api, view)
-        return list(choices.keys())[list(choices.values()).index(True)], m_id
+        return tuple(choices.keys())[tuple(choices.values()).index(True)], m_id
 
 
 __all__ = ("SingleChoice",)
diff --git a/telegrinder/tools/__init__.py b/telegrinder/tools/__init__.py
index 79a970af..2ba1b705 100755
--- a/telegrinder/tools/__init__.py
+++ b/telegrinder/tools/__init__.py
@@ -100,8 +100,8 @@
     "KeyboardSetBase",
     "KeyboardSetYAML",
     "Lifespan",
-    "Link",
     "LimitedDict",
+    "Link",
     "LoopWrapper",
     "Mention",
     "ParseMode",
diff --git a/telegrinder/tools/limited_dict.py b/telegrinder/tools/limited_dict.py
index bd501d2b..ebabbbc6 100644
--- a/telegrinder/tools/limited_dict.py
+++ b/telegrinder/tools/limited_dict.py
@@ -11,12 +11,22 @@ def __init__(self, *, maxlimit: int = 1000) -> None:
         self.maxlimit = maxlimit
         self.queue: deque[KT] = deque(maxlen=maxlimit)
 
-    def __setitem__(self, key: KT, value: VT, /) -> None:
+    def set(self, key: KT, value: VT, /) -> VT | None:
+        """Set item in the dictionary.
+        Returns a value that was deleted when the limit in the dictionary
+        was reached, otherwise None.
+        """
+
+        deleted_item = None
         if len(self.queue) >= self.maxlimit:
-            self.pop(self.queue.popleft(), None)
+            deleted_item = self.pop(self.queue.popleft(), None)
         if key not in self.queue:
             self.queue.append(key)
         super().__setitem__(key, value)
+        return deleted_item
+
+    def __setitem__(self, key: KT, value: VT, /) -> None:
+        self.set(key, value)
     
     def __delitem__(self, key: KT) -> None:
         if key in self.queue:
diff --git a/telegrinder/types/methods.py b/telegrinder/types/methods.py
index 1c1bacc0..3ef68718 100644
--- a/telegrinder/types/methods.py
+++ b/telegrinder/types/methods.py
@@ -139,9 +139,7 @@ async def delete_webhook(
         )
         return full_result(method_response, bool)
 
-    async def get_webhook_info(
-        self, **other: typing.Any
-    ) -> Result[WebhookInfo, APIError]:
+    async def get_webhook_info(self, **other: typing.Any) -> Result[WebhookInfo, APIError]:
         """Method `getWebhookInfo`, see the [documentation](https://core.telegram.org/bots/api#getwebhookinfo)
 
         Use this method to get current webhook status. Requires no parameters.
@@ -215,11 +213,7 @@ async def send_message(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -357,11 +351,7 @@ async def copy_message(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[MessageId, APIError]:
@@ -473,11 +463,7 @@ async def send_photo(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -546,11 +532,7 @@ async def send_audio(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -630,11 +612,7 @@ async def send_document(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -714,11 +692,7 @@ async def send_video(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -805,11 +779,7 @@ async def send_animation(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -889,11 +859,7 @@ async def send_voice(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -960,11 +926,7 @@ async def send_video_note(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -1020,9 +982,7 @@ async def send_video_note(
     async def send_media_group(
         self,
         chat_id: int | str,
-        media: list[
-            InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo
-        ],
+        media: list[InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo],
         business_connection_id: str | None = None,
         message_thread_id: int | None = None,
         disable_notification: bool | None = None,
@@ -1076,11 +1036,7 @@ async def send_location(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -1147,11 +1103,7 @@ async def send_venue(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -1216,11 +1168,7 @@ async def send_contact(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -1285,11 +1233,7 @@ async def send_poll(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -1373,11 +1317,7 @@ async def send_dice(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
@@ -2379,9 +2319,7 @@ async def delete_chat_sticker_set(
         )
         return full_result(method_response, bool)
 
-    async def get_forum_topic_icon_stickers(
-        self, **other: typing.Any
-    ) -> Result[list[Sticker], APIError]:
+    async def get_forum_topic_icon_stickers(self, **other: typing.Any) -> Result[list[Sticker], APIError]:
         """Method `getForumTopicIconStickers`, see the [documentation](https://core.telegram.org/bots/api#getforumtopiciconstickers)
 
         Use this method to get custom emoji stickers, which can be used as a forum
@@ -3011,9 +2949,7 @@ async def get_chat_menu_button(
         self,
         chat_id: int | None = None,
         **other: typing.Any,
-    ) -> Result[
-        Variative[MenuButtonCommands, MenuButtonWebApp, MenuButtonDefault], APIError
-    ]:
+    ) -> Result[Variative[MenuButtonCommands, MenuButtonWebApp, MenuButtonDefault], APIError]:
         """Method `getChatMenuButton`, see the [documentation](https://core.telegram.org/bots/api#getchatmenubutton)
 
         Use this method to get the current value of the bot's menu button in a private 
@@ -3028,8 +2964,7 @@ async def get_chat_menu_button(
             get_params(locals()),
         )
         return full_result(
-            method_response,
-            Variative[MenuButtonCommands, MenuButtonWebApp, MenuButtonDefault],
+            method_response, Variative[MenuButtonCommands, MenuButtonWebApp, MenuButtonDefault]
         )
 
     async def set_my_default_administrator_rights(
@@ -3424,11 +3359,7 @@ async def send_sticker(
         protect_content: bool | None = None,
         reply_parameters: ReplyParameters | None = None,
         reply_markup: (
-            InlineKeyboardMarkup
-            | ReplyKeyboardMarkup
-            | ReplyKeyboardRemove
-            | ForceReply
-            | None
+            InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None
         ) = None,
         **other: typing.Any,
     ) -> Result[Message, APIError]:
diff --git a/telegrinder/types/objects.py b/telegrinder/types/objects.py
index ddf9c15b..6865f19e 100644
--- a/telegrinder/types/objects.py
+++ b/telegrinder/types/objects.py
@@ -492,9 +492,7 @@ class ChatFullInfo(Model):
     personal_chat: Option["Chat"] = Nothing
     """Optional. For private chats, the personal channel of the user."""
 
-    available_reactions: Option[
-        list[Variative["ReactionTypeEmoji", "ReactionTypeCustomEmoji"]]
-    ] = Nothing
+    available_reactions: Option[list[Variative["ReactionTypeEmoji", "ReactionTypeCustomEmoji"]]] = Nothing
     """Optional. List of available reactions allowed in the chat. If omitted, 
     then all emoji reactions are allowed."""
 
@@ -647,12 +645,7 @@ class Message(MaybeInaccessibleMessage):
     bot chat which might share the same identifier."""
 
     forward_origin: Option[
-        Variative[
-            "MessageOriginUser",
-            "MessageOriginHiddenUser",
-            "MessageOriginChat",
-            "MessageOriginChannel",
-        ]
+        Variative["MessageOriginUser", "MessageOriginHiddenUser", "MessageOriginChat", "MessageOriginChannel"]
     ] = Nothing
     """Optional. Information about the original message for forwarded messages."""
 
@@ -941,9 +934,7 @@ def chat_title(self) -> str:
         Full name, for `private` chat."""
 
         return (
-            self.chat.full_name.unwrap()
-            if self.chat.type == ChatType.PRIVATE
-            else self.chat.title.unwrap()
+            self.chat.full_name.unwrap() if self.chat.type == ChatType.PRIVATE else self.chat.title.unwrap()
         )
 
     def __eq__(self, other: typing.Any) -> bool:
@@ -1049,10 +1040,7 @@ class ExternalReplyInfo(Model):
     """
 
     origin: Variative[
-        "MessageOriginUser",
-        "MessageOriginHiddenUser",
-        "MessageOriginChat",
-        "MessageOriginChannel",
+        "MessageOriginUser", "MessageOriginHiddenUser", "MessageOriginChat", "MessageOriginChannel"
     ]
     """Origin of the message replied to by the given message."""
 
@@ -1785,9 +1773,7 @@ class BackgroundTypeFill(BackgroundType):
     type: typing.Literal["fill"]
     """Type of the background, always `fill`."""
 
-    fill: Variative[
-        "BackgroundFillSolid", "BackgroundFillGradient", "BackgroundFillFreeformGradient"
-    ]
+    fill: Variative["BackgroundFillSolid", "BackgroundFillGradient", "BackgroundFillFreeformGradient"]
     """The background fill."""
 
     dark_theme_dimming: int
@@ -1829,9 +1815,7 @@ class BackgroundTypePattern(BackgroundType):
     document: "Document"
     """Document with the pattern."""
 
-    fill: Variative[
-        "BackgroundFillSolid", "BackgroundFillGradient", "BackgroundFillFreeformGradient"
-    ]
+    fill: Variative["BackgroundFillSolid", "BackgroundFillGradient", "BackgroundFillFreeformGradient"]
     """The background fill that is combined with the pattern."""
 
     intensity: int
@@ -1865,10 +1849,7 @@ class ChatBackground(Model):
     """
 
     type: Variative[
-        "BackgroundTypeFill",
-        "BackgroundTypeWallpaper",
-        "BackgroundTypePattern",
-        "BackgroundTypeChatTheme",
+        "BackgroundTypeFill", "BackgroundTypeWallpaper", "BackgroundTypePattern", "BackgroundTypeChatTheme"
     ]
     """Type of the background."""
 
@@ -3111,9 +3092,7 @@ def age(self) -> Option[int]:
         """Optional. Contains the user's age, if the user has a birth year specified."""
 
         return self.year.map(
-            lambda year: (
-                (datetime.now() - datetime(year, self.month, self.day)) // 365
-            ).days
+            lambda year: ((datetime.now() - datetime(year, self.month, self.day)) // 365).days
         )
 
 
@@ -3528,9 +3507,7 @@ class ChatBoost(Model):
     """Point in time (Unix timestamp) when the boost will automatically expire, 
     unless the booster's Telegram Premium subscription is prolonged."""
 
-    source: Variative[
-        "ChatBoostSourcePremium", "ChatBoostSourceGiftCode", "ChatBoostSourceGiveaway"
-    ]
+    source: Variative["ChatBoostSourcePremium", "ChatBoostSourceGiftCode", "ChatBoostSourceGiveaway"]
     """Source of the added boost."""
 
 
@@ -3562,9 +3539,7 @@ class ChatBoostRemoved(Model):
     remove_date: datetime
     """Point in time (Unix timestamp) when the boost was removed."""
 
-    source: Variative[
-        "ChatBoostSourcePremium", "ChatBoostSourceGiftCode", "ChatBoostSourceGiveaway"
-    ]
+    source: Variative["ChatBoostSourcePremium", "ChatBoostSourceGiftCode", "ChatBoostSourceGiveaway"]
     """Source of the removed boost."""
 
 
@@ -4196,9 +4171,7 @@ class InlineQueryResultGif(InlineQueryResult):
     gif_duration: Option[int] = Nothing
     """Optional. Duration of the GIF in seconds."""
 
-    thumbnail_mime_type: Option[
-        typing.Literal["image/jpeg", "image/gif", "video/mp4"]
-    ] = Nothing
+    thumbnail_mime_type: Option[typing.Literal["image/jpeg", "image/gif", "video/mp4"]] = Nothing
     """Optional. MIME type of the thumbnail, must be one of `image/jpeg`, `image/gif`, 
     or `video/mp4`. Defaults to `image/jpeg`."""
 
@@ -4259,9 +4232,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
     mpeg4_duration: Option[int] = Nothing
     """Optional. Video duration in seconds."""
 
-    thumbnail_mime_type: Option[
-        typing.Literal["image/jpeg", "image/gif", "video/mp4"]
-    ] = Nothing
+    thumbnail_mime_type: Option[typing.Literal["image/jpeg", "image/gif", "video/mp4"]] = Nothing
     """Optional. MIME type of the thumbnail, must be one of `image/jpeg`, `image/gif`, 
     or `video/mp4`. Defaults to `image/jpeg`."""
 
diff --git a/typegen/__init__.py b/typegen/__init__.py
index e7a29b91..50b44ecf 100755
--- a/typegen/__init__.py
+++ b/typegen/__init__.py
@@ -6,12 +6,12 @@
     MethodGenerator,
     ObjectGenerator,
     SchemaJson,
-    check_version,
     convert_schema_to_model,
     find_nicifications,
     generate,
     get_schema_json,
-    read_config_literal_types,
+    read_config_literals,
+    sort_all,
 )
 from .models import (
     MethodParameter,
@@ -36,10 +36,10 @@
     "ObjectSchema",
     "SchemaJson",
     "TelegramBotAPISchema",
-    "check_version",
     "convert_schema_to_model",
     "find_nicifications",
     "generate",
     "get_schema_json",
-    "read_config_literal_types",
+    "read_config_literals",
+    "sort_all",
 )
diff --git a/typegen/__main__.py b/typegen/__main__.py
index 30f69ba6..4149b1a7 100755
--- a/typegen/__main__.py
+++ b/typegen/__main__.py
@@ -1,4 +1,4 @@
-import generator
+from typegen.generator import generate
 
 if __name__ == "__main__":
-    generator.generate()
+    generate()
diff --git a/typegen/generator.py b/typegen/generator.py
index c22d8ba5..4eaf331e 100755
--- a/typegen/generator.py
+++ b/typegen/generator.py
@@ -1,12 +1,14 @@
 import datetime
 import os
+import pathlib
 import re
 import typing
 from abc import ABC, abstractmethod
 
 import msgspec
 import requests
-from models import (
+
+from .models import (
     MethodParameter,
     MethodSchema,
     ObjectField,
@@ -14,16 +16,16 @@
     TelegramBotAPISchema,
 )
 
-from telegrinder.modules import logger
-
-__version__ = "Bot API 7.3"
+try:
+    from telegrinder.modules import logger
+except ImportError:
+    import logging
+    logger = logging.getLogger("typegen")
 
 ModelT = typing.TypeVar("ModelT", bound=msgspec.structs.Struct)
 
 JSON_DECODER: typing.Final[msgspec.json.Decoder[typing.Any]] = msgspec.json.Decoder(strict=True)
-URL: typing.Final[str] = (
-    "https://raw.githubusercontent.com/PaulSonOfLars/telegram-bot-api-spec/main/api.json"
-)
+URL: typing.Final[str] = "https://raw.githubusercontent.com/PaulSonOfLars/telegram-bot-api-spec/main/api.json"
 TAB: typing.Final[str] = "    "
 TYPES: typing.Final[dict[str, str]] = {
     "String": "str",
@@ -33,15 +35,15 @@
     "Unixtime": "datetime",
 }
 INPUTFILE_DOCSTRING: typing.Final[str] = (
-    "to upload a new one using multipart/form-data under <file_attach_name> name."
+    "to upload a new one using multipart/form-data"
+    " under <file_attach_name> name."
 )
 MAIN_DIR: typing.Final[str] = "typegen"
 
 
 def get_schema_json() -> "SchemaJson":
-    logger.debug(f"Requesting {URL!r} to retrieve the schema...")
+    logger.debug(f"Sending a get request {URL!r}")
     raw = requests.get(URL).text
-    logger.debug("Schema successfully received! Decoding...")
     dct: dict[str, typing.Any] = JSON_DECODER.decode(raw)
     dct["methods"] = [d for d in dct["methods"].values()]
     dct["types"] = [d for d in dct["types"].values()]
@@ -54,11 +56,6 @@ def get_schema_json() -> "SchemaJson":
     return typing.cast(SchemaJson, dct)
 
 
-def is_new_version(schema: typing.Optional["SchemaJson"] = None) -> bool:
-    schema = schema or get_schema_json()
-    return schema["version"] != __version__
-
-
 def find_nicifications(name: str, path: str) -> tuple[str | None, list[str]]:
     with open(path, mode="r", encoding="UTF-8") as f:
         ns = f.read()
@@ -82,6 +79,15 @@ def chunks_str(s: str, sep: str = "\n"):
     return s.rstrip()
 
 
+def sort_all(path: pathlib.Path) -> None:
+    files = tuple(
+        str(p)
+        for p in path.iterdir()
+        if p.is_file() and p.suffix == ".py"
+    )
+    os.system(f"sort-all {' '.join(files)}")
+
+
 def convert_to_python_type(
     tp: str,
     parent_types: dict[str, list[str]] | None = None,
@@ -123,7 +129,7 @@ def convert_schema_to_model(schema_json: "SchemaJson", model: type[ModelT]) -> M
     return schema
 
 
-def read_config_literal_types(path: str | None = None) -> "ConfigLiteralTypes":
+def read_config_literals(path: str | None = None) -> "ConfigLiteralTypes":
     path = path or MAIN_DIR + "/config_literal_types.json"
     with open(path, mode="r", encoding="UTF-8") as f:
         return JSON_DECODER.decode(f.read())
@@ -424,7 +430,9 @@ def make_type_hint(
         )
 
     def get_param_literal_types(
-        self, method_name: str, param_name: str
+        self,
+        method_name: str,
+        param_name: str,
     ) -> ParamLiteralTypes | None:
         for cfg in self.config_literal_types:
             if cfg["name"] == method_name:
@@ -566,27 +574,25 @@ def generate(self, path: str) -> None:
 
 
 def generate(
-    path: str | None = None,
-    path_cfg_literal_types: str | None = None,
+    *,
+    path_dir: str | None = None,
+    path_config_literals: str | None = None,
+    path_nicifications: str | None = None,
     types_generator: ABCGenerator | None = None,
     methods_generator: ABCGenerator | None = None,
 ) -> None:
-    path = path or "telegrinder/types"
-    if not os.path.exists(path):
-        logger.warning(f"Path dir {path!r} not found. Making dir...")
-        os.makedirs(path)
-
-    schema_json = get_schema_json()
-    if not is_new_version(schema_json):
-        logger.warning("Schema already updated to the latest version!")
-        return
+    path_dir = path_dir or "telegrinder/types"
+    if not os.path.exists(path_dir):
+        logger.warning(f"Path dir {path_dir!r} not found. Making dir...")
+        os.makedirs(path_dir)
 
     if types_generator is None or methods_generator is None:
+        schema_json = get_schema_json()
         schema_model = convert_schema_to_model(schema_json, TelegramBotAPISchema)
-        cfg_literal_types = read_config_literal_types(path_cfg_literal_types)
+        cfg_literal_types = read_config_literals(path_config_literals)
         object_generator = types_generator or ObjectGenerator(
             objects=schema_model.objects,
-            nicification_path=MAIN_DIR + "/nicifications.py",
+            nicification_path=MAIN_DIR + (path_nicifications or "/nicifications.py"),
             config_literal_types=cfg_literal_types.get("objects"),
         )
         method_generator = methods_generator or MethodGenerator(
@@ -601,21 +607,24 @@ def generate(
             release_date=schema_json["release_date"],
         )
 
-    object_generator.generate(path)
-    method_generator.generate(path)
+    object_generator.generate(path_dir)
+    method_generator.generate(path_dir)
     logger.info("Schema has been successfully generated.")
 
     logger.debug("Run black formatter...")
-    if os.system(f"black {path} --config pyproject.toml") != 0:
+    if os.system(f"black {path_dir} --config pyproject.toml") != 0:
         logger.error("Black formatter failed.")
     else:
         logger.info("Black formatter successfully formatted files.")
 
     logger.debug("Run isort...")
-    if os.system(f"isort {path}") != 0:
+    if os.system(f"isort {path_dir}") != 0:
         logger.error("Isort failed.")
     else:
         logger.info("Isort successfully sorted imports.")
+    
+    logger.debug("Run sort-all...")
+    sort_all(pathlib.Path(path_dir))
 
 
 __all__ = (
@@ -628,10 +637,10 @@ def generate(
     "ObjectGenerator",
     "ParamLiteralTypes",
     "SchemaJson",
-    "is_new_version",
     "convert_schema_to_model",
     "find_nicifications",
     "generate",
+    "sort_all",
     "get_schema_json",
-    "read_config_literal_types",
+    "read_config_literals",
 )


Test patch:
diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml
new file mode 100644
index 00000000..a4a5e28a
--- /dev/null
+++ b/.github/workflows/lint-and-test.yml
@@ -0,0 +1,37 @@
+name: Lint and test
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+        - uses: actions/checkout@v4
+        - name: Set up CPython 3.11
+          uses: actions/setup-python@v5
+          with:
+            python-version: "3.11"
+        
+        - name: Install poetry
+          run: >
+            pip install pipx
+            pipx install poetry
+        
+        - name: Poetry check structure
+          run: >
+            poetry check
+        
+        - name: Install depends
+          run: >
+            poetry install
+        
+        - name: Lint code with ruff
+          run: >
+            poetry run ruff check --fix
+                
+        - name: Check types with basedpyright
+          run: >
+            poetry run basedpyright --level ERROR
+        
+        # NOTE: add run tests