Cluster · разработчикам

Редактор и типы SDK

Первый модуль · Справочник методов · Типы результатов

Установка одной командой

Нужен Python 3.12 или новее. Скачай установщик, затем выполни:

python3 cluster-sdk-setup.py ./my-module --name "My module"

Для существующего проекта укажи его каталог и добавь --existing, чтобы подготовить окружение без шаблона модуля. Установщик создаёт .venv, ставит cluster-sdk с Pyright, подключает интерпретатор к настройкам редактора и создаёт основу модуля, если её ещё нет. На Windows команду можно выполнить через py -3 с установленным Python 3.12 или новее.

Существующие исходники и manifest.json не заменяются. Пользовательские параметры редактора сохраняются; пути интерпретатора обновляются с резервной копией JSON. Подробности и исключения для настроек с комментариями описаны в командах проекта.

Установщик использует прямую HTTPS-ссылку на конкретный wheel с проверкой SHA-256. Снятие проверки сертификата не требуется. Ключи сайта, Telegram и доступ к ядру для установки SDK не нужны.

После установки:

./my-module/.venv/bin/python -m cluster_sdk check ./my-module --types
./my-module/.venv/bin/python -m cluster_sdk pack ./my-module

На Windows интерпретатор находится в my-module\.venv\Scripts\python.exe.

Установка в готовое окружение

Если окружение уже создано и выбрано в редакторе:

python -m pip install "cluster-sdk[dev] @ https://cluster.ninepeaks.dev/developers/downloads/cluster_sdk-0.1.0-py3-none-any.whl"
python -m cluster_sdk init . --existing

Скачать SDK.

Пакет устанавливается как cluster-sdk, импортируется как cluster_sdk. В нём есть публичные классы и конструкторы, рабочая клиентская реализация, объявления .pyi и команды подготовки, проверки и сборки проекта. Ядро, серверный транспорт, сессии, ключи и серверные проверки прав сюда не входят.

Старый пакет cluster-sdk-types содержал только объявления. Если он установлен в этом же окружении, удали его перед ручной установкой SDK:

python -m pip uninstall cluster-sdk-types

Установщик делает эту замену сам внутри выбранного окружения проекта. Для SDK не нужны extraPaths на исходники Cluster или вручную скопированная папка .types. Неизменённые старые объявления в stubPath команда init сохраняет в резервной папке; изменённые пользователем объявления не удаляются.

SDK устанавливается в окружение разработки. В requirements пакета модуля добавляются его собственные зависимости PyPI; рабочий SDK уже предоставляет Cluster.

Настройка Pyright и Pylance

Создай pyrightconfig.json в корне папки с модулями:

{
    "typeCheckingMode": "basic",
    "pythonVersion": "3.12",
    "reportMissingImports": "error",
    "reportMissingModuleSource": "none"
}

reportMissingImports продолжает показывать отсутствующие импорты. init создаёт конфигурацию автоматически. В готовом проекте можно сохранить свои параметры. Настраивать extraPaths на исходники Cluster не нужно.

Импорты в модуле

app и loader передаются загрузчиком Cluster. В локальном редакторе объяви их типы внутри TYPE_CHECKING:

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from cluster_sdk import App, Event, loader
    app: App

class Module:
    @loader.command("hello", access="owner", source="both")
    async def hello(self, ctx: Event) -> None:
        text = "Привет." if ctx.language == "ru" else "Hello."
        await ctx.reply(text)

Редактор читает этот блок и даёт подсказки. Python при запуске его пропускает; from __future__ import annotations позволяет использовать типы в сигнатурах без выполнения этих импортов. Рабочие app и loader предоставляет Cluster.

Во внутренних файлах собственного модуля импортируй типы тем же способом:

from __future__ import annotations

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from cluster_sdk import App, Record

class Storage:
    def __init__(self, app: App) -> None: self.app = app

    @asynccontextmanager
    async def record(self) -> AsyncIterator[Record]:
        async with self.app.db.record("state", {}) as record: yield record

Если класс нужен во время выполнения, например Canvas для описания картинки, импорт размещается обычно: from cluster_sdk import Canvas. Публичные конструкторы доступны и в установленном SDK, и в среде Cluster.

Не создавай второй App вместо переданного app: запросы модуля привязаны к его текущему событию и транспорту. Не используй старые служебные импорты from app, from router и from resources в новых модулях.

Проверка

В выбранном окружении можно проверить типы отдельно:

.venv/bin/python -m pip install pyright
.venv/bin/pyright --pythonpath .venv/bin/python main.py

Для проверки поведения используй отдельную тестовую среду и проверку перед публикацией. Она загружает модуль с рабочим SDK внутри изоляции. Установка типов в локальный редактор не выдаёт модулю прав на Telegram, файлы или сеть.

Объявления строятся из тех же публичных контрактов, что справочник методов. Проверка покрытия сравнивает классы, аргументы и сигнатуры. В распространяемый SDK попадают публичные клиенты и конструкторы из явного списка; ядро и серверный ввод-вывод не включаются. При обновлении переустанови текущий SDK с --upgrade и повтори python -m cluster_sdk init ..

Точные типы своих данных

