mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 09:14:58 +03:00
- каталог code/custocrm → code/chatballs; CLI custocrm → chatballs; пакеты hub_platform → chatballs, hub_backend → chatballs_backend (app labels прежние) - переменные CUS_* и CUSTOCRM_* → CHATBALLS_*; образы chatballs-*; compose-проект и база chatballs (были edevs_hub); роли Postgres chatballs_* (были custocrm_*); схема RLS chatballs и GUC chatballs.organization_id; cookie chatballs_* - CI: APP_DIR code/chatballs, DEPLOY_ROOT /opt/chatballs - deploy/migrate/rename-to-chatballs.sh — миграция существующей установки без потери данных: остановка старого проекта, .env (с резервной копией), переименование суперпользователя initdb через временную роль, остальных ролей, базы, схемы и функции RLS; проверено на локальном стеке - dev-стек хранит Postgres в bind-mount data/postgres, как prod (именованный том compose.dev был устаревшим снимком и вводил в заблуждение) - снятие демо удаляет объекты, созданные поверх демо-данных тестировавшим (звонки по демо-диалогу), вместо падения на PROTECT - реальные домены *.custocrm.ru и идентификаторы документов не тронуты Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""Ограниченный JSONPath-subset для mapping'ов контракта (SPEC-HUB-0011 §6).
|
|
|
|
Допускаются только:
|
|
$.field
|
|
$.field.nested
|
|
$.array[0].field — только целочисленный индекс, без filters/scripts
|
|
|
|
Произвольные expressions, filters или script-like expressions не допускаются.
|
|
Отсутствующий обязательный path обрабатывается вызывающей стороной (subject).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
_MISSING = object()
|
|
|
|
# Унификация: принимаем как "$.a.b", так и "a.b" (без префикса).
|
|
_PREFIX = "$."
|
|
|
|
|
|
def _normalize(path: str) -> str:
|
|
if path.startswith(_PREFIX):
|
|
return path[len(_PREFIX):]
|
|
return path
|
|
|
|
|
|
def resolve_path(data: Any, path: str) -> Any:
|
|
"""Возвращает значение по path или None, если path отсутствует.
|
|
|
|
None для optional paths; вызывающая сторона решает, обязателен ли path.
|
|
Некорректный path (не массив по индексу и т.п.) тоже даёт None — контракт
|
|
не должен ронять renderer на отсутствующих optional полях (SPEC §6).
|
|
"""
|
|
if not path or not isinstance(data, dict | list):
|
|
return None
|
|
current: Any = data
|
|
for segment in _normalize(path).split("."):
|
|
if segment == "":
|
|
continue
|
|
# Поддержка array[0] внутри сегмента: field[2]
|
|
idx = _array_index(segment)
|
|
if idx is not None:
|
|
name = segment.split("[", 1)[0]
|
|
current = _step(current, name)
|
|
if current is _MISSING:
|
|
return None
|
|
current = _index(current, idx)
|
|
if current is _MISSING:
|
|
return None
|
|
else:
|
|
current = _step(current, segment)
|
|
if current is _MISSING:
|
|
return None
|
|
return None if current is _MISSING else current
|
|
|
|
|
|
def _step(current: Any, name: str) -> Any:
|
|
if isinstance(current, dict):
|
|
return current.get(name, _MISSING)
|
|
return _MISSING
|
|
|
|
|
|
def _index(current: Any, idx: int) -> Any:
|
|
if isinstance(current, list) and -len(current) <= idx < len(current):
|
|
return current[idx]
|
|
return _MISSING
|
|
|
|
|
|
def _array_index(segment: str) -> int | None:
|
|
if "[" not in segment or not segment.endswith("]"):
|
|
return None
|
|
inner = segment[segment.index("[") + 1 : -1]
|
|
if not inner.lstrip("-").isdigit():
|
|
return None
|
|
return int(inner)
|