Создать правило качества
POST /api/v1/quality/rules
Создаёт проверку результата конкретного сайта.
Доступ
Bearer API-ключ активного пространства и право runs.read. Для записи нужны ключ с записью и sites.write на выбранном сайте. Общие определения изменяет только оператор платформы с settings.manage, не внешний ключ. Сайты и история ограничены текущей командой. На агенте POST/PATCH/DELETE запрещены: определения меняются в облаке.
Параметры
| Имя | Где | Обязательно | Описание |
|---|---|---|---|
locale |
query | нет | Язык справочника, предложений и пояснений. Если не указан, учитывается cookie akmelo_locale; иначе ru. Имена полей пользователя не переводятся. |
Пример JSON тела:
{
"site_id": 1,
"name": "Падение числа товаров",
"kind": "row_count_drop",
"params": {
"drop_percent": 30,
"min_previous": 100
},
"action": "warn",
"preset_id": "catalog-drop"
}
| Поле | Правило |
|---|---|
site_id |
Доступный сайт; обязателен для нового правила внешнего API. PATCH не переносит правило |
name |
Название для людей, до 255 символов |
kind |
Один из 12 кодов из справочника |
params |
Условия именно этого вида; неизвестные ключи отклоняются |
action |
warn по умолчанию либо mark_suspicious |
is_active |
Логическое значение, для нового правила по умолчанию true |
preset_id |
Необязательный код одного из 24 шаблонов, соответствующий kind; допускается null |
У POST обязательны name и kind; у PATCH передаются только изменяемые поля. Поле для правила выбирается по объявленной схеме сайта. Общее правило без сайта внешний API не создаёт и не изменяет. При изменении kind передавайте новый params, а не условия прежнего вида. Если params отсутствует, прежние условия сохраняются, когда вид не меняется. params=null явно возвращает начальные условия выбранного вида; для видов с обязательным полем потребуется его указать.
warn создаёт замечание и оставляет автоматическую доставку после сбора. mark_suspicious создаёт критичное замечание, помечает сбор для проверки и приостанавливает автоматическую доставку этого сбора. Ручная выгрузка не объявляется заблокированной. Ни одно действие не исправляет и не удаляет собранные записи.
Ответ 201
Пример сокращён. Числовые ID условны; полные поля описаны ниже и в скачиваемой схеме OpenAPI.
{
"id": 1,
"name": "Падение числа товаров",
"kind": "row_count_drop",
"params": {
"drop_percent": 30,
"min_previous": 100
},
"action": "warn",
"is_active": true,
"site": {
"id": 1,
"code": "parts"
},
"preset_id": "catalog-drop",
"last_check": null
}
| Поле правила | Значение |
|---|---|
id, site |
Идентификатор правила и ссылка на сайт; site=null у общего правила |
name, kind, params |
Название, стабильный вид и фактически сохранённые условия |
action, is_active |
Действие при нарушении и включение будущих проверок |
preset_id |
Код исходного шаблона либо null; несколько правил одного шаблона разрешены |
created_at, updated_at |
Даты определения правила |
last_check |
null либо последний доступный результат: status, reason, reason_label, checked_at, run_id, details |
last_check.status: passed означает выполнение условий, breached нарушение, skipped невозможность сделать вывод. last_check=null не означает успех. Исторический снимок details.params может отличаться от текущего params после редактирования. Для общих правил последняя проверка выбирается только среди доступных пользователю сайтов. Нельзя показывать результат чужой команды как результат общего правила.
Поведение и ограничения
Создайте правило на доступном site_id с правом sites.write и ключом с записью. 12 видов и допустимые params описаны в vocabulary. Новому правилу по умолчанию action=warn. Для поля укажите точный код из sites[].fields в recommendations. preset_id необязателен и должен соответствовать kind. Несколько правил разрешены. Общие правила через внешний API создать нельзя.
Правила действуют на следующие успешные полные сборы. Они не меняют старые данные и не исправляют сценарий. Проверки полей используют до 5 000 записей текущего сбора, с явной отметкой ограниченной выборки; пустые значения кроме проверки заполненности пропускаются. passed, breached, skipped различаются, а отсутствие находки не доказывает успех.
Числовые примеры сохраняют прежний контракт v1. Для публичных UUID используйте X-Identifier-Format: uuid либо тот же метод /api/v2. Это относится и к вложенным ссылкам на сайты, правила и сборы; preset_id, category_id и field остаются строковыми кодами. Форматы идентификаторов.
Явный null разрешён только для четырёх необязательных границ: min_value, max_value, min_length, max_length. У остальных параметров null отклоняется. При изменении params отсутствующие необязательные границы считаются снятыми. Числовые сравнения и проценты точные; округление нужно только для показа.
Покрытие и пропущенные проверки
До json_decode проверяются сложность и глубина JSON: общий предел 200000узлов и 16 МиБ оценённой распакованной структуры, не больше 4 МиБ на строку и 24уровней. Строковые скобки не считаются узлами. Дополнительно перед PDO-порцией и распаковкой проверяется свободная память PHP с резервом 8 МиБ. Новые причины пропуска: sample_structure_exceeded, sample_depth_exceeded, sample_decode_error, sample_memory_exceeded. Даже если первая строка слишком сложная, это пропуск по пределу, а не no_records. Все измерения до остановки частичные.
Проверяются только успешные полные обычные сборы. Пробные, частичные, неуспешные и отменённые сборы не проверяются. Продолжение или повтор прерванного сбора имеет skipped / continued_run. Сравнение объёма требует более раннего успешного полного сбора того же сайта и сценария, не помеченного для проверки; без него будет no_baseline, при слишком малом прошлом объёме baseline_too_small.
Счётчики объёма относятся ко всему сбору. Поля проверяются по первым 5000 действующим записям текущего сбора, по внутреннему ID. Это ограниченная выборка, не случайная оценка всего каталога. Общий предел: 32 различных поля, 100000 ячеек и 8 МиБ сырых данных; отдельное значение свыше 8 КиБ не читается в память. sample_limited, sample_size, sample_limit, budget_truncated, budget_bytes, byte_limit, cell_limit, field_limit, oversized_skipped раскрывают покрытие. При достижении общего бюджета результат skipped, причина sample_budget_exceeded, частичное измерение в observed_status не является итоговым успехом или находкой. Поля сверх лимита получают field_budget_exceeded.
Пустыми считаются null, пустая строка, строка только из пробельных символов Unicode и пустой массив. 0 и false заполнены. Только field_fill_rate считает пустоту нарушением. Другие виды исключают её из знаменателя и при отсутствии заполненных значений возвращают no_filled_values. Иные причины пропуска: no_records, field_missing, values_too_large, too_few_records, unsupported_kind, check_error. Ошибка проверки не превращает уже сохранённый успешный сбор в неуспешный.
Доли нарушают правило строго выше допуска: ровно 7 из 100 при допуске 7% проходят. Падение и рост объёма включают порог: 100 → 90 при пороге 10% вызывает находку. Сравнения точные, округляется только показанный процент. Свежесть измеряется относительно run.finished_at; дата без часового пояса считается UTC.
Подробные примеры всех 24 шаблонов и работа с интерфейсом: Качество данных. Схема параметров: справочник.
Как отправить запрос
В серверном окружении задайте BASE (адрес установки без завершающего слеша) и API_KEY. Секрет не вставляется в исходник. Подготовка клиентов и браузерный JavaScript.
curl
curl --fail-with-body -X POST "$BASE/api/v1/quality/rules?locale=ru" \
-H "Authorization: Bearer $API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-binary @- <<'JSON'
{
"site_id": 1,
"name": "Падение числа товаров",
"kind": "row_count_drop",
"params": {
"drop_percent": 30,
"min_previous": 100
},
"action": "warn"
}
JSON
Node.js · сервер
// Node.js, сервер. Сохраните как request.mjs.
const response = await fetch(process.env.BASE + "/api/v1/quality/rules?locale=ru", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({"site_id":1,"name":"Падение числа товаров","kind":"row_count_drop","params":{"drop_percent":30,"min_previous":100},"action":"warn"}),
signal: AbortSignal.timeout(120000)
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
console.log(await response.json());
Python
import json
import os
import urllib.request
import urllib.error
body = '{"site_id":1,"name":"Падение числа товаров","kind":"row_count_drop","params":{"drop_percent":30,"min_previous":100},"action":"warn"}'.encode("utf-8")
request = urllib.request.Request(
os.environ["BASE"] + "/api/v1/quality/rules?locale=ru",
method="POST",
headers={"Authorization": "Bearer " + os.environ["API_KEY"],
"Accept": "application/json", "Content-Type": "application/json"},
data=body,
)
try:
with urllib.request.urlopen(request, timeout=120) as response:
print(json.load(response))
except urllib.error.HTTPError as error:
print(error.code, error.read().decode("utf-8", errors="replace"))
raise
PHP · внешний клиент
<?php
$handle = curl_init(getenv("BASE") . "/api/v1/quality/rules?locale=ru");
curl_setopt_array($handle, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("API_KEY"),
"Accept: application/json",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => <<<'JSON'
{"site_id":1,"name":"Падение числа товаров","kind":"row_count_drop","params":{"drop_percent":30,"min_previous":100},"action":"warn"}
JSON,
]);
$content = curl_exec($handle);
$status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
if ($content === false) {
throw new RuntimeException(curl_error($handle));
}
curl_close($handle);
if ($status < 200 || $status >= 300) {
throw new RuntimeException("HTTP $status: $content");
}
print_r(json_decode($content, true, 512, JSON_THROW_ON_ERROR));
Go
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
"strings"
)
func main() {
body := strings.NewReader(`{"site_id":1,"name":"Падение числа товаров","kind":"row_count_drop","params":{"drop_percent":30,"min_previous":100},"action":"warn"}`)
request, err := http.NewRequest("POST", os.Getenv("BASE") + "/api/v1/quality/rules?locale=ru", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer " + os.Getenv("API_KEY"))
request.Header.Set("Accept", "application/json")
request.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 120 * time.Second}
response, err := client.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
content, err := io.ReadAll(response.Body)
if err != nil { panic(err) }
if response.StatusCode < 200 || response.StatusCode >= 300 {
panic(fmt.Sprintf("HTTP %d: %s", response.StatusCode, content))
}
fmt.Println(string(content))
}
Если запрос не выполнен
401: проверьте ключ и срок. 403: права, режим чтения/записи и 2FA. 404: идентификатор и пространство. 422: поля запроса или состояние объекта. 429: подождите Retry-After. Подробно об ошибках и повторах.