nene2-python 1.8.54__tar.gz → 1.8.55__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.55}/PKG-INFO +1 -1
- nene2_python-1.8.55/docs/field-trials/2026-05-field-trial-184.md +427 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/INDEX.md +3 -2
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/todo/current.md +8 -6
- {nene2_python-1.8.54 → nene2_python-1.8.55}/pyproject.toml +1 -1
- {nene2_python-1.8.54 → nene2_python-1.8.55}/.env.example +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/.gitignore +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/AGENTS.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/CHANGELOG.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/CLAUDE.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/Dockerfile +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/LICENSE +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/README.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/alembic/README +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/alembic/env.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/alembic.ini +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/compose.yaml +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/de/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-168.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-169.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-170.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-171.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-172.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-173.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-174.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-175.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-176.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-177.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-178.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-179.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-180.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-181.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-182.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-183.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/fr/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/reference/api.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/roadmap.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/zh/index.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/package-lock.json +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/package.json +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/__main__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/app.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/mcp.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/schema.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/conftest.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/tests/scripts/test_export_openapi.py +0 -0
- {nene2_python-1.8.54 → nene2_python-1.8.55}/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.55
|
|
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 としてリリース。
|
|
@@ -217,6 +217,7 @@
|
|
|
217
217
|
| [FT181](2026-05-field-trial-181.md) | gzip モジュール — 圧縮・解凍・メタデータ・ビルド再現性 | | |
|
|
218
218
|
| [FT182](2026-05-field-trial-182.md) | email モジュール — MIME 構築・RFC 2047・パース・アドレス操作 | | |
|
|
219
219
|
| [FT183](2026-05-field-trial-183.md) | smtplib モジュール — SMTP 送信・STARTTLS・ヘッダーインジェクション防御 | 🔒 | [#513](https://github.com/hideyukiMORI/nene2-python/issues/513) [#514](https://github.com/hideyukiMORI/nene2-python/issues/514) |
|
|
220
|
+
| [FT184](2026-05-field-trial-184.md) | urllib.request モジュール — URL フェッチ・Basic 認証・SSRF 防御 | 🔍 | [#516](https://github.com/hideyukiMORI/nene2-python/issues/516) [#517](https://github.com/hideyukiMORI/nene2-python/issues/517) |
|
|
220
221
|
|
|
221
222
|
---
|
|
222
223
|
|
|
@@ -228,8 +229,8 @@ FT3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 6
|
|
|
228
229
|
|
|
229
230
|
## クラッカーペンテスト実施済み一覧(🔍)
|
|
230
231
|
|
|
231
|
-
FT172, FT176, FT180
|
|
232
|
+
FT172, FT176, FT180, FT184
|
|
232
233
|
|
|
233
234
|
---
|
|
234
235
|
|
|
235
|
-
*最終更新: 2026-05-21 (
|
|
236
|
+
*最終更新: 2026-05-21 (FT184 / v1.8.55)*
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# TODO — current
|
|
2
2
|
|
|
3
3
|
最終更新: 2026-05-21
|
|
4
|
-
現状: **v1.8.
|
|
4
|
+
現状: **v1.8.55 安定版 / フィールドトライアルループ継続中(FT184 完了)**
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## 状態サマリー
|
|
9
9
|
|
|
10
|
-
v1.8.
|
|
11
|
-
フィールドトライアルループは
|
|
10
|
+
v1.8.55 完了済み。FT184(urllib.request / URL フェッチ・Basic 認証・SSRF 防御)を含む FT184 件を実施済み。
|
|
11
|
+
フィールドトライアルループは FT185 以降も継続中。
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -22,6 +22,8 @@ v1.8.54 完了済み。FT183(smtplib / SMTP 送信・STARTTLS・ヘッダー
|
|
|
22
22
|
|
|
23
23
|
| Issue | 内容 | 優先度 |
|
|
24
24
|
|---|---|---|
|
|
25
|
+
| [#517](https://github.com/hideyukiMORI/nene2-python/issues/517) | [FT184] DNS リバインディング攻撃への対策検討(TTL0 + IP 切り替え) | 低 |
|
|
26
|
+
| [#516](https://github.com/hideyukiMORI/nene2-python/issues/516) | [FT184] fetch_safe のリダイレクト SSRF 対策(Location ヘッダー先の IP 検証) | 中 |
|
|
25
27
|
| [#514](https://github.com/hideyukiMORI/nene2-python/issues/514) | [FT183] SmtpConfig.password を SecretStr に変更 | 低 |
|
|
26
28
|
| [#513](https://github.com/hideyukiMORI/nene2-python/issues/513) | [FT183] /send・/check-server の SSRF 対策(Private IP ブロック) | 中 |
|
|
27
29
|
| [#511](https://github.com/hideyukiMORI/nene2-python/issues/511) | [FT182] parseaddr() の寛容な挙動を How-to ドキュメントに記載 | 低 |
|
|
@@ -38,6 +40,7 @@ v1.8.54 完了済み。FT183(smtplib / SMTP 送信・STARTTLS・ヘッダー
|
|
|
38
40
|
|
|
39
41
|
| バージョン | 主な内容 |
|
|
40
42
|
|---|---|
|
|
43
|
+
| v1.8.55 | FT184: urllib.request — URL フェッチ・Basic 認証・SSRF 防御(クラッカーペンテスト実施) |
|
|
41
44
|
| v1.8.54 | FT183: smtplib — SMTP 送信・STARTTLS・ヘッダーインジェクション防御(診断実施) |
|
|
42
45
|
| v1.8.53 | FT182: email — MIME 構築・RFC 2047・パース・アドレス操作 |
|
|
43
46
|
| v1.8.52 | FT181: gzip — 圧縮・解凍・メタデータ手動解析・ビルド再現性 |
|
|
@@ -55,13 +58,12 @@ v1.8.54 完了済み。FT183(smtplib / SMTP 送信・STARTTLS・ヘッダー
|
|
|
55
58
|
|
|
56
59
|
## フィールドトライアル進捗
|
|
57
60
|
|
|
58
|
-
**実施済み**: FT1〜
|
|
61
|
+
**実施済み**: FT1〜FT184(全 184 件)
|
|
59
62
|
|
|
60
63
|
索引: [`docs/field-trials/INDEX.md`](../field-trials/INDEX.md)
|
|
61
64
|
|
|
62
65
|
**次のアクション**:
|
|
63
|
-
-
|
|
64
|
-
- FT184 は 184 % 3 = 1 → セキュリティ診断なし、ただしペンテストあり
|
|
66
|
+
- FT184 以降を継続(FT185 は 185 % 4 = 1 → ペンテストなし、185 % 3 = 2 → 診断なし)
|
|
65
67
|
|
|
66
68
|
---
|
|
67
69
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nene2_python-1.8.54 → nene2_python-1.8.55}/alembic/versions/001_create_notes_and_tags_tables.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|