Папка 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 ...