Папка scripts в Python: структура и употребление

Раздел: Прочее -> Структура проектов

Папка scripts в Python проекте: назначение и варианты использования

Папка scripts в корне Python проекта служит для хранения вспомогательных скриптов, которые не являются частью основного пакета, но необходимы для разработки, тестирования, развертывания или администрирования. Обычно такие скрипты запускаются из командной строки и не импортируются как модули. Ниже рассмотрены основные варианты наполнения этой папки, сопровождающиеся вопросами, примерами кода и типичными проблемами.

1. Как организовать одноразовые утилиты (миграции, наполнение данными)?

Наиболее распространенный сценарий - размещение скриптов для миграции базы данных, первичного наполнения тестовыми данными, очистки логов и других разовых задач. Такие скрипты выполняются разработчиком или администратором по мере необходимости.

Пример скрипта migrate_db.py:

#!/usr/bin/env python3
"""Apply database migrations."""
import sys
from myapp.database import create_connection, run_migrations

def main():
    conn = create_connection()
    try:
        run_migrations(conn)
        print("Migrations applied successfully.")
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)
    finally:
        conn.close()

if __name__ == "__main__":
    main()

папка scripts python (папка scripts в python проекте)

Пояснения:

  • В начале файла указан shebang #!/usr/bin/env python3, чтобы скрипт можно было запускать напрямую (./migrate_db.py).
  • Код размещен внутри функции main() для удобства тестирования и повторного использования.
  • Обработка ошибок и ненулевой код завершения при неудаче.

Проблема: Если скрипт не имеет расширения .py, система может не ассоциировать его с интерпретатором Python. Рекомендуется либо добавлять расширение, либо явно указывать python в командной строке.

Ошибка: Отсутствие shebang на Linux/Mac приводит к ошибке разрешения (Permission denied) при попытке запуска как исполняемого файла. Решение - установить флаг chmod +x scripts/migrate_db.py.

2. Как автоматизировать сборку, тесты или деплой?

Скрипты для CI/CD, сборки пакета (setup.py), генерации документации или запуска линтеров часто помещают в scripts. Они вызываются из Makefile, конфигурации CI или вручную.

Пример scripts/build.sh (bash-обёртка):

#!/bin/bash
set -euo pipefail
# Build the Python package
echo "Building package..."
pip install build
python -m build
# Run tests after build
echo "Running tests..."
python -m pytest tests/

Или эквивалентный Python-скрипт scripts/build.py:

#!/usr/bin/env python3
import subprocess
import sys

