Документация · keepup-admin 0.5.0

Как собрать приложение на keepup

От установки до нескольких реплик против одной базы. Всё, что здесь названо, — публичный интерфейс пакета: имена из __all__ его модулей.

Начало работы

Нужен Python 3.11 или новее. Для разработки хватит SQLite, в работе — PostgreSQL.

python3 -m venv .venv && . .venv/bin/activate
# полная панель: HTTP-слой и вход, разделы, база и один драйвер
pip install "keepup-admin[panel,postgres]" uvicorn

Возможности ставятся extras: один экстра — один дистрибутив. База — только конструктор: ядро, каталог плагинов, реестр служб, жизненный цикл и шов транспорта. Она не объявляет ни одной чужой библиотеки, даже FastAPI; каждая возможность объявляет свои.

ДистрибутивЧто даёт
keepup-adminЯдро: дескрипторы, каталог, реестр служб, жизненный цикл, настройки, роли, блокировки, шина, кэш.
keepup-dbDatabaseManagerV2, tables, retention, SQLAlchemy.
keepup-authВход, сессии панели, роли, OIDC, чужой провайдер личности.
keepup-usersТаблицы учётных записей, ролей и прав.
keepup-uiТемы панели.
keepup-auditАудит входящих запросов, события, след администратора.
keepup-metricsСборщик, /metrics и хранение снимков.
keepup-integration-logИсходящие вызовы во внешние системы.
keepup-postgresДрайвер PostgreSQL и шина LISTEN/NOTIFY.
keepup-sqliteДрайвер SQLite и шина «в своём процессе».
keepup-tasksПланировщик и службы locks и scheduler.
keepup-modulesКаталог разделов, выдачи ролей, решение о плагинах.
keepup-clusterРеестр реплик и команды им.
keepup-log-shippingДоставка журнала сборщику.
keepup-httpcreate_app, оболочка панели, маршруты, FastAPI и Starlette.

Экстра panel ставит всё, что нужно панели, кроме драйвера; драйвер выбирают отдельно — postgres в работе, sqlite для разработки. Два драйвера одной обязательной службы останавливают старт намеренно.

Приложение — это файл, который передаёт настройки и получает ASGI-приложение. Таблицы, объявленные плагинами (get_declared_tables()), ядро создаёт само при старте; первые учётные записи создаёт keepup-auth:

main.py
import os

from keepup import KeepupSettings, create_app

PLUGINS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "plugins")

app = create_app(KeepupSettings(
    title="Your product",
    project_name="yours",
    plugins_dir=PLUGINS_DIR,
    static_mounts=(),
))
config/modules.json
{"plugins": [], "roles": []}
Что приносит экстра panel. keepup-http отдаёт оболочку панели из shell_mount — она лежит внутри пакета, копировать её к себе не нужно. keepup-auth регистрирует вход, и корень / отвечает без клиентской страницы приложения. static_mounts=() нужен только тогда, когда у приложения нет своего каталога static/: иначе create_app падает на монтировании несуществующего каталога.

Два значения задаёт тот, кто разворачивает, а не код:

export SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(48))')"
export ADMIN_INITIAL_PASSWORD="придумайте-свой"
uvicorn main:app --port 8000
Без SECRET_KEY приложение не стартует. Ключ подписи сессий обязан быть не короче 32 символов, и заведомые заглушки отвергаются. Если ADMIN_INITIAL_PASSWORD не задан, пароль учётной записи admin генерируется случайно и печатается в журнал запуска один раз.

Что после этого отвечает:

  • /selfcare — панель администрирования: вход под admin, восемь разделов; оболочку и разделы ставит keepup-http.
  • /api/docs и /api/redoc — описание API.
  • /api/health — здоровье процесса.

База по умолчанию — SQLite в data/keepup_db относительно рабочего каталога.

Проверки

Дерево для разработки ставится из репозитория: pip install -e ".[test]" -e packages/keepup-*. Одна команда прогоняет всё: .github/scripts/check.sh — линт, набор с покрытием, выпуск и граф, аудит безопасности и сборку всех колёс; --fast пропускает колёса и аудит. Тесты в дереве называются *_tests.py и запускаются по пути: python -m pytest tests/your_tests.py -v.

Раскладка проекта