dict без параметров не описывает содержимое словаря. Редактор знает только сам контейнер и показывает dict[Unknown, Unknown]. Для однородных данных укажи оба типа: dict[str, int], dict[str, list[int]]. Для объекта с разными полями используй TypedDict.

db.get() и record.get() возвращают JSON: заранее неизвестно, что хранится под ключом. Аргумент default задаёт значение отсутствующей записи, но не проверяет уже сохранённую запись. Поэтому SDK не выдаёт тип значения по одному только default.

Опиши структуру и проверь её при чтении:

from __future__ import annotations

from collections.abc import Mapping
from typing import TYPE_CHECKING, TypedDict, cast

if TYPE_CHECKING: from cluster_sdk import App

class State(TypedDict):
    count: int
    users: list[int]


def state(value: object) -> State:
    if not isinstance(value, Mapping): raise ValueError("Expected a state object")
    fields = cast(Mapping[str, object], value)
    count, raw_users = fields.get("count"), fields.get("users")
    if type(count) is not int or not isinstance(raw_users, list):
        raise ValueError("Invalid state fields")
    users = cast(list[object], raw_users)
    if any(type(user) is not int for user in users): raise ValueError("Invalid user ID")
    return {"count": count, "users": cast(list[int], users)}


async def increment(app: App) -> State:
    default: State = {"count": 0, "users": []}
    document = app.document()
    async with app.db.record("state", document.encode(default)) as record:
        value = record.decode(state)
        value["count"] += 1
        record.set((), record.encode(value))
        return value

decode(decoder, path=()) передаёт копию значения проверяющей функции и сохраняет тип её результата. В этом примере value имеет тип State, value["count"]: int, а value["users"]: list[int]. Метод доступен у Document и наследующих его Record, ConfigView, Settings, Page. Можно проверить отдельное вложенное значение через path.

Ошибки проверяющей функции передаются без подмены. Проверка и изменение полученной копии не меняют документ: для записи нужен set(), а для отправки в ядро: save() либо успешный выход из контекстного менеджера записи. Если проверка завершилась ошибкой внутри async with, запись автоматически не сохраняется.

encode(value) проверяет JSON-совместимость собственного TypedDict и возвращает глубокую копию с типом JSON. Он не превращает произвольный объект в словарь и не приводит строки к числам. Для сохранения нетипичного объекта сначала явно опиши его представление в JSON.

Проверка isinstance(value, dict) уточняет только вид контейнера. Она не проверяет поля и вложенные списки. Один cast(State, value) также ничего не проверяет во время выполнения. В примере приведения применяются после проверки соответствующих значений; они не заменяют эту проверку.

Настройки проходят проверку по схеме модуля в ядре. Для них можно описать собственный TypedDict и уточнить тип в одном методе чтения. Произвольные данные из базы, файла или сети проверяй отдельно.

Пагинация и обработчики

Pages[T] сохраняет тип элементов, в том числе после переключения страницы:

from __future__ import annotations

from typing import TYPE_CHECKING
if TYPE_CHECKING: from cluster_sdk import App


def page(app: App) -> list[int]:
    pages = app.inline.pages([101, 102, 103], size=2)
    return pages.next().current

Здесь результат: list[int]. Для списка собственных TypedDict получится список того же типа. Строки клавиатуры содержат Button, слои сцены: LayerData, сведения о файле: FileInfo, задачи: TaskInfo. Их поля описаны в типах результатов.

Декораторы loader.command, handler, callback, action, function, signal, task, request, init, stop сохраняют аргументы и возвращаемый тип функции. Поддерживаются обе формы: @loader.callback и @loader.callback(...). loader.thread сохраняет аргументы синхронной функции, а результат нужно получить через await.

У методов app.bot.api и app.client.api результат имеет тип JSON; конструкторы types возвращают JSON-описания. Это динамические пространства имён: наличие имени и набор параметров проверяются по схеме Bot API или Telethon во время вызова. Типы редактора не подменяют эту проверку. Формы конкретных объектов и параметры доступны в справочнике Telegram.

Проверка неполных подсказок

Чтобы Unknown не оставался незаметным, добавь в pyrightconfig.json:

{
    "reportMissingTypeArgument": "error",
    "reportUnknownParameterType": "error",
    "reportUnknownVariableType": "error",
    "reportUnknownMemberType": "error",
    "reportUnknownArgumentType": "error",
    "reportUnknownLambdaType": "error"
}

Эти параметры дополняют конфигурацию выше. Они находят пропущенные параметры контейнеров и места, в которых потерян тип. Не исправляй их заменой всего на Any: сначала уточни структуру данных, тип пустого списка или словаря и возвращаемый тип функции.

Event используется для команд, кнопок, задач и других событий. У задачи может не быть сообщения и чата. Перед использованием ctx.chat_id, ctx.sender_id или ctx.id напрямую проверь, что нужное поле не равно None. Методы ctx.reply() и ctx.edit() выполняют свою проверку контекста.

Устанавливаемый SDK включает уточнённые типы полей объектов и результатов, типизированную пагинацию и декораторы. Document.encode() и Document.decode() доступны в рабочем SDK. Полные сигнатуры, права и ошибки приведены в справочнике.