def main():
    try:
        subprocess.run(["pip", "install", "build"], check=True)
        subprocess.run([sys.executable, "-m", "build"], check=True)
        subprocess.run([sys.executable, "-m", "pytest", "tests/"], check=True)
    except subprocess.CalledProcessError as e:
        print(f"Step failed: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

Проблема: Кроссплатформенность. Bash-скрипт не работает под Windows без WSL или Git Bash. Python-скрипт более универсален, но требует зависимости subprocess и осторожности с кодировкой вывода.

Ошибка: Использование sys.executable может указывать на виртуальное окружение, которое неактивно при запуске через cron. Рекомендуется указывать полный путь или использовать which python.

3. Как создать точку входа для Docker или сервера?

В контейнеризированных приложениях папка scripts может содержать entrypoint.sh, healthcheck.py или скрипты инициализации. Они запускаются при старте контейнера.

Пример scripts/entrypoint.py:

#!/usr/bin/env python3
import os
import sys
from myapp import create_app

def main():
    # Чтение конфигурации из переменных окружения
    host = os.getenv("HOST", "0.0.0.0")
    port = int(os.getenv("PORT", 5000))
    debug = os.getenv("DEBUG", "false").lower() == "true"

    app = create_app()
    print(f"Starting server on {host}:{port}")
    app.run(host=host, port=port, debug=debug)

if __name__ == "__main__":
    main()

Dockerfile использует этот скрипт как ENTRYPOINT: COPY scripts/entrypoint.py /entrypoint.py и ENTRYPOINT ["python", "/entrypoint.py"].

Проблема: Скрипт должен быть совместим с сигналами завершения (SIGTERM). Рекомендуется использовать signal модуль или библиотеку graceful.

Ошибка: Использование if __name__ == "__main__" необходимо, иначе при импорте (например, в тестах) произойдет запуск сервера.

4. Как хранить демонстрационные и нагрузочные скрипты?

Для демонстрации возможностей библиотеки или проведения бенчмарков удобно положить скрипты в подпапку scripts/demo или scripts/benchmarks. Они не входят в состав пакета, но документируют типовые сценарии.

Пример scripts/demo/quickstart.py:

#!/usr/bin/env python3
"""Demo script showing basic usage of myapp."""
from myapp import Client

def main():
    client = Client(api_key="demo-key")
    result = client.fetch_data(endpoint="/sample")
    print("Data received:")
    for item in result:
        print(f"  - {item['name']}: {item['value']}")

if __name__ == "__main__":
    main()

Проблема: Зависимости демо-скриптов могут отличаться от основного проекта. Лучше вынести их в отдельный файл требований requirements-demo.txt.

Ошибка: Вставка жёстко закодированных ключей API. Использовать переменные окружения или конфигурационный файл.

5. Как организовать интеграционные скрипты (синхронизация, уведомления)?

Для задач, работающих по расписанию (cron, Celery), можно размещать в scripts выполняемые модули. Например, scripts/sync_data.py, вызываемый cron-демоном.

Пример scripts/sync_data.py с обработкой аргументов:

#!/usr/bin/env python3
import argparse
import logging
from myapp.sync import synchronize

def parse_args():
    parser = argparse.ArgumentParser(description="Sync data from external API.")
    parser.add_argument("--source", required=True, help="Source API URL")
    parser.add_argument("--dry-run", action="store_true", help="Simulate sync")
    return parser.parse_args()

def main():
    args = parse_args()
    logging.basicConfig(level=logging.INFO)
    try:
        synchronize(source=args.source, dry_run=args.dry_run)
    except Exception as exc:
        logging.error("Sync failed: %s", exc)
        raise SystemExit(1)

if __name__ == "__main__":
    main()

Запуск: python scripts/sync_data.py --source https://api.example.com/data --dry-run.

Проблема: Логирование в cron не попадает в syslog, если не настроить вывод. Использовать logging.handlers.SysLogHandler или писать в файл.

Ошибка: Забыли обработать SystemExit в родительском процессе. Рекомендуется возвращать код возврата через sys.exit.

Расширенные примеры

Пример 1: Скрипт миграции БД с аргументами и интерактивным подтверждением

Скрипт scripts/migrate.py позволяет указать направление миграции (up/down) и номер версии.

Пример
#!/usr/bin/env python3
"""Database migration script."""
import argparse
import sys
from myapp.database import get_migrations, apply_migration, revert_migration

def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument("action", choices=["up", "down"], help="Migration direction")
    parser.add_argument("--version", type=int, default=None, help="Target version")
    parser.add_argument("--force", action="store_true", help="Skip confirmation")
    return parser.parse_args()

def confirm():
    answer = input("Apply migration? (y/n): ").strip().lower()
    return answer in ("y", "yes")

def main():
    args = parse_args()
    migrations = get_migrations()
    if args.version is None:
        args.version = len(migrations) if args.action == "up" else 0

    if not args.force:
        print(f"About to apply {args.action} to version {args.version}")
        if not confirm():
            print("Cancelled.")
            sys.exit(0)

    try:
        if args.action == "up":
            apply_migration(version=args.version, migrations=migrations)
        else:
            revert_migration(version=args.version, migrations=migrations)
        print("Migration successful.")
    except Exception as exc:
        print(f"Migration failed: {exc}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

Результат запуска без --force:

$ python scripts/migrate.py up --version 3
About to apply up to version 3
Apply migration? (y/n): y
Migration successful.

Пример 2: Docker healthcheck скрипт на Python

Проверяет доступность HTTP-эндпоинта и возвращает 0/1.

Пример
#!/usr/bin/env python3
"""Healthcheck script for Docker container."""
import http.client
import os
import sys

def main():
    host = os.getenv("HEALTHCHECK_HOST", "localhost")
    port = int(os.getenv("HEALTHCHECK_PORT", 5000))
    endpoint = os.getenv("HEALTHCHECK_PATH", "/health")

    conn = http.client.HTTPConnection(host, port, timeout=5)
    try:
        conn.request("GET", endpoint)
        response = conn.getresponse()
        if response.status == 200:
            print("Healthcheck passed.")
            sys.exit(0)
        else:
            print(f"Healthcheck failed: HTTP {response.status}")
            sys.exit(1)
    except Exception as exc:
        print(f"Healthcheck error: {exc}")
        sys.exit(1)
    finally:
        conn.close()

if __name__ == "__main__":
    main()

В Dockerfile:

Пример
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD ["python", "scripts/healthcheck.py"]

Пример 3: Скрипт пакетного выполнения с протоколированием в файл

Выполняет несколько команд (например, сборку статики, миграцию, тесты) и пишет лог в файл.

Пример
#!/usr/bin/env python3
"""CI pipeline script."""
import logging
import subprocess
import sys
from datetime import datetime

def setup_logger(log_file="build.log"):
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s: %(message)s",
        handlers=[
            logging.FileHandler(log_file),
            logging.StreamHandler(sys.stdout)
        ]
    )
    return logging.getLogger(__name__)

def run_step(command, label, logger):
    logger.info("Starting step: %s", label)
    result = subprocess.run(command, capture_output=True, text=True)
    if result.returncode != 0:
        logger.error("Step '%s' failed with code %d", label, result.returncode)
        logger.error("STDERR: %s", result.stderr)
        sys.exit(result.returncode)
    logger.info("Step '%s' completed successfully.", label)
    for line in result.stdout.split("\n"):
        if line.strip():
            logger.debug("  %s", line)

def main():
    logger = setup_logger()
    logger.info("Pipeline started at %s", datetime.now().isoformat())

    steps = [
        (["python", "-m", "pip", "install", "-r", "requirements-dev.txt"], "Install dev deps"),
        (["python", "-m", "flake8", "myapp", "scripts"], "Lint"),
        (["python", "-m", "pytest", "tests/", "--verbose"], "Tests"),
        (["python", "scripts/migrate.py", "up"], "Database migration"),
    ]

    for cmd, label in steps:
        run_step(cmd, label, logger)

    logger.info("Pipeline finished successfully.")

if __name__ == "__main__":
    main()

Результат (фрагмент build.log):

2025-03-15 10:23:45,123 INFO: Pipeline started at 2025-03-15T10:23:45.123456
2025-03-15 10:23:45,567 INFO: Starting step: Install dev deps
2025-03-15 10:23:56,789 INFO: Step 'Install dev deps' completed successfully.
2025-03-15 10:23:57,012 INFO: Starting step: Lint
...

Пример 4: Скрипт генерации отчёта с использованием Jinja2

Читает данные из JSON, рендерит шаблон и сохраняет HTML.

Пример
#!/usr/bin/env python3
"""Generate report from data."""
import json
import sys
from pathlib import Path
from jinja2 import Environment, FileSystemLoader

def load_data(path: Path) -> dict:
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)

