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():
ep.load()→ инстанцирует класс без аргументов, поэтому конструктор должен быть без обязательных аргументов.- Кладёт инстанс в реестр по
instance.name. - Берёт дефолты из
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)
| File | Size | Uploaded | |
|---|---|---|---|
| demon_cry_base-0.8.0.tar.gz | 9.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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