os-craft
RU: Легковесные, надежные и типизированные утилиты для взаимодействия Python-приложений с операционной системой.
EN: Lightweight, robust, and strictly typed utilities for seamless Python-to-OS interaction.
🚀 Installation / Установка
# Рекомендуемый способ (через uv - молниеносный пакетный менеджер)
uv add os-craft
# Классический способ
pip install os-craft
🛡️ Core Feature: Graceful Shutdown Manager
Почему это важно? (Why it matters)
Когда оркестратор (Kubernetes, systemd) или пользователь нажимает Ctrl+C, ОС отправляет процессу сигнал SIGTERM или SIGINT. По умолчанию Python просто прерывает выполнение. Если в этот момент ваша программа:
Писала данные в базу данных → транзакция оборвется, данные могут повредиться.
Обрабатывала фоновую задачу → результат будет потерян.
Держала открытые файловые дескрипторы → возможны утечки ресурсов.
ShutdownManager из os-craft перехватывает эти сигналы и гарантирует упорядоченное (LIFO), ограниченное по времени (timeout) и безопасное освобождение ресурсов. После завершения всех хуков процесс автоматически завершается с кодом выхода 0.
Quick Start / Быстрый старт
import asyncio
import logging
import sys
from os_craft import ShutdownManager
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def close_database():
"""Пример хука: безопасное закрытие соединения с БД."""
logger.info("Закрываем соединения с БД...")
await asyncio.sleep(1) # Имитация работы
logger.info("БД безопасно отключена.")
async def stop_background_worker():
"""Пример хука: остановка фонового воркера."""
logger.info("Останавливаем фоновый воркер...")
await asyncio.sleep(0.5) # Имитация завершения текущей задачи
logger.info("Воркер остановлен.")
async def main():
# 1. Создаем менеджер с общим таймаутом 5 секунд
manager = ShutdownManager(total_timeout=5.0)
# 2. Регистрируем хуки. Они выполняются в обратном порядке (LIFO).
# Сначала остановим воркера, потом закроем БД.
manager.add_hook(close_database)
manager.add_hook(stop_background_worker)
# 3. Подписываемся на сигналы ОС (SIGINT, SIGTERM)
# Активный event loop определяется внутри автоматически.
manager.attach_to_signals()
logger.info("Приложение запущено. Нажмите Ctrl+C для корректного завершения.")
# Имитация долгой работы приложения.
# При получении сигнала менеджер корректно отменит эту задачу.
try:
while True:
await asyncio.sleep(1)
except asyncio.CancelledError:
pass # Ожидаем, пока manager завершит все хуки
return manager
if __name__ == "__main__":
# Очистка завершена, получаем код выхода (0 - успех, 1 - ошибка в хуке)
manager = asyncio.run(main())
sys.exit(manager.exit_code)
Что произойдет при нажатии Ctrl+C:
1. INFO | Получен сигнал остановки ОС. Инициируем graceful shutdown...
2. INFO | Начинаем graceful shutdown. Зарегистрировано хуков: 2
3. INFO | Выполняем хук: stop_background_worker (доступно 5.00s)
4. INFO | Останавливаем фоновый воркер...
5. INFO | Воркер остановлен.
6. INFO | Выполняем хук: close_database (доступно 4.50s)
7. INFO | Закрываем соединения с БД...
8. INFO | БД безопасно отключена.
9. INFO | Процесс graceful shutdown успешно завершен.
После этого asyncio.run(main()) корректно вернёт управление в __main__,
и процесс завершится с кодом выхода 0. Если какой-либо хук упал или превысил
таймаут — manager.exit_code будет равен 1, и оркестратор (Kubernetes, systemd)
узнает о проблеме.
Что произойдет при сбое хука:
1. INFO | Выполняем хук: bad_hook (доступно 5.00s)
2. ... | Необработанная ошибка в хуке bad_hook. Продолжаем.
3. ERROR | Graceful shutdown завершен с ошибками (см. логи выше).
4. → Оставшиеся хуки всё равно выполнятся, процесс выйдет с кодом 1.
📖 Real-World Example: FastAPI Integration
Полный пример интеграции с FastAPI (с фоновыми задачами и имитацией БД) доступен в директории examples/fastapi_graceful_shutdown.py. Запуск:
uv run python examples/fastapi_graceful_shutdown.py
Проверка:
1. Откройте другой терминал: curl http://127.0.0.1:8000/
2. Вернитесь в терминал с сервером и нажмите Ctrl+C
3. Наблюдайте за корректным завершением всех ресурсов
📋 Config Manager: строгая загрузка конфигурации
Почему это важно? (Why it matters)
Большинство приложений берут настройки из трёх источников: значения по умолчанию, конфиг-файл и переменные окружения. Ручное склеивание этих источников быстро превращается в простыню из os.getenv с ручным приведением типов (int(os.getenv("PORT")) or 8080). Результат — баги типов, нечитаемый код и магические значения.
load_config из os-craft решает это одним вызовом:
- Схема = dataclass. Описываете конфигурацию как обычный типизированный dataclass — и получаете автодополнение в IDE и проверку типов через mypy.
- Три источника с честным приоритетом. Значения по умолчанию → TOML-файл → переменные окружения (ENV побеждает).
- Автоматическое приведение типов. Строки из ENV превращаются в
int,float,bool,Pathи дажеlist[...]без единогоint(...)в вашем коде. - Ноль тяжёлых зависимостей. Используется только стандартная библиотека (на Python 3.10 — официальный backport
tomli).
Quick Start / Быстрый старт
import logging
from dataclasses import dataclass, field
from pathlib import Path
from os_craft import load_config
logging.basicConfig(level=logging.INFO)
# 1. Описываем схему как обычный dataclass
@dataclass
class AppConfig:
host: str = "0.0.0.0"
port: int = 8080
debug: bool = False
allowed_origins: list[str] = field(default_factory=list)
db_timeout: float = 5.0
data_dir: Path = Path("/var/lib/app")
# 2. Загружаем: default -> config.toml -> переменные окружения
config = load_config(
config_cls=AppConfig,
env_prefix="APP", # ищем APP_HOST, APP_PORT, APP_DEBUG...
file_path=Path("config.toml"),
)
print(config.port) # тип int
print(config.allowed_origins) # тип list[str]
Пример файла config.toml:
# config.toml
host = "127.0.0.1"
port = 8000
debug = true
allowed_origins = ["http://localhost:3000"]
Приоритет источников (от низшего к высшему)
1. Значения по умолчанию в dataclass.
2. Значения из TOML-файла (если path указан и файл существует).
3. Переменные окружения с префиксом {env_prefix}_ (например, APP_PORT).
Переменные окружения имеют наивысший приоритет — это ключевой паттерн для контейнеров (Docker/Kubernetes), где конфиг-файл лежит в образе, а специфичные для окружения значения передаются через ENV.
Поддерживаемые типы
| Тип поля | Из TOML | Из ENV (строка → тип) |
|---|---|---|
str |
строка | как есть |
bool |
true / false |
"true", "1", "yes", "on" → True; всё остальное → False |
int |
число | "8080" → 8080 |
float |
число | "5.0" → 5.0 |
Path |
путь (строка) | "/var/lib" → Path("/var/lib") |
list[...] |
список | "a,b,c" → ["a", "b", "c"] (элементы приводятся к внутреннему типу, например list[int]) |
Если тип поля не входит в этот список (например, tuple), загрузка завершится
понятной ошибкой — неподдерживаемый тип не нарушается молча.
Ошибки (гарантии библиотеки)
- Обязательное поле (без дефолта), не заполненное нигде → корректный
TypeErrorот dataclass, а не мусорное значение. - Неверный тип в ENV или TOML →
ValueErrorс именем поля и причиной:Не удалось привести значение ENV APP_PORT='not_a_number' к типу <class 'int'>. - Передан не dataclass →
TypeError.
Real-World Example / Пример
Полный пример доступен в examples/config_usage.py, демо-конфиг — в examples/config.toml:
uv run python examples/config_usage.py
Проверка переопределения через ENV (значения из TOML должны перезаписаться):
APP_PORT=9090 APP_DEBUG=false \
APP_ALLOWED_ORIGINS="https://api.com,https://admin.com" \
uv run python examples/config_usage.py
Ожидаемый результат: Port: 9090, Debug: False, а строка из ENV автоматически
превратилась в список ['https://api.com', 'https://admin.com'].
Тесты / Checks
Модуль покрыт тестами в tests/test_config.py:
- значения по умолчанию без файлов и ENV;
- загрузка и парсинг TOML;
- неверный тип в TOML →
ValueError; - ENV-переопределение с приведением типов (включая
list[str]); Pathиlist[int]из ENV;- обязательное поле без значения →
TypeError(никакого MISSING-sentinel); - неподдерживаемый тип →
ValueErrorс понятным сообщением; - не-dataclass →
TypeError.
Запуск всех тестов проекта (31 тест: shutdown + config + hot_reload):
uv run pytest -v
🔄 Hot-Reload: горячая перезагрузка конфигурации
ConfigError + HotReloadConfig добавляют к статичной загрузке возможность
менять настройки на лету, без перезапуска процесса. Это критично для
продолжительных сервисов (воркеров, ботов, демонов), где рестарт дорог или
нежелателен.
Как это работает:
HotReloadConfigпри создании загружает конфигурацию черезload_configи запускает фоновый поток-watcher.- Каждые
poll_intervalсекунд поток проверяетmtimeфайла. - При изменении файла конфигурация перезагружается (с теми же приоритетами default → TOML → ENV) и вызываются все зарегистрированные коллбэки.
- Рабочий код читает актуальные значения через свойство
.config— без блокировок и без ручных проверок. - Watcher отказоустойчив: сбой перезагрузки или ошибка в коллбэке не убивают поток — старая конфигурация сохраняется, а проблема логируется.
Quick Start / Быстрый старт:
import logging
from dataclasses import dataclass
from pathlib import Path
from os_craft import HotReloadConfig
logging.basicConfig(level=logging.INFO)
@dataclass
class AppSettings:
app_name: str # обязательное поле
max_workers: int = 4
feature_flag_new_ui: bool = False
# Коллбэк вызовется автоматически при каждом изменении файла
def on_change(new_config: AppSettings):
logging.info(f"config updated: workers={new_config.max_workers}")
manager = HotReloadConfig(
config_cls=AppSettings,
file_path=Path("config.toml"),
env_prefix="APP",
poll_interval=2.0, # проверка каждые 2 секунды
)
manager.add_callback(on_change)
# В основном цикле всегда читаем актуальную конфигурацию через .config:
while True:
current = manager.config # всегда свежие значения
...
# При завершении приложения обязательно останавливаем фоновый поток:
manager.stop()
Поведение и гарантии:
- Отсутствующий файл при создании →
ConfigError. - Файл удалили во время работы → watcher ждёт восстановления, используя последнюю загруженную конфигурацию.
- Некорректные значения в изменённом файле → перезагрузка не применяется, остаётся прежняя конфигурация.
- ENV по-прежнему имеет высший приоритет и учитывается при каждой перезагрузке.
stop()останавливает поток-watcher (с join-timeout 2 секунды).
Пример целиком: examples/hot_reload_usage.py (демо-конфиг — свой
examples/hot_reload.toml, чтобы не конфликтовать с примером config_usage.py).
Запуск:
uv run python examples/hot_reload_usage.py
Проверка: откройте examples/hot_reload.toml, поменяйте max_workers на
другое значение и сохраните — через пару секунд в консоли появится лог
🔄 КОНФИГ ОБНОВЛЕН! с новыми значениями.
Тесты горячей перезагрузки — в tests/test_hot_reload.py:
- отсутствие файла →
ConfigError; - перезагрузка при изменении файла + срабатывание коллбэков;
- падающий коллбэк не убивает watcher;
- битый TOML — прежняя конфигурация сохраняется;
- ENV-переопределение применяется при перезагрузке;
stop()корректно останавливает поток.
⚙️ Технические детали (Technical Details)
-
LIFO Execution Order (Порядок LIFO)
Хуки выполняются в порядке "последний добавлен — первый выполнен". Это критично для корректного освобождения ресурсов:
- Сначала останавливаем прием новых задач (закрываем порт веб-сервера).
- Затем дожидаемся завершения текущих операций.
- Только потом закрываем соединения с базами данных и кэшем.
-
Unified Timeout (Единый таймаут)
Вместо таймаута на каждый хук используется общий таймаут на весь процесс shutdown. Это предотвращает ситуацию, когда первый хук "съедает" все время, а на важные последние хуки (закрытие БД) времени не остается. Если общий таймаут превышен, выполнение оставшихся хуков прерывается, и процесс завершается.
-
Async/Sync Agnostic (Поддержка синхронного и асинхронного кода)
Менеджер автоматически определяет тип функции через inspect.iscoroutinefunction:
- Async-хуки выполняются напрямую с await.
- Sync-хуки выполняются в loop.run_in_executor, чтобы никогда не блокировать asyncio event loop, даже если разработчик написал тяжелую синхронную операцию.
-
Fail-Safe Design (Отказоустойчивость)
Если один из хуков выбрасывает исключение или превышает таймаут, ShutdownManager:
- Логирует ошибку.
- Продолжает выполнение следующих хуков.
- Никогда не прерывает весь процесс очистки из-за одной ошибки.
- Помечает shutdown как неуспешный:
manager.exit_codeпосле завершения будет равен 1, чтобы оркестратор (Kubernetes, systemd) получил сигнал о проблеме вместо "тихого" кода 0.
-
Idempotency & Clean Exit (Идемпотентность и чистое завершение)
Повторный сигнал или повторный вызов выполнения хуков не запускает их заново. После завершения хуков менеджер корректно отменяет главную задачу приложения, поэтому
asyncio.run(main())возвращается штатно — безTask exception was never retrievedв логах — и код выхода можно прочитать черезmanager.exit_code. -
Automatic Process Termination (Автоматическое завершение процесса)
Процесс завершается сам: достаточно вернуть
managerизmain()и вызватьsys.exit(manager.exit_code). -
OS Signal Handling (Обработка сигналов ОС)
Менеджер подписывается на:
- SIGINT (Ctrl+C) — для локальной разработки.
- SIGTERM (сигнал 15) — для продакшена (Kubernetes, systemd, Docker).
⚠️ Важно: Сигнал SIGKILL (сигнал 9, kill -9) невозможно перехватить. Это защита ОС от зависших процессов. Всегда проектируйте систему так, чтобы она могла пережить внезапное завершение (например, используйте транзакции в БД).
🧪 Development / Разработка
Мы используем uv для молниеносного управления зависимостями и сборки.
Установка окружения
# Клонирование репозитория
git clone <repository_url>
cd os-craft
# Установка зависимостей (создаст .venv и установит все пакеты)
uv sync
# Установка пакета в editable-режиме (для локальной разработки)
uv pip install -e .
Запуск тестов
uv run pytest -v
Тесты покрывают все модули:
tests/test_shutdown.py— порядок LIFO, таймауты, sync/async хуки, идемпотентность, обработка реальных сигналов SIGINT.tests/test_config.py— значения по умолчанию, TOML, ENV-переопределения, приведение типов, обработка ошибок.tests/test_hot_reload.py— горячая перезагрузка конфигурации, коллбэки, отказоустойчивость watcher.
Проверка типов и линтинг
# Проверка типов (строгий режим)
uv run mypy src/os_craft
# Линтер (автоматическое исправление проблем)
uv run ruff check . --fix
Сборка пакета
uv build
Это создаст папку dist/ с .tar.gz и .whl файлами, готовыми для публикации на PyPI.
🏗️ Project Structure / Структура проекта
os-craft/
├── .github/
│ └── workflows/
│ └── ci.yml # CI: ruff, mypy strict, pytest (Python 3.10-3.12)
├── pyproject.toml # Конфигурация проекта, зависимостей и инструментов
├── uv.lock # Lock-файл для воспроизводимых сборок
├── README.md # Этот файл
├── LICENSE # MIT License
├── .gitignore # Исключения для git (вкл. editor swap-файлы)
├── .python-version # Версия Python для uv
├── src/
│ └── os_craft/
│ ├── __init__.py # Публичный API пакета (версия из metadata)
│ ├── shutdown.py # ShutdownManager (graceful shutdown)
│ ├── config.py # load_config + HotReloadConfig (конфигурация)
│ └── py.typed # Маркер для mypy (PEP 561)
├── tests/
│ ├── test_shutdown.py # Тесты для ShutdownManager
│ ├── test_config.py # Тесты для load_config
│ └── test_hot_reload.py # Тесты для HotReloadConfig
└── examples/
├── fastapi_graceful_shutdown.py # Пример интеграции с FastAPI
├── graceful_shutdown.py # Пример graceful shutdown без FastAPI
├── config_usage.py # Пример load_config
├── config.toml # Демо-конфиг для config_usage
├── hot_reload_usage.py # Пример HotReloadConfig
└── hot_reload.toml # Демо-конфиг для hot_reload_usage
🤝 Contributing / Участие в разработке
Мы приветствуем вклад в развитие проекта! Если вы нашли баг или хотите добавить новую утилиту:
1. Форкните репозиторий.
2. Создайте ветку для вашей фичи: git checkout -b feature/amazing-feature
3. Убедитесь, что все тесты проходят: uv run pytest
4. Проверьте линтер и типы: uv run ruff check . && uv run mypy src/os_craft
5. Сделайте коммит: git commit -m 'Add amazing feature'
6. Отправьте в main: git push origin feature/amazing-feature
7. Откройте Pull Request.
📄 License / Лицензия
MIT License. Свободно используйте в коммерческих и open-source проектах. См. файл LICENSE для подробностей.
🙏 Acknowledgments / Благодарности
Проект создан с любовью к чистому коду, принципу KISS и уважению к разработчикам, которые хотят писать надежные Python-приложения. Спасибо сообществу Python за потрясающие инструменты: asyncio, contextvars, uv, ruff, mypy.
RU: Если у вас есть вопросы или предложения, открывайте Issue или пишите в обсуждения.
EN: If you have any questions or suggestions, feel free to open an Issue or start a Discussion.
Metadata
Release files for os-craft 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| os_craft-0.1.3.tar.gz | 76.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| os_craft-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 96.9 kB
Release files / os_craft-0.1.3.tar.gz
| Download URL | os_craft-0.1.3.tar.gz |
|---|---|
| Size | 76.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
84197394467b5a270729e6b8ae8a1d45d19e05fbf9a96afbe6162a297290bcd2
|
|
BLAKE2b-256 checksum How to use checksums |
2c83cfd433257d15618efa2352dbcf9d589d64591a8c449538b8b42725444371
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"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 / os_craft-0.1.3-py3-none-any.whl
| Download URL | os_craft-0.1.3-py3-none-any.whl |
|---|---|
| Size | 20.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
de137136468739c49fad1cf10791db2b24ac4f844e2cefd5915bf106287f2daa
|
|
BLAKE2b-256 checksum How to use checksums |
610107ed1222f6061f8dd540c85e13de2325b22962d2007cb9dc82b199766409
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"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}
|