def render_report(data: dict, template_name: str) -> str:
    env = Environment(loader=FileSystemLoader("scripts/templates"))
    template = env.get_template(template_name)
    return template.render(data=data)

def main():
    if len(sys.argv) < 3:
        print("Usage: generate_report.py  ", file=sys.stderr)
        sys.exit(1)

    data_path = Path(sys.argv[1])
    output_path = Path(sys.argv[2])

    if not data_path.is_file():
        print(f"Data file not found: {data_path}", file=sys.stderr)
        sys.exit(1)

    data = load_data(data_path)
    html = render_report(data, "report.jinja2")

    output_path.parent.mkdir(parents=True, exist_ok=True)
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(html)

    print(f"Report generated: {output_path}")

if __name__ == "__main__":
    main()

Шаблон scripts/templates/report.jinja2:

Пример
<!DOCTYPE html>
<html>
<head><title>Report</title></head>
<body>
<h1>Report for {{ data.title }}</h1>
<ul>
{% for item in data.items %}
    <li>{{ item.name }}: {{ item.value }}</li>
{% endfor %}
</ul>
</body>
</html>

Пример 5: Кроссплатформенный скрипт для запуска тестов с выбором окружения

Пример
#!/usr/bin/env python3
"""Run tests in different environments."""
import os
import platform
import subprocess
import sys

def get_venv_python():
    """Return python executable from active venv or system."""
    if hasattr(sys, "real_prefix") or (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix):
        return sys.executable
    # Fallback
    return "python3" if platform.system() != "Windows" else "python"

def main():
    python = get_venv_python()
    print(f"Using Python: {python}")

    commands = [
        [python, "-m", "pytest", "--cov=myapp", "--cov-report=term-missing"],
        [python, "-m", "black", "--check", "myapp", "scripts"],
        [python, "-m", "isort", "--check", "myapp", "scripts"],
    ]

    for cmd in commands:
        print(f"Running: {' '.join(cmd)}")
        subprocess.run(cmd, check=True)

if __name__ == "__main__":
    main()

Результат на Linux:

Using Python: /home/user/project/.venv/bin/python3
Running: /home/user/project/.venv/bin/python3 -m pytest --cov=myapp --cov-report=term-missing
...
Running: /home/user/project/.venv/bin/python3 -m black --check myapp scripts
...
Running: /home/user/project/.venv/bin/python3 -m isort --check myapp scripts
...

Папка scripts в Python проекте - comments

En
папка scripts python (python)