chancellery
Канцелярия — библиотека русского склонения и сборки служебных
документов. Склоняет по падежам ФИО, должности и воинские звания (с
автоопределением рода и согласованием слов во фразе) и собирает готовый
docx по шаблону с русской разметкой, где падеж задаётся местом
подстановки, а не данными.
Изначальное видение и границы — в CONCEPT.md.
Статус: 0.1.0. Движок переехал из
«Дьяка» 0.3.3, где обкатан на настоящих
кадровых документах; поведение сверено с эталоном донора поабзацно.
План переноса — в
specs/T001-engine-extraction/spec.md.
Потребители: «Дьяк» (кадровые документы пачкой из таблицы) и «Летопись» (приказы из кадровой базы).
Быстрый старт
uv add 'chancellery>=0.1.0,<0.2.0'
Зафиксируйте минорную версию. Пока библиотека в 0.x, обратная
совместимость публичного API не обещается: ломающие правки контракта
выходят в минорной версии (0.2.0), а не в мажорной. С открытой
границей очередной выпуск подтянется молча и уронит потребителя — чаще
всего не на машине разработчика, а при сборке у пользователя. Верхнюю
границу поднимайте осознанно, прочитав CHANGELOG.md.
Сценарий целиком — таблица со строками данных плюс размеченный шаблон
docx дают папку готовых документов, по одному на строку:
from chancellery import generate_documents
written = generate_documents(
table='employees.xlsx',
template='order_template.docx',
out='out',
filename='{{ Фамилия }}.docx', # необязательно: по умолчанию — по ФИО
)
Если сценарий не подходит (источник данных не таблица, свой цикл, своя
обработка ошибок) — те же слои доступны по отдельности: read_table,
build_context, render_document, check_table. А если документы не
нужны вовсе, а нужно только склонение:
from chancellery import Case, Person, PetrovichInflector, PhraseInflector, build_context
context = build_context(
Person(
cells={
'Фамилия': 'Иванов',
'Имя': 'Пётр',
'Отчество': 'Семёнович',
'Должность': 'старший механик-водитель',
}
),
roles={
'surname': 'Фамилия',
'name': 'Имя',
'patronymic': 'Отчество',
'position': 'Должность',
},
inflector=PetrovichInflector(),
position_inflector=PhraseInflector(),
)
context['ФИО'].inflect(Case.DATV) # Иванову Петру Семёновичу
context['Должность'].inflect(Case.DATV) # старшему механику-водителю
Разметка шаблона
Падеж задаётся местом подстановки, а не данными: один и тот же человек в шапке стоит в именительном, а в теле приказа — в дательном, и шаблон говорит об этом фильтром.
Падежные фильтры:
{{ ФИО | ип }} именительный Иванов Пётр Семёнович
{{ ФИО | рд }} родительный Иванова Петра Семёновича
{{ ФИО | дт }} дательный Иванову Петру Семёновичу
{{ ФИО | вн }} винительный Иванова Петра Семёновича
{{ ФИО | тв }} творительный Ивановым Петром Семёновичем
{{ ФИО | пр }} предложный Иванове Петре Семёновиче
Тег без фильтра даёт именительный. Фильтр применим к любому склоняемому значению — ФИО, части имени, должности, званию, произвольной колонке.
Согласование по полу — фильтр согл, мужская форма первой; пол
берётся из ФИО (определяется автоматически по имени и отчеству):
{{ ФИО | согл('ознакомлен', 'ознакомлена') }}
Теги ФИО. Колонка «ФИО» разбирается на части, а части собираются обратно, поэтому доступны сразу все формы:
{{ ФИО }} Иванов Пётр Семёнович
{{ Фамилия }} Иванов {{ Имя }}, {{ Отчество }} — так же
{{ Инициалы }} Иванов П. С.
{{ Инициалы_впереди }} П. С. Иванов
{{ Инициалы_слитно }} Иванов П.С.
{{ Имя_инициал }} П. одна буква с точкой
Инициалы тоже склоняются: {{ Инициалы | рд }} → «Иванова П. С.».
Пустое отчество не роняет рендер — форма просто становится короче.
Остальные колонки доступны по своему заголовку: {{ Должность | дт }},
{{ Номер приказа }}. Заголовок нормализуется (пробелы → подчёркивания,
спецсимволы вычищаются), поэтому «л/н» в таблице пишется в шаблоне как
{{ л_н }}.
Неизвестная переменная — ошибка, а не пустое место в готовом приказе: шаблон рендерится в строгом режиме. Пустое значение, наоборот, убирается вместе с осиротевшей пунктуацией.
Разработка
Менеджер зависимостей и окружения: uv.
uv sync # поставить зависимости
uv run pytest # прогнать тесты
Зависимости
uv add <pkg> # runtime
uv add --dev <pkg> # dev
Ручные формы склонения
Русская морфология неоднозначна, и на редких фамилиях, иностранных
именах или несогласуемых оборотах движок ошибается. Библиотека не
притворяется безошибочной: любую форму можно задать вручную —
конфигурацией YAML, которую читает load_config.
overrides:
# Ключ — ФИО целиком, как в таблице. Значение — формы ЦЕЛОГО ФИО.
fio:
"Бивень Иван Петрович":
дт: "Бивеню Ивану Петровичу"
рд: "Бивеня Ивана Петровича"
# Ключ — должность как в таблице.
position:
"заместитель генерального директора":
рд: "заместителя генерального директора"
дт: "заместителю генерального директора"
# Ключ — звание как в таблице.
rank:
"младший лейтенант юстиции":
дт: "младшему лейтенанту юстиции"
# Ручное указание пола для неоднозначных имён (перекрывает автоопределение).
genders:
"Саган Мишель Леоновна": ж
# Фамилии-нарицательные, которые НАДО склонять: по умолчанию не опознанные
# морфологией как фамилии (Бивень, Кузнец) остаются в именительном.
decline_surnames:
- Бивень
Падежи задаются теми же сокращениями, что и фильтры шаблона
(ип/рд/дт/вн/тв/пр), и указывать все шесть не нужно —
незаданные возьмёт движок. Ручная форма всегда имеет приоритет.
У overrides.fio ключ — ФИО целиком, и заданная форма подставляется
тоже целиком. Части ({{ Фамилия | дт }}) и инициалы
({{ Инициалы | дт }}) продолжает склонять движок: разобрать готовую
форму обратно на фамилию, имя и отчество нельзя. Если нужно поправить
именно часть — правьте таблицу или заводите отдельную колонку.
Конфигурация целиком необязательна: без файла работает пустая.
Ошибки
Все ожидаемые ошибки библиотеки наследуют ChancelleryError:
ConfigError, TableError, TemplateError (и его частный случай
UndefinedVariableError), ReverseError. Один обработчик ловит весь
слой:
from chancellery import ChancelleryError
try:
generate_documents(table, template, out)
except ChancelleryError as exc:
print(f'Ошибка: {exc}')
Если у вашего приложения есть свой корень ошибок, его удобно
подвесить под библиотечный — тогда один except ловит оба слоя:
class MyAppError(ChancelleryError):
"""Корень ошибок приложения."""
Ловушка, на которой спотыкаются при переходе на библиотеку.
Наследование корня само по себе не чинит уже написанные
обработчики. Код, который ловил except MyAppError, перестанет
ловить ошибки движка: они наследуют ChancelleryError, а не ваш
корень — наследование идёт в другую сторону. Обработчики верхнего
уровня надо переписать на except ChancelleryError, иначе вместо
понятного сообщения пользователь получит трейсбек. Ровно это всплыло
при переводе «Дьяка»: четыре обработчика в его CLI перестали ловить
TableError и TemplateError.
Выпуск версии
scripts/publish.sh --test # репетиция на TestPyPI (нужен PYPI_TEST_TOKEN)
scripts/publish.sh # боевой PyPI
Скрипт сам гоняет четыре гейта, собирает колесо и архив исходников,
проверяет артефакты twine и выкладывает. Токен читается из .secrets
(под git-ignore, образец — .secrets.example) и машину не покидает.
Порядок: закрыть версию в CHANGELOG.md, поднять version в
pyproject.toml, слить, поставить git-тег вида 0.1.0, выложить.
Проверки перед push
uv run ruff check .
uv run ruff format --check .
uv run mypy src
scripts/pytest-guard.sh --cov=src --cov-report=term-missing --cov-fail-under=80
Все четыре должны проходить с 0 ошибок. Обходные манёвры (# noqa,
# type: ignore, расширение ignore-секции) — только по согласованию.
Структура проекта
src/— корень исходников.CONCEPT.md— изначальное видение проекта (immutable).DECISIONS.md— архитектурные решения с обоснованиями (ADR-Lite).BOARD.md— рабочая Kanban-доска (To Do / Doing / Done).BACKLOG.md— парковка идей и побочных находок.CHANGELOG.md— журнал заметных изменений.specs/— спецификации крупных фич.CLAUDE.md— проектные правила для Claude (Claude Code).
Методика работы
Проект создан из шаблона vlakir/dreamteam. Подробное описание методики (scope discipline, ритуал spec/clarify/analyze для крупных фич, pre-push контроль) — см. репозиторий шаблона.
Release files for chancellery 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| chancellery-0.2.0.tar.gz | 192.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| chancellery-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 267.4 kB
Release files / chancellery-0.2.0.tar.gz
| Download URL | chancellery-0.2.0.tar.gz |
|---|---|
| Size | 192.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
797542eeb0db87ebee9bdd1fd0294bbb23d04420dccbee3198eff2545c0b7c0b
|
|
BLAKE2b-256 checksum How to use checksums |
676430399ec44195c4a8cabcea139f1e698de98112e04ee3d8ba18b8938b8635
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / chancellery-0.2.0-py3-none-any.whl
| Download URL | chancellery-0.2.0-py3-none-any.whl |
|---|---|
| Size | 75.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
422718a89f193076b9b0b7d86376fa34db77c9dc18fee4f50bac3acb1fb79b94
|
|
BLAKE2b-256 checksum How to use checksums |
bf7871cd8cd57dfca43f398b8027c389d73722d457f53dbf3a0d72529feaec38
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|