Каркас читает данные относительно рабочего каталога процесса, а код плагинов — из каталога, который назовёт приложение. Поэтому запускайте процесс из корня своего приложения:

yours/
├── main.py                   # KeepupSettings -> app
├── plugins/
│   └── hello.py              # class HelloPlugin(BasePlugin)
├── config/
│   ├── modules.json          # плагины, разделы панели, роли
│   ├── auth.yaml             # provider: local (необязателен)
│   └── postgres.properties   # где база (необязателен)
└── static/                   # свой фронтенд, если он есть

Каталог плагинов вычисляйте от расположения файла, а не от рабочего каталога — тогда плагины находятся, откуда бы ни запустили процесс:

import os

PLUGINS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "plugins")
# Дальше каталог называет настройка: plugins_dir=PLUGINS_DIR.

Два приложения на keepup в одном каталоге — ошибка, а не беспорядок: при старте каталог разделов панели дополняется из config/modules.json, и второе приложение получило бы разделы первого.

KeepupSettings

Единственная точка настройки. Всё, чего каркас не может знать сам, приходит сюда; значения по умолчанию закрывают, а не открывают.

ПолеПо умолчаниюЧто задаёт
title"KeepUP"Имя приложения и заголовок описания API.
project_name"keepup"Имя проекта в журналах.
plugins_dirNoneКаталог, из которого ядро грузит плагины.
plugins_config_path"config/modules.json"Объявление плагинов, разделов и ролей.
plugin_managerNoneСвой экземпляр PluginManager, если приложение собрало его само.
profileNoneПрофиль — заплатка поверх каталога: "panel" или "metrics-only".
database_dialectNoneКакой драйвер запускать: "postgresql" или "sqlite".
cors_origins()Источники, которым можно читать ответы. Пусто — никому.
metrics_publicFalseОтдавать ли /metrics без учётных данных.
security_headersTrueЗаголовки безопасности на каждом ответе.
content_security_policyПолитика каркасаПолитика содержимого для панели; None не отправляет её.
csp_report_onlyFalseОтправлять политику как отчёт, ничего не запрещая.
max_upload_bytesNoneПредел для маршрута, который читает тело сам; None — решает маршрут.
max_json_bytes2 МиБПредел тела, которое разбирает каркас; None — без предела.
performanceпустой объектПул соединений, буфер аудита и интервал метрик одним объектом.
docs_url, redoc_url"/api/docs", "/api/redoc"Где описание API; None убирает.
openapi_url"/openapi.json"Где отдаётся схема OpenAPI; None убирает её.
openapi_publicFalseОтвечать ли схему кому угодно, а не только администратору.
static_dir, static_mounts"static", семь точекСвоя статика; StaticMount(path, directory, name).
shell_mount"/keepup-static"Где отдаётся оболочка панели из keepup-http.
client_page"main.html"Страница, которой отвечает /.
built_in_themes()Темы приложения: (name, page_file, brand, logo).
gated_pages{}Страница отдаётся, только пока работает названный плагин.
audit_redactionNoneЧто аудит пишет из тела запроса; по умолчанию — только имена полей.
password_ruleNone(password, username) -> текст проблемы или None.
oidc, oidc_account_policyNoneВход через внешний провайдер и кого впускать.
identity_providerNoneЧужая система личности, когда её настраивают кодом.
notification_channelNoneКанал шины между репликами; без него шины нет.
remote_log_url, remote_log_tokenNoneСборщик логов и учётные данные к нему.
public_configNoneЧто показывать на экране до входа.
record_login, pending_documentsNoneЧто делать после входа; какие документы пользователь ещё не принял.
declared_event_typesNoneТипы событий для фильтра в панели.
on_startup, on_shutdownNoneКорутины (app) до плагинов и после них.

profile — заплатка поверх каталога плагинов (plugins/builtin.json объявляет «panel» и «metrics-only»), а не второй формат настройки: один код служит полной панелью, воркером и процессом, который отвечает только /metrics. Профиль metrics-only заменил удалённую настройку disable_http_server. Настройки app_tables и extra_setup и модуль keepup.schema с его init_db() удалены в 0.5.0.

Плагины и маршруты

Возможность приложения — плагин: класс-наследник BasePlugin в своём файле. Каркас грузит его по объявлению в config/modules.json.

Имя класса выводится из id

