Skip to main content

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

Три движка склонения

Склоняемое значение выбирает потребитель — движки не угадывают тип строки сами:

  • Phrase — произвольная фраза: должность, подразделение, любая текстовая колонка. Склоняет голову с согласованными словами, замораживает родительный хвост.
  • Rank — воинское или служебное звание: голова склоняется, генитивный хвост замирает.
  • Staff — штатная формулировка из штатного расписания, где вокруг головы стоят шифры, сокращения и номера: «1 рота ПРТД БпС», «Старший оператор», «Войсковая часть 12345».
from chancellery import Case, Staff, StaffInflector

inflector = StaffInflector()  # создаётся один раз, кеширует морфологию
Staff('1 рота ПРТД БпС', inflector).inflect(Case.GENT)  # '1 роты ПРТД БпС'
Staff('Старший оператор', inflector).inflect(Case.ACCS)  # 'старшего оператора'

Staff отличается от Phrase тем, что узнаёт сокращения в лицо и не трогает их: латиницу (FPV), смешанный регистр (БпС, БпЛА) и короткий капс (ТЯЖ, ПРТД). Кричащий капс обычного слова, наоборот, гасится («ОТДЕЛЕНИЕ СВЯЗИ» → «отделения связи»), а часть наименования с одной заглавной сохраняется («Южного военного округа»). Голова ищется как первое существительное без прилагательного разбора, поэтому «Старший оператор» даёт «старшего оператора», а не «старшего оператор».

Рядом — lemmas(text), нормальные формы слов фразы: «2 роты БпЛА Р-У» и «РОТА FPV КТ» обе содержат «рота», поэтому строки можно искать по смыслу, а не по написанию. И is_abbreviation(word) — тот самый признак сокращения.

Ручные формы склонения

Русская морфология неоднозначна, и на редких фамилиях, иностранных именах или несогласуемых оборотах движок ошибается. Библиотека не притворяется безошибочной: любую форму можно задать вручную — конфигурацией 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.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for chancellery 0.3.0
File Size Uploaded
chancellery-0.3.0.tar.gz 203.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for chancellery 0.3.0
File Interpreter ABI Platform
chancellery-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 284.8 kB

Release files / chancellery-0.3.0.tar.gz

Download URL chancellery-0.3.0.tar.gz
Size 203.5 kB
Tags Source
SHA-256 checksum
How to use checksums
f7d0132a2e285b94ce61d561d30144c659480e92707ab564fb98367821aafb81
BLAKE2b-256 checksum
How to use checksums
580dc8d75d7ff8ea50b7f5fd01ea3c537e3ee03fd821278f8c134a180612a655
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.3.0-py3-none-any.whl

Download URL chancellery-0.3.0-py3-none-any.whl
Size 81.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ea1f6f94bc09fe0c0aabc707d826dcdf6e5546db74a227053c36e33d60c10226
BLAKE2b-256 checksum
How to use checksums
4cf43dabc7d5cc7b07a50ab00b1860be182960cb257bf3d01867c1fd19ceb8ce
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 history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page