Skip to main content

demon-cry-base

Базовые контракты для OSINT-плагинов demon-cry

Установка

pip install demon-cry-base

Использование

Правило импортов: импортируйте только из корней контекстов — demon_cry_base.plugin (всё для плагина) и demon_cry_base.runner (всё для раннера). Глубокие пути (например demon_cry_base.plugin.models) — приватные, не полагайтесь на них.

Плагин без конфига

Для простых Stateless-плагинов можно использовать PluginConfig напрямую:

from demon_cry_base.plugin import BasePlugin, PluginConfig, PluginParameters
from demon_cry_base.runner import PluginResult


async def ping_run(config: PluginConfig, params: PluginParameters) -> PluginResult:
    return PluginResult.build()


class PingPlugin(BasePlugin):
    name = "ping"
    description = "Check if host is alive"
    category = "utility"
    parameters_model = PluginParameters
    execute_func = "my_package.ping:ping_run"

execute_func — путь к раннеру в формате путь.до.модуля:имя_функции. Раннер — отдельная async-функция (config, params) -> PluginResult, ядро импортирует её через importlib.

Создание плагина

from demon_cry_base.plugin import BasePlugin, PluginConfig, PluginParameters
from demon_cry_base.runner import PluginResult


class MyPluginParams(PluginParameters):
    target: str
    query: str | None = None


class MyPluginConfig(PluginConfig):
    timeout: int = 30


class MyPlugin(BasePlugin):
    name = "my_plugin"
    description = "My OSINT plugin"
    category = "custom"
    config_model = MyPluginConfig
    parameters_model = MyPluginParams
    execute_func = "my_package.my_plugin:my_plugin_run"


async def my_plugin_run(config: MyPluginConfig, params: MyPluginParams) -> PluginResult:
    return PluginResult.build()

Два источника данных

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

Config Parameters
Откуда БД / ядро Запрос / пользователь
Формат Pydantic-модель (PluginConfig) Pydantic-модель (PluginParameters)
Зачем Настройки плагина Входные данные
Передаётся config в функцию из execute_func params в функцию из execute_func

Config — настройки из БД

PluginConfig — это Pydantic-модель. Наследуйте её и добавляйте поля:

class ReconConfig(PluginConfig):
    deep: bool = False
    ports: list[int] = [80, 443]


class ApiConfig(PluginConfig):
    api_key: str
    rate_limit: int = 100
    timeout: int = 30

Затем укажите config_model в плагине. Ядро загрузит конфиг из БД и передаст в функцию из execute_func:

from demon_cry_base.runner import PluginResult


class ReconParams(PluginParameters):
    target: str


class ReconConfig(PluginConfig):
    deep: bool = False
    ports: list[int] = [80, 443]


class ReconPlugin(BasePlugin):
    name = "recon"
    description = "Network reconnaissance"
    category = "osint"
    config_model = ReconConfig
    parameters_model = ReconParams
    execute_func = "my_package.recon:recon_run"


async def recon_run(config: ReconConfig, params: ReconParams) -> PluginResult:
    if config.deep:
        ...

Parameters — входные данные

PluginParameters — это Pydantic-модель. Наследуйте её и добавляйте поля:

from typing import Literal
from pydantic import Field
from demon_cry_base.plugin import PluginParameters


class SearchParams(PluginParameters):
    query: str
    category: Literal["general", "images", "files", "it", "social media", "news"] = (
        "general"
    )
    time_range: Literal["day", "week", "month", "year", "all"] = "all"
    max_results: int = Field(
        default=10, ge=1, le=100, description="Max results to return"
    )

Затем укажите parameters_model в плагине:

from demon_cry_base.plugin import BasePlugin, PluginConfig
from demon_cry_base.runner import PluginResult


class SearchPlugin(BasePlugin):
    name = "search"
    description = "Web search"
    category = "osint"
    parameters_model = SearchParams
    execute_func = "my_package.search:search_run"


async def search_run(config: PluginConfig, params: SearchParams) -> PluginResult:
    # params.query — строка
    # params.category — enum
    # params.max_results — int с валидацией
    ...
Паттерны параметров
Паттерн Пример
Обязательное поле target: str
Optional поле query: str | None = None
Дефолт timeout: int = 30
Enum-like category: Literal["a", "b", "c"] = "a"
Валидация max_results: int = Field(default=10, ge=1, le=100)
List поле ports: list[int] = [80, 443]

Выход плагина: entities

Раннер возвращает не dict, строку или число, а типизированный PluginResult:

from demon_cry_base.runner import BaseEntity, PluginResult


class SearchHit(BaseEntity):
    title: str
    url: str
    snippet: str


async def search_run(config: SearchConfig, params: SearchParams) -> PluginResult:
    ...
    return PluginResult.build(
        entities=[
            SearchHit(title="...", url="...", snippet="..."),
            SearchHit(title="...", url="...", snippet="..."),
        ]
    )
  • BaseEntity — базовый класс сущности. Наследуйтесь и описывайте поля, которые плагин возвращает: qtype/value, title/url/snippet — чем угодно.
  • PluginResult.entities — список сущностей. Один запуск обычно возвращает их пачкой (2+), пустой список — валидный результат «ничего не найдено».
  • PluginResult.errors — список ошибок (list[ErrorEntity]). Хранится отдельно от entities, не смешивайте их.
  • PluginResult.status — вычисляемое поле (computed_field), Literal["ok", "error", "partial"], руками не задаётся:
    • "ok" — успех или «ничего не найдено» (есть данные без ошибок, либо пусто вообще);
    • "error" — полный провал: данных нет, есть ошибки;
    • "partial" — частичный успех: есть и данные, и ошибки (например часть источников отвалилась).