Файл — plugins/<id>.py, класс — f"{id.capitalize()}Plugin": hello → HelloPlugin, integration_logs → Integration_logsPlugin. Ошибка в имени не роняет старт: плагин просто не загружается, а в журнале остаётся предупреждение.

Хуки

МетодКогда и зачем
__init__(self, config=None)Каркас передаёт один аргумент — config из объявления. Вызовите super().__init__(id, name, config).
async initialize() -> boolСвои таблицы, фоновые задачи, проверки. False или исключение — плагин не поднят, причина видна в панели.
get_api_routes() -> listМаршруты HTTP словарями.
get_websocket_routes() -> listМаршруты WebSocket; по умолчанию пусто.
get_handlers() -> dictИменованные функции для других плагинов: plugin_manager.get_plugin(id).get_handlers(). Наружу по HTTP не публикуются.
async post_construct()После полного старта сервера — для самопроверок, которым нужен живой HTTP.
async cleanup()При остановке.

Маршрут — это словарь

Декораторов нет: каркас сам строит обёртку FastAPI для каждого маршрута, поэтому вход, разбор параметров и аудит у всех маршрутов одинаковы.

КлючПо умолчаниюСмысл
path—Путь, с параметрами в фигурных скобках.
methods—["GET"], ["POST"] и т. д.
handler—Корутина-обработчик.
require_authTrueНужен ли вход; обработчик получает current_user.
permissionнетПраво, которое обязан иметь вызывающий; проверяется до обработчика.
auditTrueПисать ли запрос в аудит входящих; False — не писать.
max_body_bytesпредел приложенияНаибольшее тело, которое принимает маршрут.
response_media_typeJSONЧто отвечает маршрут, когда это не JSON.
paramsнетМаска параметров: по имени type (str, int, float, bool), required, in, choices, min, max, max_length, pattern.
is_uploadFalseОбработчик получает сам запрос, а не разобранный JSON.
raw_requestFalseОбработчик получает Request и проверяет вход сам.
include_in_schemaTrueПоказывать ли маршрут в описании API.

Обработчик принимает current_user: dict и обычные именованные аргументы: параметры пути и строки запроса приходят по имени, тело записи — в request. Незнакомый параметр — ответ 400, а не 500: если у маршрута есть маска, решает она и приводит типы; если нет — сигнатура обработчика (обработчик с **kwargs принимает всё).

def get_api_routes(self):
    return [{
        "path": "/api/shops/{shop_id}/offers",
        "methods": ["GET"],
        "handler": self.list_offers,
        "params": {
            "shop_id": {"type": "int"},
            "status": {"type": "str", "choices": ["active", "archived"]},
        },
    }]

async def list_offers(self, current_user: dict, shop_id: int, status: str = "active"):
    ...

Объявление и включение

{
  "plugins": [
    {"id": "hello", "name": "Hello", "enabled": true, "priority": 100,
     "config": {"greeting": "hi"}}
  ],
  "roles": [
    {"name": "ADMIN", "modules": [], "plugins": ["hello"]}
  ]
}

Будет ли объявленный плагин работать, решается по порядку, от сильного к слабому: PLUGINS_DISABLE → PLUGINS_ENABLE → переключатель в панели → флаг enabled в файле. Меньший priority поднимается раньше. Список plugins у роли — это видимость в /api/plugins, а не включение.

Ядро

Плагин загружается конструктором — ядром. Плагин объявляет дескриптор: id, вид (required, optional, transport), что ему нужно (requires, wants) и что он даёт (provides, contributions). Из дескрипторов собирается каталог: собственный каталог каркаса (plugins/builtin.json), файл приложения (config/modules.json) и профиль (settings.profile) — каждый следующий слой правит предыдущий. Плагины публикуют службы в реестр (provides), а берут чужие через requires; жизненный цикл запускает их в две фазы — сперва те, чьи требования уже выполнены, затем остальные. Обязательный плагин и транспорт включаются самим фактом установки, необязательный — файлом, PLUGINS_ENABLE, профилем или администратором.

from keepup.kernel import create_runtime

runtime = create_runtime(settings)   # Runtime, ещё не запущенный

Имена служб, которые плагин может требовать и публиковать, перечислены в doc/service-catalogue.md; контракт конструктора — в doc/plugin_constructor.md. Если приложение собрало менеджер плагинов само, настройка plugin_manager передаёт его ядру.

