Files
index/index1c/writers.py
T

335 lines
12 KiB
Python
Raw Permalink 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.
"""Запись индексов: JSONL, Markdown, SQLite FTS5."""
from __future__ import annotations
import json
import sqlite3
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable
from .parse_meta import ObjectRec
def _utcnow() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> int:
path.parent.mkdir(parents=True, exist_ok=True)
n = 0
with path.open("w", encoding="utf-8") as f:
for row in rows:
f.write(json.dumps(row, ensure_ascii=False) + "\n")
n += 1
return n
def write_objects_markdown(path: Path, objects: list[ObjectRec], *, title: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
by_type: dict[str, list[ObjectRec]] = defaultdict(list)
for obj in objects:
by_type[obj.object_type].append(obj)
lines: list[str] = [
f"# {title}",
"",
f"Сгенерировано: {_utcnow()}. Быстрый поиск метаданных без обхода XML.",
"",
]
for otype in sorted(by_type):
lines.append(f"## {otype} ({len(by_type[otype])})")
lines.append("")
for obj in sorted(by_type[otype], key=lambda o: o.name.casefold()):
syn = f"{obj.synonym}" if obj.synonym else ""
lines.append(f"### `{obj.full_name}`{syn}")
if obj.comment:
lines.append(f"- Комментарий: {obj.comment}")
if obj.tooltip:
lines.append(f"- Подсказка: {obj.tooltip}")
if obj.owners:
lines.append(f"- Владельцы: {', '.join(obj.owners)}")
if obj.register_records:
lines.append(f"- Движения: {', '.join(obj.register_records)}")
if obj.based_on:
lines.append(f"- Вводится на основании: {', '.join(obj.based_on)}")
attrs = [f for f in obj.fields if f.kind != "TabularSection"]
if attrs:
lines.append("- Поля:")
for f in attrs:
loc = f".{f.tabular_section}" if f.tabular_section else ""
types = ", ".join(f.types) if f.types else ""
syn_f = f" ({f.synonym})" if f.synonym else ""
type_s = f" : `{types}`" if types else ""
refs = ""
if f.refs:
refs = "" + ", ".join(
f"{r['kind']}.{r['name']}" for r in f.refs
)
lines.append(
f" - `{f.kind}{loc}.{f.name}`{syn_f}{type_s}{refs}"
)
lines.append("")
path.write_text("\n".join(lines), encoding="utf-8")
def write_refs_markdown(path: Path, objects: list[ObjectRec], *, title: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
inv: dict[str, list[str]] = defaultdict(list)
for obj in objects:
for f in obj.fields:
for r in f.refs:
target = f"{r['kind']}.{r['name']}"
loc = f".{f.tabular_section}" if f.tabular_section else ""
inv[target].append(f"{obj.full_name}{loc}.{f.name}")
lines = [
f"# {title}",
"",
f"Сгенерировано: {_utcnow()}.",
"",
]
for target in sorted(inv, key=lambda s: s.casefold()):
lines.append(f"## `{target}`")
for src in sorted(set(inv[target])):
lines.append(f"- `{src}`")
lines.append("")
path.write_text("\n".join(lines), encoding="utf-8")
def write_sqlite_fts(path: Path, objects: list[ObjectRec], config: str) -> None:
if path.exists():
path.unlink()
path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(path)
try:
conn.execute("PRAGMA journal_mode=WAL")
conn.execute(
"""
CREATE TABLE objects (
id INTEGER PRIMARY KEY,
config TEXT NOT NULL,
object_type TEXT NOT NULL,
name TEXT NOT NULL,
full_name TEXT NOT NULL,
synonym TEXT,
comment TEXT,
path TEXT,
json TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE VIRTUAL TABLE objects_fts USING fts5(
full_name,
synonym,
comment,
fields_text,
object_id UNINDEXED,
tokenize='unicode61'
)
"""
)
conn.execute(
"""
CREATE TABLE fields (
object_id INTEGER NOT NULL,
kind TEXT,
name TEXT,
synonym TEXT,
types TEXT,
tabular_section TEXT,
refs_json TEXT,
FOREIGN KEY(object_id) REFERENCES objects(id)
)
"""
)
conn.execute(
"""
CREATE TABLE refs (
from_full_name TEXT NOT NULL,
from_field TEXT NOT NULL,
to_kind TEXT NOT NULL,
to_name TEXT NOT NULL
)
"""
)
for obj in objects:
fields_text = " ".join(
" ".join(
filter(
None,
[
f.kind,
f.name,
f.synonym,
f.comment,
f.tabular_section,
" ".join(f.types),
],
)
)
for f in obj.fields
)
payload = json.dumps(obj.to_dict(), ensure_ascii=False)
cur = conn.execute(
"""
INSERT INTO objects
(config, object_type, name, full_name, synonym, comment, path, json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
config,
obj.object_type,
obj.name,
obj.full_name,
obj.synonym,
obj.comment,
obj.path,
payload,
),
)
oid = cur.lastrowid
conn.execute(
"""
INSERT INTO objects_fts
(full_name, synonym, comment, fields_text, object_id)
VALUES (?, ?, ?, ?, ?)
""",
(obj.full_name, obj.synonym or "", obj.comment or "", fields_text, oid),
)
for f in obj.fields:
conn.execute(
"""
INSERT INTO fields
(object_id, kind, name, synonym, types, tabular_section, refs_json)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
oid,
f.kind,
f.name,
f.synonym,
", ".join(f.types),
f.tabular_section,
json.dumps(f.refs, ensure_ascii=False),
),
)
for r in f.refs:
loc = f"{f.tabular_section}." if f.tabular_section else ""
conn.execute(
"""
INSERT INTO refs
(from_full_name, from_field, to_kind, to_name)
VALUES (?, ?, ?, ?)
""",
(obj.full_name, f"{loc}{f.name}", r["kind"], r["name"]),
)
conn.execute("CREATE INDEX idx_objects_type ON objects(object_type)")
conn.execute("CREATE INDEX idx_objects_name ON objects(name)")
conn.execute("CREATE INDEX idx_refs_to ON refs(to_kind, to_name)")
conn.execute("CREATE INDEX idx_refs_from ON refs(from_full_name)")
conn.commit()
finally:
conn.close()
def write_manifest(path: Path, data: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
data = {**data, "generated_at": _utcnow()}
path.write_text(
json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
def write_type_summaries(out_dir: Path, objects: list[ObjectRec]) -> None:
by_type: dict[str, list[ObjectRec]] = defaultdict(list)
for obj in objects:
by_type[obj.object_type].append(obj)
types_dir = out_dir / "by_type"
types_dir.mkdir(parents=True, exist_ok=True)
for otype, items in by_type.items():
lines = [f"# {otype}", "", f"Объектов: {len(items)}", ""]
for obj in sorted(items, key=lambda o: o.name.casefold()):
syn = f"{obj.synonym}" if obj.synonym else ""
n_fields = len([f for f in obj.fields if f.kind != "TabularSection"])
lines.append(f"- `{obj.name}`{syn} (полей: {n_fields})")
(types_dir / f"{otype}.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
def write_global_index_md(cache_root: Path, summary: dict[str, Any]) -> Path:
"""Сводка по единому index.sqlite."""
cache_root.mkdir(parents=True, exist_ok=True)
configs = summary.get("configs") or []
lines = [
"# Индекс метаданных и модулей 1С (проект)",
"",
f"Сгенерировано: {_utcnow()}.",
f"Режим: `{summary.get('mode', '?')}`, БД: `cache/1c_meta/index.sqlite`.",
f"Потоки: {summary.get('workers', '?')}, время: {summary.get('elapsed_sec', '?')} с.",
"",
"Поиск: `python tools/index/query_1c.py search \"\"`",
"Переиндексация: `python tools/index/index_1c.py reindex` (инкремент) / `--full`.",
"",
"## Конфигурации",
"",
"| Конфигурация | Объектов | Полей | Ссылок | Модулей |",
"|---|---:|---:|---:|---:|",
]
for c in sorted(configs, key=lambda x: x.get("name") or x.get("config") or ""):
name = c.get("name") or c.get("config") or "?"
lines.append(
"| `{name}` | {objects} | {fields} | {refs} | {modules} |".format(
name=name,
objects=c.get("objects", 0),
fields=c.get("fields", 0),
refs=c.get("refs", 0),
modules=c.get("modules", 0),
)
)
lines.extend(
[
"",
"## Статистика прогона",
"",
f"- meta разобрано: {summary.get('meta_parsed', 0)}, пропущено (без изменений): {summary.get('meta_skipped', 0)}",
f"- modules разобрано: {summary.get('modules_parsed', 0)}, пропущено: {summary.get('modules_skipped', 0)}",
f"- удалено файлов: {summary.get('files_removed', 0)}, ошибок: {summary.get('errors', 0)}",
"",
"## Артефакты",
"",
"- `index.sqlite` — единый кэш проекта (объекты, поля, ссылки, модули, FTS)",
"- `INDEX.md` — эта сводка",
"- `md/<config>/` — опциональный Markdown (`reindex --md`)",
"",
]
)
path = cache_root / "INDEX.md"
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
write_manifest(
cache_root / "manifest.json",
{
"db": "index.sqlite",
"mode": summary.get("mode"),
"configs": configs,
"summary": {
k: summary.get(k)
for k in (
"meta_parsed",
"meta_skipped",
"modules_parsed",
"modules_skipped",
"files_removed",
"errors",
"elapsed_sec",
"workers",
)
},
},
)
return path