Создавайте результат через фабрику PluginResult.build():

return PluginResult.build()  # ok, пусто — «ничего не найдено»
return PluginResult.build(entities=[SearchHit(...)])  # ok
return PluginResult.build(
    errors=[ErrorEntity(code="AUTH_FAILED", message="bad api key")]
)  # error
return PluginResult.build(
    entities=[SearchHit(...)], errors=[ErrorEntity(...)]
)  # partial

Прямой конструктор — PluginResult(entities=[...], errors=[...]), эквивалент build(). Статус при этом всё равно выводится сам, отдельного параметра status нет.

Ошибки: ErrorEntity

ErrorEntity — подкласс BaseEntity с полями code/message/details, но складывайте его только в errors, а не в entities:

from demon_cry_base.runner import BaseEntity, ErrorEntity, PluginResult


class SearchHit(BaseEntity):
    title: str
    url: str


async def search_run(config: SearchConfig, params: SearchParams) -> PluginResult:
    hits = [SearchHit(title="...", url="...")]
    errors = [
        ErrorEntity(
            code="HTTP_429", message="rate limited", details={"retry_after": 60}
        )
    ]
    return PluginResult.build(entities=hits, errors=errors)
  • ErrorEntity.code: str — машинный код (HTTP_429, AUTH_FAILED, TIMEOUT).
  • ErrorEntity.message: str — человекочитаемое описание.
  • ErrorEntity.details: dict | None = None — произвольный контекст (retry_after, source, status_code).
  • Правило: entities — только данные (никогда не кладите туда ErrorEntity), errors — только ErrorEntity. Статус выведется сам: пусто + ошибки → error, данные + ошибки → partial, иначе → ok.

Полный пример: плагин с обоими источниками

import aiodns
from typing import Literal
from demon_cry_base.plugin import BasePlugin, PluginConfig, PluginParameters
from demon_cry_base.runner import BaseEntity, PluginResult


class DnsLookupParams(PluginParameters):
    domain: str
    record_type: list[
        Literal["A", "AAAA", "MX", "NS", "TXT", "CNAME", "SOA", "PTR"]
    ] = ["A"]


class DnsLookupConfig(PluginConfig):
    name_servers: list[str] = ["1.1.1.1", "8.8.8.8"]


class DnsRecord(BaseEntity):
    qtype: str
    value: str


class DnsLookup(BasePlugin):
    name = "dns_lookup"
    description = "Finds DNS records"
    category = "network"
    config_model = DnsLookupConfig
    parameters_model = DnsLookupParams
    execute_func = "my_package.dns:dns_lookup_run"


async def dns_lookup_run(
    config: DnsLookupConfig, params: DnsLookupParams
) -> PluginResult:
    resolver = aiodns.DNSResolver(nameservers=config.name_servers)
    # config — настройки из БД (name_servers)
    # params.domain — домен из параметров
    # params.record_type — типы записей
    ...
    return PluginResult.build(
        entities=[
            DnsRecord(qtype="A", value="93.184.216.34"),
            DnsRecord(qtype="AAAA", value="2606:2800:220:1:248:1893:25c8:1946"),
        ]
    )

Подключение к ядру (entry points)

Ядро находит плагины через importlib.metadata.entry_points(group="demon_cry.plugins").

Укажите entry-point в pyproject.toml вашего плагина:

[project.entry-points."demon_cry.plugins"]
my_plugin = "my_package.my_plugin:MyPlugin"
  • Ключ слева (my_plugin) — имя для логов/отладки. Регистрирует ядро по instance.name, а не по ключу — держите их одинаковыми, чтобы не путаться.
  • Значение справа — импорт-путь:КлассПлагина (например my_package.my_plugin:MyPlugin). Класс должен наследовать BasePlugin и задавать name, description, category, execute_func.

Не путайте два похожих формата модуль:объект:

  • entry-point (my_package.my_plugin:MyPlugin) — указывает на класс плагина;
  • execute_func (my_package.my_plugin:my_plugin_run) — указывает на async-функцию-раннер (config, params) -> PluginResult.

Как это работает на стороне ядра при discover():

  1. ep.load() → инстанцирует класс без аргументов, поэтому конструктор должен быть без обязательных аргументов.
  2. Кладёт инстанс в реестр по instance.name.
  3. Берёт дефолты из instance.config_model().model_dump() и создаёт запись в БД с enabled=False.

Требования к плагину: уникальное name, конструктор без аргументов, дефолтный конфиг должен быть валидным без секретов в коде.

Metadata

Release files for demon-cry-base 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for demon-cry-base 0.8.0
File Size Uploaded
demon_cry_base-0.8.0.tar.gz 9.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for demon-cry-base 0.8.0
File Interpreter ABI Platform
demon_cry_base-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 18.1 kB

Release files / demon_cry_base-0.8.0.tar.gz

Download URL demon_cry_base-0.8.0.tar.gz
Size 9.4 kB
Tags Source
SHA-256 checksum
How to use checksums
fbfcf760a2c25f36fd4c673c36f28ed0bc0b7e72c9b9bb2066d0610deebb1234
BLAKE2b-256 checksum
How to use checksums
c35076c38f9110ca5c2a06719b65e64e74026bc14370f101e304f06ae3a4b14d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / demon_cry_base-0.8.0-py3-none-any.whl

Download URL demon_cry_base-0.8.0-py3-none-any.whl
Size 8.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fc58614a07c8ab9277a540e6c10caaa6a4b631db524c3222aef4782a857ddcbd
BLAKE2b-256 checksum
How to use checksums
072887b03972024da0920e2c90faa119aaf5120dd7ab3363980175bf686e04ef
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.8.0 This release

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page