Панель

Панель живёт на /selfcare. Оболочка — страницы тем, скрипт и стили — едет с keepup-http и отдаётся из shell_mount; база фронтенд не несёт, копировать оболочку к себе не нужно.

Восемь разделов каркаса

Их и оболочку ставит keepup-http. При старте они заносятся в каталог и по умолчанию выданы роли ADMIN:

  • Пользователи — учётные записи, роли, сессии.
  • Разделы — каталог разделов панели и выдача их ролям.
  • Кластер — реплики, их плагины и состояние; остановка, запуск, перезапуск.
  • Метрики — процессор и память по репликам.
  • Темы — оформление панели.
  • События — журнал событий приложения.
  • Фоновые задачи — задачи планировщика и распределённые блокировки.
  • Журнал интеграций — исходящие вызовы во внешние системы.

Свой раздел

Раздел — это скрипт и, при желании, стиль, объявленные в modules[] того же modules.json, и строка в modules роли. Оболочка загружает скрипт и вызывает window[initFunction]().

{
  "modules": [{
    "id": "orders",
    "name": "Orders",
    "js": "/static/modules/js/orders.js",
    "css": "/static/modules/css/orders.css",
    "initFunction": "initOrdersModule",
    "routePath": "/selfcare/modules/orders",
    "sectionId": "orders-section",
    "config": {}
  }],
  "roles": [{"name": "CLIENT", "modules": ["orders"], "plugins": []}]
}

Каталог живёт в базе; файл — затравка. При старте добавляются только разделы и выдачи, которых в базе ещё нет, — то, что администратор выключил в панели, так и остаётся выключенным.

Тема отвечает за обрамление, раздел — за себя. Стиль раздела не объявляет правил для боковой панели, навигации и основной области: стили разделов подключаются после страницы, и обрамление начало бы зависеть от того, какие разделы выданы роли.

Темы

Своих тем каркас не навязывает: темы — дистрибутив keepup-ui (keepup_ui.themes), приложение перечисляет их в built_in_themes кортежами (name, page_file, brand, logo). На пустой базе активной становится первая; на существующей остаётся выбор администратора.

База данных

Где база

Если задана переменная DB_TYPE, всё берётся из окружения: DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_PATH, DB_POOL_*. Если нет — из config/postgres.properties (ключи db.*); пароль и тогда можно передать через DB_PASSWORD. Без того и другого — SQLite.

export DB_TYPE=postgres DB_HOST=db.internal DB_NAME=yours DB_USER=yours
export DB_PASSWORD=…

Запросы

DatabaseManagerV2 — движок SQLAlchemy с пулом. Параметры только именованные.

from keepup_db import DatabaseManagerV2

rows = DatabaseManagerV2.execute(
    "SELECT id, name FROM orders WHERE owner_id = :owner", {"owner": user_id})

with DatabaseManagerV2.get_session() as session:   # commit или rollback сам
    ...

Таблица объявляется один раз

Миграций нет. Таблица — объявление SQLAlchemy Core для обоих диалектов; ensure_tables() при каждом старте создаёт недостающее: CREATE TABLE IF NOT EXISTS, недостающие столбцы, индексы. Новый столбец — это просто новый Column.

from sqlalchemy import Column, Integer, Text
from keepup_db import tables

orders = tables.table(
    "orders",
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("owner_id", Integer, nullable=False),
    Column("title", Text, nullable=False),
    tables.foreign_key("owner_id", "users", ("id",), ondelete="CASCADE"),
)

tables.ensure_tables(orders)

Различия диалектов живут в объявлении (with_variant, ddl_if, tables.per_dialect(postgres=…, sqlite=…)). Столбец на чужой таблице — например, на users — добавляется через tables.ensure_columns("users", Column(…)), а не вторым объявлением.

Несколько реплик против одной базы

Приложение на keepup рассчитано на несколько процессов за балансировщиком. Всё согласование идёт через общую базу — реплики не знают адресов друг друга.

Распределённая блокировка

Фоновая работа, которая должна идти в одной реплике, берётся под блокировку в таблице distributed_locks. Держатель продлевает её, пока работает, поэтому работа может длиться сколько угодно; перехватывается только блокировка того, кто замолчал.

from keepup.locks import distributed_lock, with_distributed_lock

