Files
index/index1c/config.py
T
mihailkudravcev 91521e6604 Release 0.3.4 with external processing indexing.
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.
2026-07-21 13:03:25 +03:00

367 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Каталоги конфигураций проекта и типы объектов метаданных 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"