nene2-python 1.8.48__tar.gz → 1.8.50__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.48 → nene2_python-1.8.50}/PKG-INFO +1 -1
- nene2_python-1.8.50/docs/field-trials/2026-05-field-trial-178.md +242 -0
- nene2_python-1.8.50/docs/field-trials/2026-05-field-trial-179.md +242 -0
- nene2_python-1.8.50/docs/field-trials/INDEX.md +231 -0
- nene2_python-1.8.50/docs/todo/current.md +65 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/pyproject.toml +2 -1
- nene2_python-1.8.48/docs/todo/current.md +0 -52
- {nene2_python-1.8.48 → nene2_python-1.8.50}/.env.example +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/.gitignore +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/AGENTS.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/CHANGELOG.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/CLAUDE.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/Dockerfile +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/LICENSE +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/README.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/alembic/README +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/alembic/env.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/alembic.ini +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/compose.yaml +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/de/index.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-168.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-169.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-170.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-171.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-172.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-173.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-174.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-175.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-176.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-177.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/fr/index.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/index.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/index.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/reference/api.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/roadmap.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/zh/index.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/package-lock.json +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/package.json +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/__main__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/app.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/mcp.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/schema.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/conftest.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/tests/scripts/test_export_openapi.py +0 -0
- {nene2_python-1.8.48 → nene2_python-1.8.50}/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.50
|
|
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,242 @@
|
|
|
1
|
+
# FT178: base64 モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: エンコード・デコード・URL セーフ変換・データ URI・HTTP Basic Auth パース
|
|
5
|
+
**セキュリティ診断**: なし(178 % 3 = 1)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
Web API で頻繁に使う `base64` モジュールを検証する。
|
|
12
|
+
標準エンコーディングと URL セーフ変換の違い、パディング扱い、データ URI 生成、
|
|
13
|
+
HTTP Basic Auth ヘッダーのパースまで網羅し、落とし穴となる箇所を洗い出す。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 実装したサンプルアプリ
|
|
18
|
+
|
|
19
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft178-base64/`
|
|
20
|
+
|
|
21
|
+
### 主要機能
|
|
22
|
+
|
|
23
|
+
| 関数 | 概要 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `encode_standard(data)` | 標準 base64 エンコード(RFC 4648 §4) |
|
|
26
|
+
| `decode_standard(s)` | 標準 base64 デコード(`validate=True` で厳格検証) |
|
|
27
|
+
| `encode_url_safe(data)` | URL セーフ base64(パディングなし) |
|
|
28
|
+
| `decode_url_safe(s)` | URL セーフ base64 デコード(パディング自動補完 + 文字セット検証) |
|
|
29
|
+
| `encode_text(text)` | UTF-8 テキスト → base64 |
|
|
30
|
+
| `decode_text(s)` | base64 → UTF-8 テキスト(非 UTF-8 は `None`) |
|
|
31
|
+
| `is_valid_base64(s)` | 長さ・文字セット・パディングを検証 |
|
|
32
|
+
| `is_valid_url_safe_base64(s)` | URL セーフ文字セット検証 |
|
|
33
|
+
| `make_data_uri(content, mime_type)` | RFC 2397 データ URI 生成 |
|
|
34
|
+
| `parse_data_uri(uri)` | データ URI → `(mime_type, bytes)` |
|
|
35
|
+
| `encode_basic_auth(username, password)` | HTTP Basic Auth ヘッダー値生成 |
|
|
36
|
+
| `parse_basic_auth(header)` | Authorization ヘッダー → `(username, password)` |
|
|
37
|
+
|
|
38
|
+
### HTTP エンドポイント
|
|
39
|
+
|
|
40
|
+
| メソッド | パス | 概要 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| POST | `/encode` | 標準 base64 エンコード |
|
|
43
|
+
| POST | `/decode` | 標準 base64 デコード |
|
|
44
|
+
| POST | `/encode/url-safe` | URL セーフ base64 エンコード |
|
|
45
|
+
| POST | `/decode/url-safe` | URL セーフ base64 デコード |
|
|
46
|
+
| POST | `/encode/text` | テキスト → base64 |
|
|
47
|
+
| POST | `/decode/text` | base64 → テキスト |
|
|
48
|
+
| POST | `/data-uri/encode` | データ URI 生成 |
|
|
49
|
+
| POST | `/data-uri/parse` | データ URI 解析 |
|
|
50
|
+
| POST | `/auth/basic/encode` | Basic Auth ヘッダー生成 |
|
|
51
|
+
| POST | `/auth/basic/parse` | Basic Auth ヘッダー解析 |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## テスト結果
|
|
56
|
+
|
|
57
|
+
**58 passed**(初回 1 失敗 → 修正後 58 全通過)
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
58 passed in 0.43s
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
mypy: Success / ruff: All checks passed / pip-audit: PYSEC-2025-183(継続監視)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 摩擦ポイント
|
|
68
|
+
|
|
69
|
+
### F-1: `urlsafe_b64decode` が不正文字をサイレントに無視する(深刻度: 高)
|
|
70
|
+
|
|
71
|
+
**事象**: `decode_url_safe("!!!invalid!!!")` が `None` を返さず `b"\x8a{\xda\x96'"` を返した。
|
|
72
|
+
|
|
73
|
+
**原因**: `base64.urlsafe_b64decode()` は RFC 4648 の「ignore non-alphabet characters」モードで動作し、
|
|
74
|
+
`!` などの不正文字を黙って無視してデコードを続ける。
|
|
75
|
+
一方、標準の `b64decode(s, validate=True)` は不正文字で `binascii.Error` を raise する。
|
|
76
|
+
|
|
77
|
+
**対応**: `urlsafe_b64decode` の前に文字セット正規表現で事前バリデーション:
|
|
78
|
+
```python
|
|
79
|
+
_URL_SAFE_CHARS_RE = re.compile(r"^[A-Za-z0-9_\-=]*$")
|
|
80
|
+
|
|
81
|
+
def decode_url_safe(s: str) -> bytes | None:
|
|
82
|
+
stripped = s.rstrip("=")
|
|
83
|
+
if not stripped or not _URL_SAFE_CHARS_RE.match(stripped):
|
|
84
|
+
return None
|
|
85
|
+
...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**ライブラリ設計上の問題**: `urlsafe_b64decode` に `validate=True` パラメータが存在しない(`b64decode` にはある)。
|
|
89
|
+
URL セーフ版は自前バリデーションが必要という非対称な API 設計。
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 観察点
|
|
94
|
+
|
|
95
|
+
### 観察1: 標準 base64 vs URL セーフ — `validate=True` の非対称性
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# 標準版: validate パラメータがある
|
|
99
|
+
base64.b64decode("not!!!valid", validate=True) # → binascii.Error
|
|
100
|
+
|
|
101
|
+
# URL セーフ版: validate パラメータがない
|
|
102
|
+
base64.urlsafe_b64decode("not!!!valid") # → サイレントに無視してデコード
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
JWT トークンや OAuth コードは URL セーフ base64 を使う。
|
|
106
|
+
`validate=True` に相当するガードを自前で実装しないと、
|
|
107
|
+
不正トークンを誤って「有効」として処理する脆弱性になりうる。
|
|
108
|
+
|
|
109
|
+
### 観察2: パディング補完の必要性
|
|
110
|
+
|
|
111
|
+
RFC 4648 §5 では URL セーフ base64 のパディング(`=`)は省略可能とされており、
|
|
112
|
+
JWT の `alg`・`typ` フィールドなど実際のトークンはパディングなしで渡ってくる。
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# パディングなし JWT ヘッダーを補完してデコード
|
|
116
|
+
stripped = s.rstrip("=")
|
|
117
|
+
padding = 4 - len(stripped) % 4
|
|
118
|
+
if padding != 4:
|
|
119
|
+
s = stripped + "=" * padding
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`padding != 4` の条件で「すでに 4 の倍数の場合はパディングを追加しない」ことも重要。
|
|
123
|
+
|
|
124
|
+
### 観察3: `partition(":")` で パスワード中のコロン対応
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
username, _, password = text.partition(":") # partition は最初の : で分割
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`split(":", 1)` でも同じだが `partition` の方が意図が明示的で、
|
|
131
|
+
`a:b:c:d` → `("a", ":", "b:c:d")` の分解が 1 行で書ける。
|
|
132
|
+
|
|
133
|
+
### 観察4: データ URI の MIME タイプ検証
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
re.match(r"^[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*$", mime_type)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`image/png`・`application/octet-stream`・`text/plain; charset=utf-8` のような MIME タイプは正規表現で検証。
|
|
140
|
+
`javascript:`・`vbscript:` などの XSS ペイロードをデータ URI に埋め込む試みをブロックできる。
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## nene2-python フレームワークとの統合
|
|
145
|
+
|
|
146
|
+
- `encode_basic_auth` / `parse_basic_auth` は `ApiKeyAuthMiddleware` の拡張として組み込み可能
|
|
147
|
+
- `make_data_uri` はファイルアップロード API のレスポンス形式として使える
|
|
148
|
+
- Pydantic `max_length` フィールドで全エンドポイントの DoS 対策済み
|
|
149
|
+
- `APIRouter` + `create_app()` パターン(FT177 摩擦 F-1 の対応)を最初から適用
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Developer Experience (DX) Review
|
|
154
|
+
|
|
155
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
156
|
+
|
|
157
|
+
画像アップロード API でクライアントから base64 エンコードされたデータを受け取る実装をしている。
|
|
158
|
+
|
|
159
|
+
**ドキュメント理解**: `b64encode` / `b64decode` のペアは分かりやすい。
|
|
160
|
+
URL セーフ版との違い(`+/` vs `-_`、パディング省略)はドキュメントに書いてないと気づきにくい。
|
|
161
|
+
**事故リスク**: 中。標準版と URL セーフ版を混在させると復号に失敗し、バイナリ化けとして現れる。
|
|
162
|
+
エラーより「壊れたデータ」として扱われるため気づきにくい。
|
|
163
|
+
**規約の使いやすさ**: `encode_standard(data)` が `str` を返し、`decode_standard(s)` が `bytes | None` を返すのは直感的。
|
|
164
|
+
|
|
165
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
166
|
+
|
|
167
|
+
JWT パースのコードを既存プロジェクトからコピーしており、URL セーフ base64 を使っている。
|
|
168
|
+
|
|
169
|
+
**コピペ可能性**: `decode_url_safe` の自前バリデーションが必要な点はコメントがないと気づかない。
|
|
170
|
+
`urlsafe_b64decode` を直接使うと F-1 の罠にはまる。
|
|
171
|
+
**拡張時の罠**: パディング補完のコードを「なんか動いてるから削ってもいいかな」と消すと壊れる。
|
|
172
|
+
なぜ必要かのコメントが欲しい。
|
|
173
|
+
**セキュリティ的な事故リスク**: 高。JWT 検証で `urlsafe_b64decode` を `validate=True` なしで使うと、
|
|
174
|
+
改ざんトークンが「デコード成功」として扱われる可能性がある。
|
|
175
|
+
|
|
176
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
177
|
+
|
|
178
|
+
TypeScript の `atob()` / `btoa()` に慣れており、Python で同じことをしようとしている。
|
|
179
|
+
|
|
180
|
+
**エラーレスポンスの質**: 400 Bad Request に具体的なメッセージ("Invalid base64 input" 等)が返るのは良い。
|
|
181
|
+
クライアント側でデバッグしやすい。
|
|
182
|
+
**Python 固有概念の学習コスト**: `bytes.hex()` / `bytes.fromhex()` の往復はTS では意識しない変換。
|
|
183
|
+
`atob()` は文字列を返すが `b64decode` は `bytes` を返す差異に戸惑う可能性。
|
|
184
|
+
**事故リスク**: 低。HTTP 境界での Pydantic バリデーションが充実。
|
|
185
|
+
|
|
186
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
187
|
+
|
|
188
|
+
JWT ライブラリの内部実装を理解しており、raw base64 操作をすることもある。
|
|
189
|
+
|
|
190
|
+
**他フレームワークとの差異**: Django の `base64.urlsafe_b64decode` 直接利用パターンと同じ罠(F-1)が
|
|
191
|
+
nene2-python でも起きる。フレームワーク側での救済ではなく実装者が知識として持つ必要がある。
|
|
192
|
+
**nene2-python の薄さへの評価**: base64 は stdlib を薄くラップするだけが適切。
|
|
193
|
+
`decode_url_safe` のバリデーション付きラッパーは価値あるユーティリティ。
|
|
194
|
+
**本番投入可能性**: Basic Auth のパースは `parse_basic_auth` 一本で安全に使える設計が好評価。
|
|
195
|
+
|
|
196
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
197
|
+
|
|
198
|
+
**コードレビューチェックポイント**:
|
|
199
|
+
- [x] `urlsafe_b64decode` を自前バリデーションなしで使っていないか(F-1 の罠)
|
|
200
|
+
- [x] パスワードが base64 エンコードのみで「保護されている」と勘違いしていないか
|
|
201
|
+
(base64 は暗号化ではなくエンコード)
|
|
202
|
+
- [x] Basic Auth をデコードして得たパスワードをそのまま平文比較していないか
|
|
203
|
+
(`hmac.compare_digest` が必要)
|
|
204
|
+
|
|
205
|
+
**チームでの安全な共有パターン**: `decode_url_safe` に自前バリデーションを含めたラッパーを
|
|
206
|
+
ユーティリティとして共有すると、チーム全員が安全に使える。
|
|
207
|
+
**ツール追加の必要性**: `ruff` に base64 固有のルールはなし。コードレビューで担保。
|
|
208
|
+
|
|
209
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
210
|
+
|
|
211
|
+
**ポリシー達成度**: 高
|
|
212
|
+
**「初心者でも安全な API」達成度**: 中
|
|
213
|
+
— F-1(`urlsafe_b64decode` の無バリデーション問題)は初心者が直接 `base64.urlsafe_b64decode` を使うと再発する。
|
|
214
|
+
`decode_url_safe` ラッパーを使う運用を周知する必要がある。
|
|
215
|
+
**設計上の負債**: `validate=True` が URL セーフ版に存在しないのは Python stdlib の設計問題。
|
|
216
|
+
ユーザー向けに警告コメントを `decode_url_safe` に追記する価値がある。
|
|
217
|
+
**Follow-up Issue 候補**: なし(現状の実装で十分)
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Follow-up Issues
|
|
222
|
+
|
|
223
|
+
今回の FT では実装上の重大な摩擦はなし(F-1 は実装内で解消済み)。
|
|
224
|
+
|
|
225
|
+
| 優先度 | タイトル | 種別 |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| 低 | `decode_url_safe` に「`urlsafe_b64decode` は validate=True がないため自前バリデーションが必要」コメントを追記 | docs |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## まとめ
|
|
232
|
+
|
|
233
|
+
FT178 では `base64` モジュールを中心に、Web API でよく使うエンコード操作を実装した。
|
|
234
|
+
58 テストが全通過し、mypy/ruff も問題なし。
|
|
235
|
+
|
|
236
|
+
最大の発見は F-1: `base64.urlsafe_b64decode` に `validate=True` がなくサイレントに不正入力を処理してしまう問題。
|
|
237
|
+
JWT・OAuth コード等を URL セーフ base64 で扱うコードが多い中、この挙動は高リスクな落とし穴。
|
|
238
|
+
文字セット正規表現による事前バリデーションで対応済み。
|
|
239
|
+
|
|
240
|
+
APIRouter パターン(FT177 F-1 からの改善)を最初から適用し、テストが一発で通過した。
|
|
241
|
+
|
|
242
|
+
v1.8.49 としてリリース。
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# FT179: zlib モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: データ圧縮・解凍・CRC32/Adler-32 整合性検証・展開爆弾対策
|
|
5
|
+
**セキュリティ診断**: なし(179 % 3 = 2)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
Python 標準ライブラリの `zlib` モジュールを検証する。
|
|
12
|
+
単純な圧縮・解凍にとどまらず、展開爆弾(decompression bomb)対策のストリーミング解凍、
|
|
13
|
+
CRC32 と Adler-32 の両チェックサムアルゴリズム、圧縮レベル 1〜9 の比較、
|
|
14
|
+
チャンク単位のストリーミング圧縮まで網羅し、Web API での実用的な使い方を検証する。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 実装したサンプルアプリ
|
|
19
|
+
|
|
20
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft179-zlib/`
|
|
21
|
+
|
|
22
|
+
### 主要機能
|
|
23
|
+
|
|
24
|
+
| 関数/クラス | 概要 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `compress(data, level)` | zlib 圧縮(入力 10MB 上限、`CompressResult` 返却) |
|
|
27
|
+
| `decompress(compressed_hex)` | hex 文字列から解凍(展開爆弾対策付き) |
|
|
28
|
+
| `_decompress_bytes(data)` | ストリーミング解凍コア(50MB 上限をチャンクごとに監視) |
|
|
29
|
+
| `decompress_streaming(data)` | raw bytes を受け取る解凍インターフェース |
|
|
30
|
+
| `compute_crc32(data)` | CRC32 チェックサム計算(`ChecksumResult` 返却) |
|
|
31
|
+
| `compute_adler32(data)` | Adler-32 チェックサム計算(`ChecksumResult` 返却) |
|
|
32
|
+
| `verify_crc32(data, expected_hex)` | CRC32 検証 |
|
|
33
|
+
| `verify_adler32(data, expected_hex)` | Adler-32 検証 |
|
|
34
|
+
| `compare_compression_levels(data)` | レベル 1〜9 の圧縮結果比較(`LevelComparison` リスト) |
|
|
35
|
+
| `compress_streaming(chunks, level)` | チャンクリストのストリーミング圧縮 |
|
|
36
|
+
|
|
37
|
+
### HTTP エンドポイント
|
|
38
|
+
|
|
39
|
+
| メソッド | パス | 概要 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| POST | `/compress` | データを zlib 圧縮(`level` 指定可) |
|
|
42
|
+
| POST | `/decompress` | zlib 圧縮データを解凍 |
|
|
43
|
+
| POST | `/checksum/crc32` | CRC32 チェックサム計算 |
|
|
44
|
+
| POST | `/checksum/adler32` | Adler-32 チェックサム計算 |
|
|
45
|
+
| POST | `/verify` | チェックサム検証(`algorithm` で切り替え) |
|
|
46
|
+
| POST | `/compress/levels` | レベル 1〜9 の圧縮比較 |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## テスト結果
|
|
51
|
+
|
|
52
|
+
**39 passed**(初回から全通過)
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
39 passed in 0.67s
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
mypy: Success / ruff: All checks passed / pip-audit: PYSEC-2025-183(継続監視)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 摩擦ポイント
|
|
63
|
+
|
|
64
|
+
**今回の FT では実装上の摩擦はゼロだった。**
|
|
65
|
+
|
|
66
|
+
APIRouter パターン(FT177 F-1 対応)を最初から適用し、テストが一発で全通過した。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 観察点
|
|
71
|
+
|
|
72
|
+
### 観察1: 展開爆弾(Decompression Bomb)対策にはストリーミング解凍が必須
|
|
73
|
+
|
|
74
|
+
`zlib.decompress()` は解凍後サイズをチェックする前に全データをメモリに展開する。
|
|
75
|
+
ゼロバイト 50MB を level=9 で圧縮すると数百バイトになり、解凍すると 50MB になる。
|
|
76
|
+
悪意ある入力をそのまま `decompress()` すると OOM になりえる。
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# 危険: 上限チェック前に全解凍
|
|
80
|
+
zlib.decompress(huge_compressed_data) # → OOM の危険
|
|
81
|
+
|
|
82
|
+
# 安全: ストリーミングでチャンクごとに上限チェック
|
|
83
|
+
decompressor = zlib.decompressobj()
|
|
84
|
+
total = 0
|
|
85
|
+
for offset in range(0, len(data), CHUNK_SIZE):
|
|
86
|
+
chunk = decompressor.decompress(data[offset : offset + CHUNK_SIZE])
|
|
87
|
+
total += len(chunk)
|
|
88
|
+
if total > MAX_OUTPUT_BYTES:
|
|
89
|
+
return None # 上限超過で早期終了
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`zlib.decompressobj()` を使うストリーミング方式により、
|
|
93
|
+
解凍途中で上限(50MB)を超えたと判断できる。
|
|
94
|
+
|
|
95
|
+
### 観察2: CRC32 と Adler-32 の使い分け
|
|
96
|
+
|
|
97
|
+
両者とも `zlib` モジュールに含まれるが特性が異なる。
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
zlib.crc32(b"hello") & 0xFFFFFFFF # → 907060870
|
|
101
|
+
zlib.adler32(b"hello") & 0xFFFFFFFF # → 103547413
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| 特性 | CRC32 | Adler-32 |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| 用途 | ファイル整合性(PNG, ZIP, gzip) | zlib ストリームヘッダー |
|
|
107
|
+
| 計算速度 | やや遅い | 高速(加算のみ) |
|
|
108
|
+
| 小データ衝突耐性 | 高い | 低い(短文字列で衝突しやすい) |
|
|
109
|
+
| & 0xFFFFFFFF の必要性 | あり(符号ありの場合がある) | あり |
|
|
110
|
+
|
|
111
|
+
Web API で「ファイルのダウンロード整合性確認」には CRC32、
|
|
112
|
+
「zlib ストリームの内部チェックサム」には Adler-32 が適している。
|
|
113
|
+
|
|
114
|
+
### 観察3: 圧縮レベルの実効差
|
|
115
|
+
|
|
116
|
+
繰り返しデータ(`b"hello world! " * 100 = 1300 bytes`)では、
|
|
117
|
+
レベル 1〜9 の差は小さいが高圧縮データでは差が出る。
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
results = compare_compression_levels(b"hello world! " * 100)
|
|
121
|
+
# level=1: 34 bytes (ratio=0.0262)
|
|
122
|
+
# level=9: 22 bytes (ratio=0.0169)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
一般的な API ペイロード(JSON 等)ではレベル 6(デフォルト)が
|
|
126
|
+
速度と圧縮率のバランス点として適切。
|
|
127
|
+
|
|
128
|
+
### 観察4: ストリーミング圧縮の結果は oneshot と roundtrip 互換
|
|
129
|
+
|
|
130
|
+
`zlib.compressobj()` によるストリーミング圧縮の出力は、
|
|
131
|
+
`zlib.decompress()` や `zlib.decompressobj()` で正常に解凍できる。
|
|
132
|
+
チャンク境界に関係なくストリーム形式は同一なので、
|
|
133
|
+
ネットワーク越しの分割送信データをチャンク単位で圧縮してもラウンドトリップが保証される。
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
chunks = [data[i : i + 64] for i in range(0, len(data), 64)]
|
|
137
|
+
streamed = compress_streaming(chunks)
|
|
138
|
+
assert decompress_streaming(streamed) == data # ✅ 常に成立
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## nene2-python フレームワークとの統合
|
|
144
|
+
|
|
145
|
+
- `compress` / `decompress` エンドポイントは Content-Encoding 圧縮 API の基盤として使える
|
|
146
|
+
- `MAX_INPUT_BYTES = 10MB` + Pydantic `max_length=20_971_520`(hex 換算)で DoS 対策済み
|
|
147
|
+
- `MAX_OUTPUT_BYTES = 50MB` の展開爆弾対策は、ファイルアップロード API のメモリ安全性に直結
|
|
148
|
+
- `verify_crc32` / `verify_adler32` は `hmac.compare_digest` 相当の定数時間比較ではない点に注意
|
|
149
|
+
(チェックサム比較はタイミング攻撃対象にはならないため問題なし)
|
|
150
|
+
- `APIRouter` + `create_app()` パターン(FT177 F-1 対応)を最初から適用済み
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Developer Experience (DX) Review
|
|
155
|
+
|
|
156
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
157
|
+
|
|
158
|
+
ファイルアップロード API で圧縮ストレージを実装しようとしている。
|
|
159
|
+
|
|
160
|
+
**ドキュメント理解**: `zlib.compress()` / `zlib.decompress()` のペアは直感的。
|
|
161
|
+
圧縮レベルのデフォルト値(6)がなぜ最適なのかは公式ドキュメントに書いていない。
|
|
162
|
+
**事故リスク**: 高。`zlib.decompress()` に信頼できないデータを渡すと OOM になりうる。
|
|
163
|
+
`MAX_OUTPUT_BYTES` によるガードを知らずに実装すると本番で問題になる。
|
|
164
|
+
**規約の使いやすさ**: `hex()` / `bytes.fromhex()` の往復は Python 固有概念として最初の壁になる。
|
|
165
|
+
|
|
166
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
167
|
+
|
|
168
|
+
既存の zlib 圧縮コードをコピーして API に組み込もうとしている。
|
|
169
|
+
|
|
170
|
+
**コピペ可能性**: `compress()` / `decompress()` のラッパーは分かりやすい。
|
|
171
|
+
`_decompress_bytes()` の展開爆弾対策ロジックは読んでも「なぜ必要か」が分かりにくい。
|
|
172
|
+
**拡張時の罠**: `MAX_OUTPUT_BYTES` の定数を削除または増やすと展開爆弾に脆弱になる。
|
|
173
|
+
「動いているから削ってもいいか」と判断する人がいる。
|
|
174
|
+
**セキュリティ的な事故リスク**: 高。展開爆弾対策なしの実装はサービス停止に直結する。
|
|
175
|
+
|
|
176
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
177
|
+
|
|
178
|
+
TypeScript の `pako`(zlib の JS 実装)に慣れており、Python で同じことをしようとしている。
|
|
179
|
+
|
|
180
|
+
**エラーレスポンスの質**: 400 Bad Request に具体的なメッセージが返るのは良い。
|
|
181
|
+
圧縮爆弾で `None` が返ったときの 400 レスポンスがなぜ「Invalid compressed data」なのかは
|
|
182
|
+
クライアント実装側からは分かりにくい(「サイズ上限超過」と区別できない)。
|
|
183
|
+
**Python 固有概念の学習コスト**: `bytes.hex()` / `bytes.fromhex()` の往復は JS にない概念。
|
|
184
|
+
`zlib.decompressobj()` のストリーミング API は `pako` の `Inflate` に相当するが設計が異なる。
|
|
185
|
+
**事故リスク**: 低。HTTP 境界での Pydantic バリデーションが充実。
|
|
186
|
+
|
|
187
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
188
|
+
|
|
189
|
+
zlib を直接使うより、HTTP レスポンスの Content-Encoding や S3 のサーバー側圧縮を使うことが多い。
|
|
190
|
+
|
|
191
|
+
**他フレームワークとの差異**: Django では `GZipMiddleware` が透過的に圧縮するため、
|
|
192
|
+
zlib を直接操作するコードは書かない。nene2-python では zlib 操作がアプリコードに露出しており、
|
|
193
|
+
ユースケースが明確(ファイルストレージ等)でなければ設計レビューで指摘される。
|
|
194
|
+
**nene2-python の薄さへの評価**: `_decompress_bytes()` の展開爆弾対策ロジックは再利用可能な
|
|
195
|
+
ミドルウェア候補。フレームワーク側に `DecompressionSizeLimitMiddleware` として組み込む価値がある。
|
|
196
|
+
**本番投入可能性**: 展開爆弾対策が明示的に実装されており、本番品質として評価できる。
|
|
197
|
+
|
|
198
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
199
|
+
|
|
200
|
+
**コードレビューチェックポイント**:
|
|
201
|
+
- [x] `zlib.decompress()` を直接呼ばず、ストリーミング解凍で上限チェックをしているか
|
|
202
|
+
- [x] `MAX_OUTPUT_BYTES` が `MAX_INPUT_BYTES` より大きいことを確認(圧縮率を考慮)
|
|
203
|
+
- [x] `verify_crc32` / `verify_adler32` の比較が文字列の `==` であることの妥当性
|
|
204
|
+
(チェックサム比較はタイミング攻撃対象外なので OK)
|
|
205
|
+
|
|
206
|
+
**チームでの安全な共有パターン**: `_decompress_bytes()` を内部 API として隠蔽し、
|
|
207
|
+
公開 API は `decompress()` と `decompress_streaming()` の 2 つのみに絞った設計が良い。
|
|
208
|
+
**ツール追加の必要性**: `bandit` (ruff S ルール相当) の B322(`zlib.decompress` 直接使用)は
|
|
209
|
+
ruff にはないが、コードレビューチェックリストに追加すべき。
|
|
210
|
+
|
|
211
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
212
|
+
|
|
213
|
+
**ポリシー達成度**: 高
|
|
214
|
+
**「初心者でも安全な API」達成度**: 中
|
|
215
|
+
— `zlib.decompress()` 直接使用の罠は `_decompress_bytes()` の命名(アンダースコアで内部実装を示す)で
|
|
216
|
+
ある程度ガードできているが、stdlib の `zlib.decompress()` を直接呼ぶと再発する。
|
|
217
|
+
**設計上の負債**: 展開爆弾対策を nene2-python フレームワークの共通ユーティリティとして
|
|
218
|
+
`nene2.io.SafeDecompressor` 等に昇格させる価値がある。
|
|
219
|
+
**Follow-up Issue 候補**: なし(現状の実装で十分。フレームワーク統合は別 Issue で検討)
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Follow-up Issues
|
|
224
|
+
|
|
225
|
+
| 優先度 | タイトル | 種別 |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| 低 | `decompress` の 400 エラーメッセージをサイズ超過と不正データで分離する | feat |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## まとめ
|
|
232
|
+
|
|
233
|
+
FT179 では `zlib` モジュールを中心に、データ圧縮・解凍・チェックサム計算を実装した。
|
|
234
|
+
39 テストが全通過し、mypy/ruff も問題なし。
|
|
235
|
+
|
|
236
|
+
最大の発見は展開爆弾(Decompression Bomb)対策の必要性。
|
|
237
|
+
`zlib.decompress()` は解凍後サイズを事前チェックできないため、
|
|
238
|
+
`zlib.decompressobj()` によるストリーミング解凍でチャンクごとに上限(50MB)を監視する実装が必須。
|
|
239
|
+
|
|
240
|
+
APIRouter パターン(FT177 F-1 の改善)を最初から適用し、テストが一発で全通過した。
|
|
241
|
+
|
|
242
|
+
v1.8.50 としてリリース。
|