@with_distributed_lock("nightly_report", timeout=300)
async def nightly_report():
    ...   # идёт в одной реплике; в остальных запуск пропускается

async def purge():
    try:
        async with distributed_lock("purge_journal", timeout=1):
            ...
    except Exception:
        pass  # чистит другая реплика — этого достаточно

Декоратор при занятой блокировке пропускает запуск и возвращает None; контекстный менеджер бросает исключение.

Шина сообщений

Задайте notification_channel — и каркас поднимет шину на PostgreSQL LISTEN/NOTIFY до инициализации плагинов. deliver() отправляет конверт другим репликам и раздаёт его подписчикам своей; publish() — только другим.

from keepup import notification_bus

bus = notification_bus.get_notification_bus()
if bus is not None:
    bus.subscribe(self.on_envelope)          # в initialize() плагина

await notification_bus.deliver({"kind": "order_changed", "order_id": 42})
Доставка — по возможности. NOTIFY не хранит и не повторяет, поэтому конверт называет объект, а не несёт его состояние (предел — 7500 байт), и подписчик перечитывает объект сам. На SQLite шина есть, но в своём процессе (InProcessTransport): publish() возвращает True, однако сообщение не покидает процесс.

Реестр кластера

Каждая реплика публикует себя в cluster_members и забирает адресованные ей команды из cluster_commands. Раздел «Кластер» панели показывает реплики и их плагины и умеет три команды: stop — процесс жив, но отвечает 503 на API и приостанавливает планировщик; start — обратно; restart — штатная остановка и перезапуск той же командной строки.

Фоновые задачи

Планировщик, который ставит keepup-tasks (keepup_tasks.scheduler), — один на процесс, его задачи видны в разделе «Фоновые задачи» и запускаются оттуда вручную. От запуска одной и той же задачи во всех репликах защищает не планировщик, а блокировка вокруг самой задачи.

Пользователи и вход

Учётные записи живут в базе приложения (provider: local в config/auth.yaml; без файла — то же самое). Роли две — ADMIN и CLIENT (keepup.roles), но пользователь держит набор ролей: он лежит в таблице user_roles, и разделы с плагинами всех ролей набора склеиваются. Спрашивать надо keepup_auth.user_roles.has_role(user, ROLE_ADMIN) (в панели — window.AppRoles.has(user, role)), а не сравнивать user["role"]. Колонки users.role больше нет: в ответах входа (/api/auth/me и список пользователей) поле role осталось посчитанным из набора — для старых панелей. Пустой набор запрещён на запись, а доступ закрывают блокировкой учётной записи, а не снятием ролей. Вход выдаёт токен, подписанный SECRET_KEY, а панель держит сессию в cookie.

  • Регистрация закрыта, пока SELF_REGISTRATION_ENABLED не равна true.
  • Правило пароля — password_rule(password, username) в настройках: вернуть текст проблемы или None.
  • Свои маршруты вне плагинов получают пользователя зависимостями get_current_user и get_current_admin из keepup_auth.dependencies.

Вход через внешний провайдер

OpenID Connect включается одним полем. Без него маршруты /api/auth/oidc/login и /api/auth/oidc/callback даже не регистрируются.

import os

from keepup import KeepupSettings
from keepup.settings import OidcSettings

settings = KeepupSettings(
    title="Your product",
    oidc=OidcSettings(
        issuer="https://id.example.com/realms/yours",
        client_id="yours",
        client_secret=os.environ["OIDC_CLIENT_SECRET"],
        redirect_uri="https://yours.example.com/api/auth/oidc/callback",
        roles_claim="groups",
    ),
)

Без oidc_account_policy человеку, которого приложение ещё не знает, вход закрыт. Политика — функция от утверждений провайдера; готовые лежат в keepup_auth.oidc_policy: refuse_unknown(), create_account(), create_if_email_domain([...]) — последняя сверяет адрес, только если провайдер его подтвердил.

Это внутренний интерфейс: в 0.5.0 модуль keepup_auth.oidc_policy по-прежнему не объявляет __all__. Готовые политики работают, но их имена не входят в обещание совместимости.

Наблюдаемость

