91521e6604
Add virtual baseconf support for external data processors so reindex can discover and index them alongside regular src-based configurations, and update docs to use generic examples without workspace-specific names.
367 lines
13 KiB
Python
367 lines
13 KiB
Python
"""Каталоги конфигураций проекта и типы объектов метаданных 1С."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
|
||
# Алиасы имён конфигураций 1С для CLI (--baseconf / -b), задаются через --config
|
||
# (поле baseconf_aliases в YAML). См. config.example.yml.
|
||
_RUNTIME_ALIASES: dict[str, str] = {}
|
||
_RUNTIME_DEFAULT_BASECONFS: list[str] = []
|
||
|
||
# Папка EDT → канонический тип (английский идентификатор выгрузки).
|
||
OBJECT_TYPE_FOLDERS: dict[str, str] = {
|
||
"AccumulationRegisters": "AccumulationRegister",
|
||
"BusinessProcesses": "BusinessProcess",
|
||
"Catalogs": "Catalog",
|
||
"ChartsOfAccounts": "ChartOfAccounts",
|
||
"ChartsOfCalculationTypes": "ChartOfCalculationTypes",
|
||
"ChartsOfCharacteristicTypes": "ChartOfCharacteristicTypes",
|
||
"CommandGroups": "CommandGroup",
|
||
"CommonAttributes": "CommonAttribute",
|
||
"CommonCommands": "CommonCommand",
|
||
"CommonForms": "CommonForm",
|
||
"CommonModules": "CommonModule",
|
||
"CommonPictures": "CommonPicture",
|
||
"CommonTemplates": "CommonTemplate",
|
||
"Constants": "Constant",
|
||
"DataProcessors": "DataProcessor",
|
||
"DefinedTypes": "DefinedType",
|
||
"DocumentJournals": "DocumentJournal",
|
||
"DocumentNumerators": "DocumentNumerator",
|
||
"Documents": "Document",
|
||
"Enums": "Enum",
|
||
"EventSubscriptions": "EventSubscription",
|
||
"ExchangePlans": "ExchangePlan",
|
||
"FilterCriteria": "FilterCriterion",
|
||
"FunctionalOptions": "FunctionalOption",
|
||
"FunctionalOptionsParameters": "FunctionalOptionsParameter",
|
||
"HTTPServices": "HTTPService",
|
||
"InformationRegisters": "InformationRegister",
|
||
"IntegrationServices": "IntegrationService",
|
||
"Reports": "Report",
|
||
"Roles": "Role",
|
||
"ScheduledJobs": "ScheduledJob",
|
||
"SessionParameters": "SessionParameter",
|
||
"SettingsStorages": "SettingsStorage",
|
||
"StyleItems": "StyleItem",
|
||
"Subsystems": "Subsystem",
|
||
"Tasks": "Task",
|
||
"WebServices": "WebService",
|
||
"WSReferences": "WSReference",
|
||
"XDTOPackages": "XDTOPackage",
|
||
"Bots": "Bot",
|
||
"ExternalDataSources": "ExternalDataSource",
|
||
"ExternalDataProcessors": "ExternalDataProcessor",
|
||
"Languages": "Language",
|
||
}
|
||
|
||
# Алиасы типов для --types (русский / короткий / мн.ч.).
|
||
TYPE_ALIASES: dict[str, str] = {
|
||
"catalog": "Catalog",
|
||
"catalogs": "Catalog",
|
||
"справочник": "Catalog",
|
||
"справочники": "Catalog",
|
||
"document": "Document",
|
||
"documents": "Document",
|
||
"документ": "Document",
|
||
"документы": "Document",
|
||
"informationregister": "InformationRegister",
|
||
"informationregisters": "InformationRegister",
|
||
"регистрсведений": "InformationRegister",
|
||
"регистрысведений": "InformationRegister",
|
||
"рс": "InformationRegister",
|
||
"accumulationregister": "AccumulationRegister",
|
||
"accumulationregisters": "AccumulationRegister",
|
||
"регистрнакопления": "AccumulationRegister",
|
||
"регистрынакопления": "AccumulationRegister",
|
||
"рн": "AccumulationRegister",
|
||
"constant": "Constant",
|
||
"constants": "Constant",
|
||
"константа": "Constant",
|
||
"константы": "Constant",
|
||
"enum": "Enum",
|
||
"enums": "Enum",
|
||
"перечисление": "Enum",
|
||
"перечисления": "Enum",
|
||
"commonmodule": "CommonModule",
|
||
"commonmodules": "CommonModule",
|
||
"общиймодуль": "CommonModule",
|
||
"общиемодули": "CommonModule",
|
||
"dataprocessor": "DataProcessor",
|
||
"dataprocessors": "DataProcessor",
|
||
"обработка": "DataProcessor",
|
||
"обработки": "DataProcessor",
|
||
"externaldataprocessor": "ExternalDataProcessor",
|
||
"externaldataprocessors": "ExternalDataProcessor",
|
||
"внешняяобработка": "ExternalDataProcessor",
|
||
"внешниеобработки": "ExternalDataProcessor",
|
||
"report": "Report",
|
||
"reports": "Report",
|
||
"отчет": "Report",
|
||
"отчеты": "Report",
|
||
"exchangeplan": "ExchangePlan",
|
||
"exchangeplans": "ExchangePlan",
|
||
"планыобмена": "ExchangePlan",
|
||
"definedtype": "DefinedType",
|
||
"definedtypes": "DefinedType",
|
||
"определяемыетипы": "DefinedType",
|
||
"chartofcharacteristictypes": "ChartOfCharacteristicTypes",
|
||
"пвх": "ChartOfCharacteristicTypes",
|
||
"role": "Role",
|
||
"roles": "Role",
|
||
"роль": "Role",
|
||
"роли": "Role",
|
||
"subsystem": "Subsystem",
|
||
"subsystems": "Subsystem",
|
||
"подсистема": "Subsystem",
|
||
"подсистемы": "Subsystem",
|
||
"httpservice": "HTTPService",
|
||
"httpservices": "HTTPService",
|
||
"documentjournal": "DocumentJournal",
|
||
"documentjournals": "DocumentJournal",
|
||
"журналыдокументов": "DocumentJournal",
|
||
"task": "Task",
|
||
"tasks": "Task",
|
||
"задача": "Task",
|
||
"задачи": "Task",
|
||
"businessprocess": "BusinessProcess",
|
||
"businessprocesses": "BusinessProcess",
|
||
"бизнеспроцесс": "BusinessProcess",
|
||
"бизнеспроцессы": "BusinessProcess",
|
||
"модуль": "CommonModule",
|
||
"модули": "CommonModule",
|
||
"bsl": "CommonModule",
|
||
}
|
||
|
||
# Типы с разбором реквизитов / измерений / ссылок.
|
||
FIELD_RICH_TYPES: frozenset[str] = frozenset(
|
||
{
|
||
"Catalog",
|
||
"Document",
|
||
"InformationRegister",
|
||
"AccumulationRegister",
|
||
"ChartOfCharacteristicTypes",
|
||
"ChartOfAccounts",
|
||
"ChartOfCalculationTypes",
|
||
"ExchangePlan",
|
||
"BusinessProcess",
|
||
"Task",
|
||
"DocumentJournal",
|
||
"Constant",
|
||
"DefinedType",
|
||
"CommonAttribute",
|
||
"Enum",
|
||
}
|
||
)
|
||
|
||
# Типы, которые по умолчанию пропускаем (шум / картинки).
|
||
DEFAULT_SKIP_TYPES: frozenset[str] = frozenset(
|
||
{
|
||
"CommonPicture",
|
||
"StyleItem",
|
||
"XDTOPackage",
|
||
"CommonTemplate",
|
||
"Language",
|
||
"Bot",
|
||
}
|
||
)
|
||
|
||
FOLDER_BY_TYPE: dict[str, str] = {v: k for k, v in OBJECT_TYPE_FOLDERS.items()}
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ConfigPaths:
|
||
name: str
|
||
root: Path
|
||
src: Path
|
||
source_kind: str = "src" # src | external_processor
|
||
external_name: str = ""
|
||
|
||
|
||
def project_root_from_here() -> Path:
|
||
"""Корень проекта: каталог с index_1c.py и VERSION."""
|
||
here = Path(__file__).resolve().parent
|
||
for candidate in (here.parent, here.parent.parent, here.parent.parent.parent):
|
||
if (candidate / "index_1c.py").is_file() and (candidate / "VERSION").is_file():
|
||
return candidate
|
||
return here.parent
|
||
|
||
|
||
def set_config_aliases(aliases: dict[str, str] | None) -> None:
|
||
"""Задать алиасы baseconf из файла --config (baseconf_aliases)."""
|
||
global _RUNTIME_ALIASES
|
||
_RUNTIME_ALIASES = {str(k): str(v) for k, v in (aliases or {}).items()}
|
||
|
||
|
||
def get_config_aliases() -> dict[str, str]:
|
||
return dict(_RUNTIME_ALIASES)
|
||
|
||
|
||
def set_default_baseconfs(names: list[str] | None) -> None:
|
||
"""Задать default_baseconfs из файла --config (YAML)."""
|
||
global _RUNTIME_DEFAULT_BASECONFS
|
||
_RUNTIME_DEFAULT_BASECONFS = [str(x) for x in (names or []) if str(x).strip()]
|
||
|
||
|
||
def get_default_baseconfs() -> list[str]:
|
||
"""Список baseconf по умолчанию для reindex без -b."""
|
||
return list(_RUNTIME_DEFAULT_BASECONFS)
|
||
|
||
|
||
def resolve_config_name(name: str) -> str:
|
||
key = name.strip()
|
||
aliases = get_config_aliases()
|
||
return aliases.get(key, aliases.get(key.lower(), key))
|
||
|
||
|
||
def list_config_dirs(project_root: Path) -> list[str]:
|
||
"""Все источники индексации в корне проекта.
|
||
|
||
Возвращает:
|
||
- обычные конфигурации/расширения: <name> (если есть <name>/src/)
|
||
- внешние обработки: <dir>.<Обработка> для каталогов вида <dir>/*.xml + <dir>/<Обработка>/
|
||
"""
|
||
if not project_root.is_dir():
|
||
return []
|
||
found: list[str] = []
|
||
for child in sorted(project_root.iterdir()):
|
||
if not child.is_dir() or child.name.startswith("."):
|
||
continue
|
||
if (child / "src").is_dir():
|
||
found.append(child.name)
|
||
continue
|
||
if child.name.endswith(".ext"):
|
||
found.extend(_discover_external_processors(child))
|
||
return found
|
||
|
||
|
||
_SAFE_NAME_RE = re.compile(r"^[A-Za-zА-Яа-яЁё0-9_]+$")
|
||
|
||
|
||
def _looks_like_external_processor_xml(path: Path) -> bool:
|
||
try:
|
||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||
except OSError:
|
||
return False
|
||
probe = text[:8000]
|
||
return "<ExternalDataProcessor" in probe and "<MetaDataObject" in probe
|
||
|
||
|
||
def _discover_external_processor_entries(root: Path) -> list[tuple[str, str, Path]]:
|
||
"""Найти внешние обработки под root (включая вложенные папки).
|
||
|
||
Возвращает кортежи: (virtual_name, processor_name, processor_root_dir).
|
||
"""
|
||
out: list[tuple[str, str, Path]] = []
|
||
try:
|
||
xml_files = sorted(root.rglob("*.xml"))
|
||
except OSError:
|
||
return out
|
||
for xml in xml_files:
|
||
parent = xml.parent
|
||
name = xml.stem.strip()
|
||
if not name or not _SAFE_NAME_RE.match(name):
|
||
continue
|
||
obj_dir = parent / name
|
||
if not obj_dir.is_dir():
|
||
continue
|
||
if not _looks_like_external_processor_xml(xml):
|
||
continue
|
||
out.append((f"{root.name}.{name}", name, parent))
|
||
return out
|
||
|
||
|
||
def _discover_external_processors(root: Path) -> list[str]:
|
||
return [name for name, _proc, _parent in _discover_external_processor_entries(root)]
|
||
|
||
|
||
def resolve_object_type(name: str) -> str:
|
||
raw = name.strip()
|
||
if raw in FOLDER_BY_TYPE:
|
||
return raw
|
||
if raw in OBJECT_TYPE_FOLDERS:
|
||
return OBJECT_TYPE_FOLDERS[raw]
|
||
compact = raw.replace(" ", "").replace("_", "").replace("-", "").casefold()
|
||
if compact in TYPE_ALIASES:
|
||
return TYPE_ALIASES[compact]
|
||
for folder, otype in OBJECT_TYPE_FOLDERS.items():
|
||
if folder.casefold() == raw.casefold() or otype.casefold() == raw.casefold():
|
||
return otype
|
||
raise ValueError(f"Неизвестный тип объекта метаданных: {name!r}")
|
||
|
||
|
||
def discover_configs(project_root: Path, names: list[str] | None = None) -> list[ConfigPaths]:
|
||
wanted = [resolve_config_name(n) for n in names] if names else list_config_dirs(project_root)
|
||
found: list[ConfigPaths] = []
|
||
seen: set[str] = set()
|
||
|
||
def _append(cfg: ConfigPaths) -> None:
|
||
if cfg.name in seen:
|
||
return
|
||
seen.add(cfg.name)
|
||
found.append(cfg)
|
||
|
||
for name in wanted:
|
||
root = project_root / name
|
||
src = root / "src"
|
||
if src.is_dir():
|
||
_append(ConfigPaths(name=name, root=root, src=src))
|
||
continue
|
||
# virtual baseconf для внешних обработок:
|
||
# <dir>.<processor> → ищем <...>/<processor>.xml + <...>/<processor>/
|
||
if "." in name:
|
||
base, ext_name = name.rsplit(".", 1)
|
||
ext_root = project_root / base
|
||
if ext_root.is_dir():
|
||
entries = _discover_external_processor_entries(ext_root)
|
||
for _virtual_name, proc_name, proc_root in entries:
|
||
if proc_name != ext_name:
|
||
continue
|
||
_append(
|
||
ConfigPaths(
|
||
name=name,
|
||
root=proc_root,
|
||
src=proc_root,
|
||
source_kind="external_processor",
|
||
external_name=proc_name,
|
||
)
|
||
)
|
||
break
|
||
else:
|
||
# имя может уже быть каноническим virtual name
|
||
for virtual_name, proc_name, proc_root in entries:
|
||
if virtual_name != name:
|
||
continue
|
||
_append(
|
||
ConfigPaths(
|
||
name=virtual_name,
|
||
root=proc_root,
|
||
src=proc_root,
|
||
source_kind="external_processor",
|
||
external_name=proc_name,
|
||
)
|
||
)
|
||
break
|
||
if name in seen:
|
||
continue
|
||
# если передали только папку (например my-transfer.ext), развернуть все обработки
|
||
if root.is_dir():
|
||
for virtual_name, proc_name, proc_root in _discover_external_processor_entries(root):
|
||
_append(
|
||
ConfigPaths(
|
||
name=virtual_name,
|
||
root=proc_root,
|
||
src=proc_root,
|
||
source_kind="external_processor",
|
||
external_name=proc_name,
|
||
)
|
||
)
|
||
return found
|
||
|
||
|
||
def cache_dir(project_root: Path) -> Path:
|
||
return project_root / "cache" / "1c_meta"
|