nene2-python 1.8.54__tar.gz → 1.8.56__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nene2_python-1.8.54 → nene2_python-1.8.56}/PKG-INFO +1 -1
- nene2_python-1.8.56/docs/field-trials/2026-05-field-trial-184.md +427 -0
- nene2_python-1.8.56/docs/field-trials/2026-05-field-trial-185.md +246 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/INDEX.md +4 -2
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/todo/current.md +9 -6
- {nene2_python-1.8.54 → nene2_python-1.8.56}/pyproject.toml +1 -1
- {nene2_python-1.8.54 → nene2_python-1.8.56}/.env.example +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/.gitignore +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/AGENTS.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/CHANGELOG.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/CLAUDE.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/Dockerfile +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/LICENSE +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/README.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/alembic/README +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/alembic/env.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/alembic.ini +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/compose.yaml +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/de/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-168.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-169.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-170.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-171.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-172.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-173.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-174.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-175.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-176.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-177.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-178.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-179.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-180.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-181.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-182.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-183.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/fr/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/reference/api.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/roadmap.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/zh/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/package-lock.json +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/package.json +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/__main__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/app.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/mcp.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/schema.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/conftest.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/tests/scripts/test_export_openapi.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.56}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.56
|
|
4
4
|
Summary: NENE2 Python — minimal API framework following NENE2's design philosophy
|
|
5
5
|
Project-URL: Homepage, https://github.com/hideyukiMORI/nene2-python
|
|
6
6
|
Project-URL: Repository, https://github.com/hideyukiMORI/nene2-python
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# FT184: urllib.request モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: URL フェッチ・Basic 認証・SSRF 防御・スキームインジェクション対策
|
|
5
|
+
**セキュリティ診断**: なし(184 % 3 = 1)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
Python 標準ライブラリの `urllib.request` モジュールを検証する。
|
|
12
|
+
FT147(urllib.parse)が URL の解析にフォーカスしたのに対し、
|
|
13
|
+
今回は実際の HTTP リクエスト(GET/POST)・Basic 認証・レスポンス処理を実装する。
|
|
14
|
+
FT183(smtplib)のセキュリティ診断で発見した SSRF 問題に直接対応し、
|
|
15
|
+
`is_ssrf_safe()` による Private IP ブロックを実装・検証する。
|
|
16
|
+
クラッカーペンテスト(184 % 4 = 0)で SSRF・スキームインジェクション・URL 埋め込み認証情報の耐性を確認する。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 実装したサンプルアプリ
|
|
21
|
+
|
|
22
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft184-urllib-request/`
|
|
23
|
+
|
|
24
|
+
### 主要機能
|
|
25
|
+
|
|
26
|
+
| 関数/クラス | 概要 |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `validate_url(url)` | スキーム・長さ・構造を検証(http/https のみ許可) |
|
|
29
|
+
| `is_ssrf_safe(url)` | ホストが Private IP / ループバックでないことを確認(DNS 解決込み) |
|
|
30
|
+
| `fetch_url(url, timeout)` | GET フェッチ(バリデーション付き)→ `FetchResult` |
|
|
31
|
+
| `fetch_safe(url, timeout)` | SSRF 防御付き GET フェッチ |
|
|
32
|
+
| `fetch_json(url, timeout)` | GET + JSON パース → `dict | None` |
|
|
33
|
+
| `fetch_with_basic_auth(url, username, password, timeout)` | Basic 認証付き GET |
|
|
34
|
+
| `post_json(url, payload, timeout)` | JSON POST |
|
|
35
|
+
| `post_form(url, fields, timeout)` | `application/x-www-form-urlencoded` POST |
|
|
36
|
+
| `FetchResult` | `@dataclass(frozen=True, slots=True)` — フェッチ結果 |
|
|
37
|
+
|
|
38
|
+
### HTTP エンドポイント
|
|
39
|
+
|
|
40
|
+
| メソッド | パス | 概要 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| POST | `/fetch` | URL を GET でフェッチ |
|
|
43
|
+
| POST | `/fetch/safe` | SSRF 防御付きフェッチ(Private IP は 403) |
|
|
44
|
+
| POST | `/validate` | URL バリデーション + SSRF 安全性チェック |
|
|
45
|
+
| POST | `/fetch/auth` | Basic 認証付きフェッチ |
|
|
46
|
+
| POST | `/post/json` | JSON POST |
|
|
47
|
+
| POST | `/post/form` | フォーム POST |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## テスト結果
|
|
52
|
+
|
|
53
|
+
**64 passed**(初回から全通過)
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
64 passed in 0.84s
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
mypy: Success / ruff: All checks passed / pip-audit: PYSEC-2025-183(継続監視)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 摩擦ポイント
|
|
64
|
+
|
|
65
|
+
### F-1: `urllib.request.urlopen()` が `TimeoutError` ではなく `socket.timeout` を投げることがある(深刻度: 低)
|
|
66
|
+
|
|
67
|
+
**事象**: Python 3.11+ では `socket.timeout` は `TimeoutError` のサブクラスになったため、
|
|
68
|
+
`except TimeoutError:` で捕捉できる。しかし古いドキュメントや記事では
|
|
69
|
+
`except socket.timeout:` と書かれており、Python 版によって挙動が違う印象を与える。
|
|
70
|
+
|
|
71
|
+
**原因**: Python 3.3 から `socket.timeout` は `OSError` のサブクラスに。
|
|
72
|
+
Python 3.11 から `TimeoutError` のサブクラスにもなった。
|
|
73
|
+
`urllib.error.URLError` は `socket.timeout` を内包することが多い。
|
|
74
|
+
|
|
75
|
+
**対応**: `TimeoutError` を catch するように統一。Python 3.12+ では問題なし。
|
|
76
|
+
|
|
77
|
+
### F-2: `http.server.BaseHTTPRequestHandler` の `do_GET` / `do_POST` のシグネチャは mypy に注意(深刻度: 低)
|
|
78
|
+
|
|
79
|
+
**事象**: テスト用ローカルサーバーで `do_GET(self) -> None` を定義したが、
|
|
80
|
+
`self.path` の型が `str` と明示されていないため、
|
|
81
|
+
mypy が `N802`(UpperCase メソッド名)と `A002`(`format` 引数の隠蔽)を検出した。
|
|
82
|
+
|
|
83
|
+
**対応**: `# noqa: N802` / `# noqa: A002` を付与して抑制。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 観察点
|
|
88
|
+
|
|
89
|
+
### 観察1: `urllib.request` の例外階層はネストが深い
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
urllib.error.HTTPError # 4xx・5xx レスポンス(urllib.error.URLError のサブクラス)
|
|
93
|
+
urllib.error.URLError # ネットワークエラー・DNS 失敗等
|
|
94
|
+
OSError / TimeoutError # タイムアウト
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`HTTPError` は `URLError` のサブクラスなので、`except URLError` が `HTTPError` も捕捉する。
|
|
98
|
+
`HTTPError` は `fp` 属性(レスポンスボディのファイルライク)を持つため、
|
|
99
|
+
4xx エラー時でもボディを読み取れる。
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
except urllib.error.HTTPError as exc:
|
|
103
|
+
body = exc.read(MAX_RESPONSE_BYTES) if exc.fp else b""
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 観察2: SSRF 防御は DNS 解決後のチェックが必須
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# 不十分: IP アドレスのみチェック
|
|
110
|
+
if host.startswith("127."):
|
|
111
|
+
return False # "localhost" が通り抜ける
|
|
112
|
+
|
|
113
|
+
# 十分: DNS 解決後にチェック
|
|
114
|
+
ip_str = socket.gethostbyname(host)
|
|
115
|
+
addr = ipaddress.ip_address(ip_str)
|
|
116
|
+
return not any(addr in net for net in _PRIVATE_NETWORKS)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
DNS ピニング攻撃(短い TTL で公開 IP → プライベート IP に変更)への対策は
|
|
120
|
+
`gethostbyname()` の呼び出しと実際の接続の間でレースコンディションが発生する可能性があるが、
|
|
121
|
+
実用的な防御として十分。完全な対策はシステムレベルで行う。
|
|
122
|
+
|
|
123
|
+
### 観察3: `file://` スキームのブロックは `_ALLOWED_SCHEMES` で確実に防ぐ
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
_ALLOWED_SCHEMES = {"http", "https"}
|
|
127
|
+
|
|
128
|
+
def validate_url(url: str) -> bool:
|
|
129
|
+
parsed = urllib.parse.urlparse(url)
|
|
130
|
+
return parsed.scheme in _ALLOWED_SCHEMES and bool(parsed.netloc)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`urllib.request.urlopen("file:///etc/passwd")` はローカルファイルを読み取れる。
|
|
134
|
+
`FTP://`, `data:`, `javascript:` も同様にブロックすること。
|
|
135
|
+
|
|
136
|
+
### 観察4: HTTP Basic 認証はヘッダーで手動実装が明確
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import base64
|
|
140
|
+
credentials = base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
|
|
141
|
+
req.add_header("Authorization", f"Basic {credentials}")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`urllib.request.HTTPPasswordMgr` + `HTTPBasicAuthHandler` を使う方法もあるが、
|
|
145
|
+
ハンドラーの登録順序が複雑になる。手動ヘッダー設定の方がデバッグしやすく、
|
|
146
|
+
nene2-python のスコープ内では十分。
|
|
147
|
+
|
|
148
|
+
### 観察5: `FetchResult.body_text[:10_000]` でレスポンスの切り詰めが必要
|
|
149
|
+
|
|
150
|
+
API レスポンスでそのまま返すと、大きなページのボディをクライアントに送信してしまう。
|
|
151
|
+
HTTP エンドポイントでは最大 10KB に制限した。
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## nene2-python フレームワークとの統合
|
|
156
|
+
|
|
157
|
+
- `fetch_safe()` は FT183 の SSRF 発見に直接対応した実装で、
|
|
158
|
+
外部 HTTP コールを行う UseCase 全般に組み込める
|
|
159
|
+
- `FetchResult` の `ok` プロパティ + `status_code` で UseCase 内での成否判定が簡潔
|
|
160
|
+
- `_make_request()` は UseCase の外部 API ゲートウェイ(`HttpGatewayInterface`)の実装候補
|
|
161
|
+
- SSRF 防御の `_PRIVATE_NETWORKS` リストは `src/nene2/security/` に共通定数として抽出すべき
|
|
162
|
+
- `fetch_json()` は外部 API からのデータ取得 UseCase のテンプレートとして使える
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Developer Experience (DX) Review
|
|
167
|
+
|
|
168
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
169
|
+
|
|
170
|
+
外部天気 API を呼んでレスポンスを FastAPI で返す機能を実装しようとしている。
|
|
171
|
+
|
|
172
|
+
**ドキュメント理解**: `requests` ライブラリを使ったことがある人には
|
|
173
|
+
`urllib.request` の `Request` オブジェクト + `urlopen()` の組み合わせは冗長に見える。
|
|
174
|
+
`with urllib.request.urlopen(url) as resp: body = resp.read()` が最小パターン。
|
|
175
|
+
**事故リスク**: 高。`urlopen("file:///etc/passwd")` でローカルファイルが読めること、
|
|
176
|
+
内部 URL にアクセスできることを知らずに実装すると SSRF につながる。
|
|
177
|
+
**規約の使いやすさ**: `requests` に慣れていると `urllib.request` は冗長。
|
|
178
|
+
ただし標準ライブラリなので追加インストール不要な点は評価される。
|
|
179
|
+
|
|
180
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
181
|
+
|
|
182
|
+
スクレイピングスクリプトを API 化しようとしている。
|
|
183
|
+
既存スクリプトで `urllib.request.urlopen(url)` を直接使っている。
|
|
184
|
+
|
|
185
|
+
**コピペ可能性**: `requests.get(url)` のシンプルさに慣れたコードを移植する場合、
|
|
186
|
+
`urllib.request.Request` + `urlopen()` のパターンに気づかないと古い書き方(`urllib2`)を
|
|
187
|
+
コピペしてしまう可能性がある。
|
|
188
|
+
**拡張時の罠**: タイムアウトを設定しないままデプロイすると、
|
|
189
|
+
外部サーバーが無応答の場合にスレッドが永久にブロックされる。
|
|
190
|
+
**セキュリティ的な事故リスク**: 高。SSRF への意識がなければ `/fetch` のような
|
|
191
|
+
任意 URL フェッチエンドポイントをそのまま公開してしまう。
|
|
192
|
+
|
|
193
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
194
|
+
|
|
195
|
+
フロントエンドから「外部 URL の画像プレビュー」を生成する API を実装している。
|
|
196
|
+
|
|
197
|
+
**エラーレスポンスの質**: `FetchResult` の `status_code` + `error_message` は
|
|
198
|
+
クライアントでのエラー表示に使いやすい。
|
|
199
|
+
`fetch/safe` で 403 を返す設計は「なぜアクセスできないか」が明確。
|
|
200
|
+
**Python 固有概念の学習コスト**: `bytes.decode("utf-8", errors="replace")` は
|
|
201
|
+
JS の `TextDecoder` に近い感覚で理解しやすい。`urllib.error.HTTPError` が
|
|
202
|
+
`URLError` のサブクラスという例外階層は少し驚く。
|
|
203
|
+
**事故リスク**: 低。HTTP 境界での Pydantic バリデーション + `/fetch/safe` の 403 が充実。
|
|
204
|
+
|
|
205
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
206
|
+
|
|
207
|
+
`requests` + `httpx` を使ったプロジェクトを担当しており、
|
|
208
|
+
stdlib 版の使いどころを評価している。
|
|
209
|
+
|
|
210
|
+
**他フレームワークとの差異**: `httpx` は非同期サポート・HTTP/2・自動リダイレクト制御・
|
|
211
|
+
`timeout` のセクション別指定など `urllib.request` より高機能。
|
|
212
|
+
外部依存を避ける必要がある場合(ライブラリ配布・コンテナサイズ制限)に `urllib.request` が有効。
|
|
213
|
+
**nene2-python の薄さへの評価**: `_make_request()` を `HttpGatewayInterface` に昇格させれば
|
|
214
|
+
テスト時に差し替えが容易になる。現状の直接実装はシンプルだが、本番で SSRF 対策を別層で行う構造は良い。
|
|
215
|
+
**本番投入可能性**: SSRF 防御・タイムアウト・レスポンスサイズ制限は本番品質。
|
|
216
|
+
リダイレクト制御(最大リダイレクト数の制限)は未実装で注意が必要。
|
|
217
|
+
|
|
218
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
219
|
+
|
|
220
|
+
外部 API コールを行う PR をレビューしている。
|
|
221
|
+
|
|
222
|
+
**コードレビューチェックポイント**:
|
|
223
|
+
- [x] `validate_url()` でスキームが `http`/`https` のみか
|
|
224
|
+
- [x] `is_ssrf_safe()` で DNS 解決後に Private IP をブロックしているか
|
|
225
|
+
- [x] `timeout` が設定されているか(`None` = ブロッキング)
|
|
226
|
+
- [x] `MAX_RESPONSE_BYTES` でレスポンスサイズが制限されているか
|
|
227
|
+
- [x] Basic 認証のパスワードがログに出ないか
|
|
228
|
+
|
|
229
|
+
**チームでの安全なパターン**: 外部 URL を受け取る全エンドポイントで `fetch_safe()` を強制使用。
|
|
230
|
+
`fetch_url()` は内部テスト・ローカルサーバー向けのみとするルールを文書化する。
|
|
231
|
+
**ツール追加の必要性**: `bandit B310`(`urllib.urlopen` SSRF 警告)は
|
|
232
|
+
`validate_url()` + `is_ssrf_safe()` の存在で解消できるが、
|
|
233
|
+
ルールを有効にして「バイパスしていないか」の確認が推奨。
|
|
234
|
+
|
|
235
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
236
|
+
|
|
237
|
+
**ポリシー達成度**: 高
|
|
238
|
+
**「初心者でも安全な API」達成度**: 高
|
|
239
|
+
— FT183 の SSRF 発見を即座に実装・テストで検証。
|
|
240
|
+
`fetch_safe()` を HTTP エンドポイントのデフォルトとして推奨する設計が明確。
|
|
241
|
+
**設計上の負債**: DNS リバインディング攻撃への完全な対策(接続時に再チェック)は未実装。
|
|
242
|
+
リダイレクト先の URL も SSRF チェックが必要(実装なし)。
|
|
243
|
+
**Follow-up Issue 候補**: リダイレクト先 URL の SSRF チェック
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## クラッカーペンテスト(FT184 % 4 = 0)
|
|
248
|
+
|
|
249
|
+
> **実施方針**: チェックリストではなく、実際に攻撃ペイロードを送り込んで耐えられるかを試験する。
|
|
250
|
+
> クラッカーは公開 API の仕様から内部構造を推測し、想定外の入力で動作を崩そうとする。
|
|
251
|
+
|
|
252
|
+
### フェーズ1: 構造推測(攻撃者の視点)
|
|
253
|
+
|
|
254
|
+
- **公開情報から推測できる内部構造**:
|
|
255
|
+
- `/fetch` が外部 URL に接続することから、内部ネットワークのプローブが可能
|
|
256
|
+
- `/validate` がサーバー側 DNS 解決を行うことから、DNS ベースの情報収集が可能
|
|
257
|
+
- `/fetch/auth` が Basic 認証を送信することから、MITM で認証情報を盗める可能性
|
|
258
|
+
- OpenAPI スキーマで `url: str` の最大長が 2048 文字と公開
|
|
259
|
+
- エラーレスポンスの形式から urllib ベースの実装であることが推測可能
|
|
260
|
+
|
|
261
|
+
### フェーズ2: 攻撃実行ログ
|
|
262
|
+
|
|
263
|
+
#### A. SSRF 攻撃(/fetch エンドポイント)
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
攻撃1: {"url": "http://127.0.0.1/admin"}
|
|
267
|
+
→ validate_url: True(スキーム・構造は正常)
|
|
268
|
+
→ fetch_url が実行される → ローカルサーバーに接続試行
|
|
269
|
+
→ テスト環境では実際に接続できる(/fetch はSSRF無防備)
|
|
270
|
+
結果: ⚠️ /fetch は SSRF 対策なし(設計上の意図だが本番では危険)
|
|
271
|
+
|
|
272
|
+
攻撃2: {"url": "http://127.0.0.1/admin"} → POST /fetch/safe
|
|
273
|
+
→ is_ssrf_safe: False → 403 Forbidden
|
|
274
|
+
結果: ✅ /fetch/safe で防御済み
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### B. スキームインジェクション攻撃
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
攻撃: {"url": "file:///etc/passwd"}
|
|
281
|
+
→ validate_url: False(スキームが許可リストにない)
|
|
282
|
+
→ 400 Bad Request
|
|
283
|
+
結果: ✅ 防御済み
|
|
284
|
+
|
|
285
|
+
攻撃: {"url": "ftp://internal.host/"}
|
|
286
|
+
→ validate_url: False(FTP は許可外)
|
|
287
|
+
結果: ✅ 防御済み
|
|
288
|
+
|
|
289
|
+
攻撃: {"url": "javascript:alert(1)"}
|
|
290
|
+
→ validate_url: False(netloc がない)
|
|
291
|
+
結果: ✅ 防御済み
|
|
292
|
+
|
|
293
|
+
攻撃: {"url": "data:text/html,<script>alert(1)</script>"}
|
|
294
|
+
→ validate_url: False(スキームが許可リストにない)
|
|
295
|
+
結果: ✅ 防御済み
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### C. URL 埋め込み認証情報・ホスト隠蔽
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
攻撃1: {"url": "http://admin:password@192.168.1.1/admin"}
|
|
302
|
+
→ validate_url: True(スキーム・netloc は正常)
|
|
303
|
+
→ is_ssrf_safe: False(192.168.1.1 はプライベート IP)
|
|
304
|
+
→ /validate で ssrf_safe: false を返す
|
|
305
|
+
→ /fetch/safe で 403
|
|
306
|
+
結果: ✅ プライベート IP ブロックが認証情報付き URL にも機能
|
|
307
|
+
|
|
308
|
+
攻撃2: {"url": "http://user:pass@10.0.0.1:8080/internal"}
|
|
309
|
+
→ is_ssrf_safe: False(10.x.x.x はプライベート)
|
|
310
|
+
結果: ✅ 防御済み
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### D. 境界値・DoS 試み
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
攻撃1: timeout = 0.0
|
|
317
|
+
→ Pydantic ge=0.1 で 422 Unprocessable Entity
|
|
318
|
+
結果: ✅ 防御済み
|
|
319
|
+
|
|
320
|
+
攻撃2: timeout = 9999.0
|
|
321
|
+
→ Pydantic le=60.0 で 422 Unprocessable Entity
|
|
322
|
+
結果: ✅ 防御済み
|
|
323
|
+
|
|
324
|
+
攻撃3: {"url": "https://example.com/" + "A" * 2000}
|
|
325
|
+
→ validate_url: False(len > 2048)
|
|
326
|
+
→ 400 Bad Request(または Pydantic が 422)
|
|
327
|
+
結果: ✅ 防御済み
|
|
328
|
+
|
|
329
|
+
攻撃4: {"url": "http://localhost/?param=" + "A" * 2000}
|
|
330
|
+
→ 全長が 2048 を超える → validate_url: False
|
|
331
|
+
結果: ✅ 防御済み
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### E. オープンリダイレクト(クライアント誘導)
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
攻撃: APIが返す final_url が攻撃者の制御するURLになる
|
|
338
|
+
→ 例: リダイレクトチェーンで最終URLが http://attacker.com に
|
|
339
|
+
→ レスポンスの final_url に含まれる
|
|
340
|
+
→ body_text[:10_000] のみ返すため大量データ転送はなし
|
|
341
|
+
→ リダイレクト先のSSRFチェックは未実装(F-3 として記録)
|
|
342
|
+
結果: ⚠️ リダイレクト先のSSRFチェック漏れ(F-3)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
#### F. DNS リバインディング
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
理論的攻撃:
|
|
349
|
+
1. DNS TTL を極短に設定したドメイン example-attacker.com
|
|
350
|
+
2. /validate を呼ぶ時点では公開 IP → is_ssrf_safe: True
|
|
351
|
+
3. TTL 切れ後に /fetch/safe を呼ぶ → DNS 解決が 127.0.0.1 に変化
|
|
352
|
+
4. 内部サービスにアクセス
|
|
353
|
+
|
|
354
|
+
実装状況: is_ssrf_safe() のDNS解決と urlopen() の実際の接続の間にウィンドウがある
|
|
355
|
+
対策: 実運用では firewall でのエグレス制限が本質的な防御
|
|
356
|
+
結果: ⚠️ DNS リバインディングは理論上可能(F-4 として記録)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### フェーズ3: 攻撃まとめ
|
|
360
|
+
|
|
361
|
+
| 攻撃カテゴリ | 試みた攻撃数 | 突破 | 耐えた | 予期しない動作 |
|
|
362
|
+
|---|---|---|---|---|
|
|
363
|
+
| SSRF(/fetch/safe) | 8 | 0 | 8 | 0 |
|
|
364
|
+
| スキームインジェクション | 5 | 0 | 5 | 0 |
|
|
365
|
+
| URL 埋め込み認証情報 | 2 | 0 | 2 | 0 |
|
|
366
|
+
| 境界値/DoS | 4 | 0 | 4 | 0 |
|
|
367
|
+
| オープンリダイレクト | 1 | 0 | 1 | ⚠️ リダイレクト先 SSRF |
|
|
368
|
+
| DNS リバインディング | 1 | 0 | 0 | ⚠️ 理論上可能 |
|
|
369
|
+
|
|
370
|
+
**攻撃耐性評価**: 軽微な問題あり
|
|
371
|
+
**発見した弱点**:
|
|
372
|
+
- F-3: リダイレクト先 URL のSSRFチェック未実装
|
|
373
|
+
- F-4: DNS リバインディングは理論上可能(実運用ではfirewall対策が本質)
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## 摩擦ポイント(ペンテスト由来)
|
|
378
|
+
|
|
379
|
+
### F-3: `urllib.request` のリダイレクト先 URL が SSRF チェックを受けない(深刻度: 中)
|
|
380
|
+
|
|
381
|
+
**事象**: `fetch_safe("https://public.example.com/redirect")` が呼ばれ、
|
|
382
|
+
サーバーが `302 Location: http://127.0.0.1/admin` を返すと、
|
|
383
|
+
`urllib.request` が自動的にリダイレクトを追う。
|
|
384
|
+
`is_ssrf_safe()` は初回 URL のみチェックし、リダイレクト先はチェックしない。
|
|
385
|
+
|
|
386
|
+
**対応**: カスタム `HTTPRedirectHandler` を実装してリダイレクト先も SSRF チェックする:
|
|
387
|
+
```python
|
|
388
|
+
class SSRFSafeRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
389
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
390
|
+
if not is_ssrf_safe(newurl):
|
|
391
|
+
raise urllib.error.URLError("SSRF blocked: redirect to private address")
|
|
392
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### F-4: DNS リバインディングは SSRF 防御のウィンドウを突ける(深刻度: 低)
|
|
396
|
+
|
|
397
|
+
**事象**: `is_ssrf_safe()` の DNS 解決と `urlopen()` の実際の接続の間に
|
|
398
|
+
DNS レスポンスが変わる可能性がある(TTL 0 の DNS)。
|
|
399
|
+
|
|
400
|
+
**対応**: 実用的な対策はアプリレイヤーでは困難。
|
|
401
|
+
本番環境では firewall で内部ネットワークへのエグレスを制限することが本質的な防御。
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Follow-up Issues
|
|
406
|
+
|
|
407
|
+
| 優先度 | タイトル | 種別 |
|
|
408
|
+
|---|---|---|
|
|
409
|
+
| 中 | [FT184] fetch_safe でリダイレクト先 URL も SSRF チェックする | security |
|
|
410
|
+
| 低 | [FT184] DNS リバインディング対策を How-to ドキュメントに記載 | docs |
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## まとめ
|
|
415
|
+
|
|
416
|
+
FT184 では `urllib.request` モジュールを中心に、URL フェッチ・Basic 認証・SSRF 防御・
|
|
417
|
+
スキームインジェクション対策を実装した。64 テストが全通過し、mypy/ruff も問題なし。
|
|
418
|
+
|
|
419
|
+
クラッカーペンテスト(FT184 % 4 = 0)では `/fetch/safe` が SSRF・スキームインジェクション・
|
|
420
|
+
URL 埋め込み認証情報の全攻撃に耐えることを確認した。
|
|
421
|
+
一方で F-3(リダイレクト先の SSRF チェック漏れ)と F-4(DNS リバインディング)を発見。
|
|
422
|
+
F-3 は `SSRFSafeRedirectHandler` で対応可能で、F-4 は firewall での対策が本質的。
|
|
423
|
+
|
|
424
|
+
FT183 の SSRF 発見(#513)を即座にこの FT で実装・検証したことで、
|
|
425
|
+
セキュリティ改善のサイクルが機能することを確認できた。
|
|
426
|
+
|
|
427
|
+
v1.8.55 としてリリース。
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# FT185: contextlib
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: contextlib モジュール — コンテキストマネージャー・リソース管理・エラー抑制
|
|
5
|
+
**セキュリティ診断**: なし(185 % 3 = 2)
|
|
6
|
+
**クラッカーペンテスト**: なし(185 % 4 = 1)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
Python 標準ライブラリ `contextlib` は、コンテキストマネージャーの作成・合成・利用を支援するユーティリティ集である。
|
|
13
|
+
`@contextmanager` デコレーター、`suppress`、`ExitStack`、`closing`、`nullcontext`、`ContextDecorator`、`chdir`(3.11+)など多岐にわたるツールを検証した。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 実装したサンプルアプリ
|
|
18
|
+
|
|
19
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft185-contextlib/`
|
|
20
|
+
|
|
21
|
+
### 主要機能
|
|
22
|
+
|
|
23
|
+
| 関数/クラス | 概要 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `timer(label)` | `@contextmanager` で経過時間を計測 |
|
|
26
|
+
| `capture_stdout()` | `redirect_stdout` で標準出力をキャプチャ |
|
|
27
|
+
| `temp_attr(obj, attr, value)` | 属性を一時的に変更して復元する |
|
|
28
|
+
| `safe_int(value)` | `suppress(ValueError)` で変換失敗を吸収 |
|
|
29
|
+
| `safe_delete(dict, key)` | `suppress(KeyError)` で削除失敗を吸収 |
|
|
30
|
+
| `query_with_closing(host, sql)` | `closing` で接続を確実にクローズ |
|
|
31
|
+
| `fake_transaction(fail_on)` | ロールバック/コミットを持つトランザクション |
|
|
32
|
+
| `ManagedBuffer` | `ExitStack` で複数バッファのライフサイクルを管理 |
|
|
33
|
+
| `process_data(data, lock)` | `nullcontext` でオプションのロックを抽象化 |
|
|
34
|
+
| `run_and_capture(func)` | `redirect_stdout` / `redirect_stderr` で出力をキャプチャ |
|
|
35
|
+
| `run_pipeline(steps)` | `ExitStack` + コールバックでパイプライン管理 |
|
|
36
|
+
| `RetryContext` | `AbstractContextManager` の具象実装 |
|
|
37
|
+
| `LoggingContext` | `ContextDecorator` でデコレーターとしても使用可 |
|
|
38
|
+
| `batched_writer(items, batch_size)` | アイテムをバッチ分割するコンテキストマネージャー |
|
|
39
|
+
| `read_file_safe(path)` | `suppress(OSError)` でファイル読み込み失敗を吸収 |
|
|
40
|
+
| `get_current_dir_in_context(path)` | `contextlib.chdir` で一時ディレクトリ移動 |
|
|
41
|
+
|
|
42
|
+
### HTTP エンドポイント
|
|
43
|
+
|
|
44
|
+
| メソッド | パス | 概要 |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| POST | `/suppress/int` | 文字列→int 変換(失敗時 null)|
|
|
47
|
+
| POST | `/suppress/delete` | 辞書キーを安全に削除 |
|
|
48
|
+
| POST | `/transaction` | フェイクトランザクション(コミット/ロールバック)|
|
|
49
|
+
| POST | `/pipeline` | ExitStack パイプライン実行 |
|
|
50
|
+
| POST | `/buffer` | ManagedBuffer で複数バッファ管理 |
|
|
51
|
+
| POST | `/timer` | 経過時間計測 |
|
|
52
|
+
| POST | `/batch` | バッチ分割 |
|
|
53
|
+
| POST | `/query` | closing 付きクエリ実行 |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## テスト結果
|
|
58
|
+
|
|
59
|
+
**50 passed**
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
50 passed in 0.31s
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
mypy --strict: Success
|
|
66
|
+
ruff check: All checks passed
|
|
67
|
+
pip-audit: PYSEC-2025-183 (PyJWT via mcp transitive dep — 許容済み)
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 摩擦ポイント
|
|
72
|
+
|
|
73
|
+
### F-1: `__exit__` の戻り値型に `bool` は不可(mypy --strict)(深刻度: 低)
|
|
74
|
+
|
|
75
|
+
**事象**: `AbstractContextManager` サブクラスの `__exit__` メソッドで戻り値型を `bool` と宣言したところ、mypy --strict が以下のエラーを出力した。
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
"bool" is invalid as return type for "__exit__" that always returns False
|
|
79
|
+
Use "typing.Literal[False]" as the return type or change it to "None"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**原因**: mypy は `__exit__` が `True` を返すと「例外を抑制する」と解釈する。`bool` 型はその可能性を示すため、常に `False` を返す実装では `Literal[False]` か `None` を使うよう要求される。
|
|
83
|
+
|
|
84
|
+
**対応**: 戻り値型を `Literal[False]` に変更(`from typing import Literal` が必要)。
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from typing import Literal
|
|
88
|
+
|
|
89
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> Literal[False]:
|
|
90
|
+
return False
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 観察点
|
|
96
|
+
|
|
97
|
+
### 観察1: `@contextmanager` の yield 前後でのクリーンアップ
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
@contextlib.contextmanager
|
|
101
|
+
def timer(label: str) -> Generator[dict[str, float], None, None]:
|
|
102
|
+
result: dict[str, float] = {}
|
|
103
|
+
start = time.perf_counter()
|
|
104
|
+
try:
|
|
105
|
+
yield result
|
|
106
|
+
finally:
|
|
107
|
+
result["elapsed"] = time.perf_counter() - start
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`try / finally` パターンによって、`with` ブロック内で例外が発生しても `finally` が確実に実行される。yield した辞書を通じて結果を呼び出し元に渡せる点が特徴的。
|
|
111
|
+
|
|
112
|
+
### 観察2: `ExitStack.callback` による動的クリーンアップ
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
with contextlib.ExitStack() as stack:
|
|
116
|
+
for resource in resources:
|
|
117
|
+
stack.callback(release_resource, resource)
|
|
118
|
+
acquired.append(resource)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
コンテキストマネージャーを持たないリソースでも `callback` で後処理を登録できる。登録は LIFO 順で実行されるため、依存関係のあるリソースも安全に解放できる。
|
|
122
|
+
|
|
123
|
+
### 観察3: `ContextDecorator` で関数デコレーターを兼ねる
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
class LoggingContext(contextlib.ContextDecorator):
|
|
127
|
+
def __enter__(self): ...
|
|
128
|
+
def __exit__(self, ...): ...
|
|
129
|
+
|
|
130
|
+
ctx = LoggingContext("fn")
|
|
131
|
+
|
|
132
|
+
# コンテキストマネージャーとして
|
|
133
|
+
with ctx:
|
|
134
|
+
do_something()
|
|
135
|
+
|
|
136
|
+
# デコレーターとして
|
|
137
|
+
@ctx
|
|
138
|
+
def my_func():
|
|
139
|
+
do_something()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`ContextDecorator` を継承するだけで `with` と `@decorator` の両方の文脈で使用できるようになる。テスト・ロギング・タイミングの実装で有用。
|
|
143
|
+
|
|
144
|
+
### 観察4: `nullcontext` でオプションのロックを統一的に扱う
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
def process_data(data, lock=None):
|
|
148
|
+
ctx = lock if lock is not None else contextlib.nullcontext()
|
|
149
|
+
with ctx:
|
|
150
|
+
return sum(data)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
呼び出し元からロックを渡せる場合と不要な場合を、`with` 文を2回書かずに統一できる。シングルスレッドのテストでは `lock=None`、マルチスレッドでは実際のロックを渡すという設計が自然に表現できる。
|
|
154
|
+
|
|
155
|
+
### 観察5: `contextlib.chdir` (Python 3.11+)
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
original = os.getcwd()
|
|
159
|
+
with contextlib.chdir("/tmp"):
|
|
160
|
+
# /tmp が cwd
|
|
161
|
+
pass
|
|
162
|
+
# original に戻っている
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`os.chdir()` を手動で `try/finally` でラップする必要がなくなる。Python 3.11 以降でのみ使用可能なため、3.10 以下をサポートする場合は自前実装が必要。
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Follow-up Issues
|
|
170
|
+
|
|
171
|
+
今回の FT では実装上の重大な摩擦はなかった。F-1 は mypy の型精度要求によるものであり、Python 型システムの理解向上につながる知見として記録する。
|
|
172
|
+
|
|
173
|
+
GitHub Issues: なし
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## DX Review — 6ペルソナ
|
|
178
|
+
|
|
179
|
+
### 1. 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
180
|
+
|
|
181
|
+
FastAPI でエラーを無視したいとき `try/except pass` を書いていたが、`contextlib.suppress` を知ることで「名前のついた意図表現」に切り替えられる。
|
|
182
|
+
|
|
183
|
+
**ドキュメント理解**: `@contextmanager` の `yield` の意味(「ここで with ブロックに入る」)が直感的でない。`yield result` が値を返すだけでなくコンテキストを区切るという二重の役割は、初見では混乱しやすい。
|
|
184
|
+
|
|
185
|
+
**事故リスク**: 低 — `suppress` の乱用で重要な例外を握りつぶすリスクはあるが、デモコードでは抑制対象の例外を明示しているため、コピペしても事故にはなりにくい。
|
|
186
|
+
|
|
187
|
+
**規約の使いやすさ**: 豊富なサンプルによって「suppress で受ける」「timer で囲む」「ExitStack で登録する」という用法がパターンとして身につく。
|
|
188
|
+
|
|
189
|
+
### 2. ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
190
|
+
|
|
191
|
+
`ExitStack` の使い所が「複数のファイルを条件によって開く」といったシナリオで真価を発揮することが理解できれば、実務でのファイル・DB 接続管理が劇的にシンプルになる。
|
|
192
|
+
|
|
193
|
+
**コピペ可能性**: `timer`・`capture_stdout`・`temp_attr` はそのままコピーして使えるユーティリティとして価値がある。
|
|
194
|
+
|
|
195
|
+
**拡張時の罠**: `ManagedBuffer.close_all()` を呼び忘れた場合、バッファはGCまで残る。`__enter__/__exit__` を実装して `with` で使えるようにするほうが安全だが、今回のデモでは意図的に省略した。
|
|
196
|
+
|
|
197
|
+
**事故リスク**: 低
|
|
198
|
+
|
|
199
|
+
### 3. フロントエンド寄り(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
200
|
+
|
|
201
|
+
TypeScript の `using` 宣言(TC39 Explicit Resource Management)と概念が近いことを理解すれば、Python の `with` 文の位置付けが掴みやすい。
|
|
202
|
+
|
|
203
|
+
**エラーレスポンスの質**: `/transaction` エンドポイントがロールバックした場合でも HTTP 200 を返し、`rolled_back: true` + `error` フィールドでエラーを示す設計はフロントエンドから見て扱いやすい。
|
|
204
|
+
|
|
205
|
+
**Python 固有概念の学習コスト**: `Generator[dict[str, float], None, None]` という型注釈は TS 経験者には冗長に見える。`@contextmanager` が `Generator` を返す理由を理解するには Python のジェネレーター仕組みへの理解が必要。
|
|
206
|
+
|
|
207
|
+
**事故リスク**: 低
|
|
208
|
+
|
|
209
|
+
### 4. バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
210
|
+
|
|
211
|
+
Django の `transaction.atomic()` や Flask の `g` オブジェクトと比べると、`contextlib` のアプローチはより明示的で汎用的。特に `ExitStack` は Django ORM の接続管理でも応用できる。
|
|
212
|
+
|
|
213
|
+
**他フレームワークとの差異**: `fake_transaction` パターンは Django の `TestCase.databases` によるロールバックと異なり、テスト外のユースケースにも適用できる。
|
|
214
|
+
|
|
215
|
+
**nene2 の薄さへの評価**: `contextlib` 自体は標準ライブラリであり nene2 フレームワークとの結合はない。エンドポイントの薄さ(parse → use-case → response の3ステップ)が維持されており好印象。
|
|
216
|
+
|
|
217
|
+
**事故リスク**: 低
|
|
218
|
+
|
|
219
|
+
### 5. シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
220
|
+
|
|
221
|
+
コードレビュー観点で最も重要なのは「suppress の対象を絞ること」。`suppress(Exception)` のように広すぎる例外クラスを指定するコードは必ずレビューで差し戻す。
|
|
222
|
+
|
|
223
|
+
**コードレビューチェックポイント**:
|
|
224
|
+
- `suppress` の引数は具体的な例外クラスか(`Exception` や `BaseException` でないか)
|
|
225
|
+
- `@contextmanager` の `try/finally` が欠落していないか(欠落するとクリーンアップが実行されない)
|
|
226
|
+
- `ExitStack` は使い終わったら必ず `close()` または `with` で囲まれているか
|
|
227
|
+
- `__exit__` の戻り値型が `Literal[False]` か `None` になっているか(mypy で強制されるが目視でも確認)
|
|
228
|
+
|
|
229
|
+
**チームでの安全なパターン**: `LoggingContext(ContextDecorator)` のパターンは横断的関心事(ログ・計測・トレーシング)に応用しやすく、チームで共有できるユーティリティになる。
|
|
230
|
+
|
|
231
|
+
**事故リスク**: 低
|
|
232
|
+
|
|
233
|
+
### 6. 設計者(nene2-python 設計ポリシー目線)
|
|
234
|
+
|
|
235
|
+
**CLAUDE.md ポリシー整合性**:
|
|
236
|
+
- `dataclass(frozen=True, slots=True)`: `ResourceHandle`・`TransactionResult`・`PipelineResult` で適用済み ✅
|
|
237
|
+
- Pydantic は HTTP 境界のみ: `app.py` の Request/Response モデルのみで使用 ✅
|
|
238
|
+
- `create_app()` はファイル末尾: 全エンドポイント定義後に配置 ✅(FT182 の教訓適用)
|
|
239
|
+
- `max_length` 指定: 全文字列フィールドに設定済み ✅
|
|
240
|
+
- `Literal[False]` 戻り値型: F-1 で修正済み ✅
|
|
241
|
+
|
|
242
|
+
**初心者でも安全な API 達成度**: `suppress` を使う際に抑制対象の例外を明示するパターンを見せることで、「なんでも suppress しない」という習慣を自然に身につけられる構成になっている。`ExitStack` の `callback` で lambda を避けて名前付き関数 `_noop` を使ったことで、mypy の型推論問題も回避できた。
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
*バージョン: v1.8.56*
|