Четыре независимых канала, все подключены в жизненном цикле приложения.

  • Аудит входящих запросов — каждый маршрут плагина пишется в incoming_requests через буфер. Из тела запроса по умолчанию остаются только имена полей; иначе решает audit_redaction (готовые — keepup_audit.audit.names_only и keep_as_is).
  • Журнал событий — await emit_event(event_type, event_text, event_data) из keepup_audit.events; раздел «События» фильтрует по declared_event_types.
  • Метрики — /metrics для Prometheus (без учётных данных — только если metrics_public=True) и снимки процессора и памяти по репликам для панели.
  • Логи — локально с ротацией, а при заданном remote_log_url ещё и пакетами в ваш сборщик.
from keepup_audit.events import emit_event

await emit_event("order_accepted", "Order #42 accepted", {"order_id": 42})

Безопасность по умолчанию

Где значению по умолчанию пришлось бы угадывать, каркас закрывает. Приложение само говорит, что ему нужно открыть.

ЧтоПо умолчаниюКак открыть
Ключ подписиНет; без него старт отказываетSECRET_KEY, от 32 символов
Первый администраторСлучайный пароль в журнале, один разADMIN_INITIAL_PASSWORD
CORSНи одного источникаcors_origins
/metricsТолько с учётными даннымиmetrics_public=True
Тело запроса в аудитеТолько имена полейaudit_redaction
Незнакомый параметр400**kwargs или маска params
РегистрацияЗакрытаSELF_REGISTRATION_ENABLED=true
Вход через провайдерНеизвестным отказoidc_account_policy
Заголовки безопасностиВключеныsecurity_headers=False
Размер тела запроса2 МиБ — умолчание каркасасвоё число: max_upload_bytes, а у маршрута — max_body_bytes

Функции, которые плагины дают друг другу через get_handlers(), по HTTP не публикуются. Проверка токенов провайдера отвергает симметричные алгоритмы и none и на любой провал отвечает одинаково, чтобы по ответу нельзя было понять, какая проверка не прошла.

Публичный интерфейс и версии

Модуль, из которого приложению можно импортировать, объявляет __all__. Всё остальное — внутреннее и может измениться без предупреждения, включая любое имя с подчёркиванием. Тест в репозитории каркаса держит это правило против настоящих приложений: имя, взятое из каркаса и не объявленное публичным, роняет прогон.

Номер версии — три числа. Мажорное меняет то, на что уже опираются: API отвечает иначе, путь проходится иначе. Минорное добавляет возможности или убирает устаревшее. Третье — срочная правка дефекта и ничего больше.

Используйте 0.5.0 или новее. База не объявляет ни одной чужой библиотеки — даже FastAPI: каждая возможность объявляет свои, а проверки репозитория (.github/scripts/check.sh) держат эту границу для каждого дистрибутива.

Переезд модулей

Почти все имена keepup.<что-то> уехали в свои дистрибутивы. Слоя совместимости нет: старый импорт даёт ModuleNotFoundError.

БылоСтало
keepup.dbkeepup_db
keepup.tableskeepup_db.tables
keepup.positional_sqlkeepup_db.positional_sql
keepup.retentionkeepup_db.retention
keepup.auditkeepup_audit.audit
keepup.eventskeepup_audit.events
keepup.admin_trailkeepup_audit.trail
keepup.schedulerkeepup_tasks.scheduler
keepup.metricskeepup_metrics
keepup.metrics_apikeepup_metrics.api
keepup.metrics_retentionkeepup_metrics.retention
keepup.clusterkeepup_cluster.cluster
keepup.log_shippingkeepup_log_shipping.log_shipping
keepup.moduleskeepup_modules.catalogue
keepup.integrationskeepup_integration_log.integrations
keepup.plugins.adminkeepup_modules.decisions
keepup.plugins.routeskeepup_http.routes
keepup.auth и keepup.auth.*keepup_auth и keepup_auth.*
keepup.themeskeepup_ui.themes
keepup.api_docskeepup_http.api_docs
keepup.events_apikeepup_http.events_api
keepup.body_limitkeepup_http.body_limit
keepup.factorykeepup_http.factory
keepup.webkeepup_http.web

Каталога-пакета keepup/ тоже нет: корень репозитория и есть пакет. В базе остались keepup.roles, keepup.locks, keepup.notification_bus, keepup.kernel, keepup.plugins.base, keepup.settings, keepup.security, keepup.cache, keepup.instance, keepup.api_versions и keepup.logging_setup.

Что изменилось от версии к версии — в CHANGELOG.