"""Каталоги конфигураций проекта и типы объектов метаданных 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]: """Все источники индексации в корне проекта. Возвращает: - обычные конфигурации/расширения: (если есть /src/) - внешние обработки: .<Обработка> для каталогов вида /*.xml + /<Обработка>/ """ 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 " 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 для внешних обработок: # . → ищем <...>/.xml + <...>// 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"