nene2-python 1.8.35__tar.gz → 1.8.37__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.35 → nene2_python-1.8.37}/PKG-INFO +1 -2
- nene2_python-1.8.37/docs/field-trials/2026-05-field-trial-165.md +389 -0
- nene2_python-1.8.37/docs/field-trials/2026-05-field-trial-166.md +246 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/pyproject.toml +1 -2
- {nene2_python-1.8.35 → nene2_python-1.8.37}/uv.lock +1 -3
- {nene2_python-1.8.35 → nene2_python-1.8.37}/.env.example +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/.gitignore +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/AGENTS.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/CHANGELOG.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/CLAUDE.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/Dockerfile +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/LICENSE +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/README.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/alembic/README +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/alembic/env.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/alembic.ini +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/compose.yaml +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/de/index.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/fr/index.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/index.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/index.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/reference/api.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/roadmap.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/todo/current.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/zh/index.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/package-lock.json +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/package.json +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/__main__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/app.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/mcp.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/schema.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/conftest.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.35 → nene2_python-1.8.37}/tests/scripts/test_export_openapi.py +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.37
|
|
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
|
|
@@ -28,7 +28,6 @@ Requires-Dist: httpx>=0.27
|
|
|
28
28
|
Requires-Dist: mcp>=1.0
|
|
29
29
|
Requires-Dist: pydantic-settings>=2.6
|
|
30
30
|
Requires-Dist: pydantic>=2.9
|
|
31
|
-
Requires-Dist: pyjwt>=2.12.0
|
|
32
31
|
Requires-Dist: python-multipart>=0.0.12
|
|
33
32
|
Requires-Dist: pyyaml>=6.0
|
|
34
33
|
Requires-Dist: sqlalchemy>=2.0.49
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# FT165: secrets モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: `secrets` モジュール — 暗号学的安全な乱数・タイミング安全比較・OTP 生成
|
|
5
|
+
**セキュリティ診断**: **あり**(165 % 3 = 0)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
Python 標準ライブラリの `secrets` モジュールを nene2-python フレームワーク上で検証した。
|
|
12
|
+
`secrets` は OS の乱数源(`/dev/urandom` 相当)を使用して暗号学的に安全なトークンを生成するモジュール。
|
|
13
|
+
セキュリティトークン・OTP・API キー生成のベストプラクティスであり、
|
|
14
|
+
`random` モジュールの代替として NIST SP 800-63B が推奨する。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 実装したサンプルアプリ
|
|
19
|
+
|
|
20
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft165-secrets/`
|
|
21
|
+
|
|
22
|
+
### 主要機能
|
|
23
|
+
|
|
24
|
+
| 関数 | 概要 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `generate_token_hex(nbytes)` | `secrets.token_hex()` で hex トークン生成、最低 128 bit を保証 |
|
|
27
|
+
| `generate_token_urlsafe(nbytes)` | `secrets.token_urlsafe()` で URL 安全トークン生成 |
|
|
28
|
+
| `generate_token_bytes_b64(nbytes)` | `secrets.token_bytes()` で raw バイト生成(Base64 エンコード返却) |
|
|
29
|
+
| `timing_safe_compare(a, b)` | `hmac.compare_digest()` でタイミング安全比較 |
|
|
30
|
+
| `randbelow_demo(upper)` | `secrets.randbelow()` で範囲内乱数生成 |
|
|
31
|
+
| `generate_otp(length)` | `secrets.choice()` で紛らわしい文字を除いた OTP 生成 |
|
|
32
|
+
| `random_module_usage_check()` | nene2-python ソースで `random` モジュール使用なしを検証 |
|
|
33
|
+
|
|
34
|
+
### HTTP エンドポイント
|
|
35
|
+
|
|
36
|
+
| メソッド | パス | 概要 |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| GET | `/secrets/token-hex` | hex トークン生成 |
|
|
39
|
+
| GET | `/secrets/token-urlsafe` | URL 安全トークン生成 |
|
|
40
|
+
| GET | `/secrets/token-bytes` | バイトトークン(Base64)生成 |
|
|
41
|
+
| POST | `/secrets/compare` | タイミング安全比較 |
|
|
42
|
+
| GET | `/secrets/randbelow` | 範囲内乱数 |
|
|
43
|
+
| POST | `/secrets/otp` | OTP 生成 |
|
|
44
|
+
| GET | `/secrets/audit/random-module-check` | セキュリティ自己診断 |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## テスト結果
|
|
49
|
+
|
|
50
|
+
**32 passed(摩擦ゼロ)**
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
32 passed in 0.87s
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 摩擦ポイント
|
|
59
|
+
|
|
60
|
+
**今回の FT では実装上の摩擦はゼロだった。**
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 観察点
|
|
65
|
+
|
|
66
|
+
### 観察1: `MIN_TOKEN_BYTES = 16` を強制してセキュリティ下限を守る
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
MIN_TOKEN_BYTES = 16 # NIST SP 800-63B: 最低 128 bit
|
|
70
|
+
|
|
71
|
+
def generate_token_hex(nbytes: int = 32) -> TokenResult:
|
|
72
|
+
safe_nbytes = max(MIN_TOKEN_BYTES, nbytes)
|
|
73
|
+
return TokenResult(value=secrets.token_hex(safe_nbytes), ...)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
呼び出し元が `nbytes=1` を渡しても 128 bit 以上を保証する。
|
|
77
|
+
nene2-python のトークン生成 API はこのパターンを標準にすべき。
|
|
78
|
+
|
|
79
|
+
### 観察2: `hmac.compare_digest` vs `secrets.compare_digest`
|
|
80
|
+
|
|
81
|
+
Python 3.12+ では `hmac.compare_digest` と `secrets.compare_digest` はどちらも定時間比較。
|
|
82
|
+
`secrets.compare_digest` は Python 3.14 で追加されたが、3.12 では `hmac.compare_digest` を使う。
|
|
83
|
+
nene2-python の `auth/local_verifier.py` は `secrets.compare_digest` を使っており正しい(3.14 環境)。
|
|
84
|
+
|
|
85
|
+
### 観察3: OTP のアルファベットから紛らわしい文字を除外
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
OTP_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # 0/O, 1/I/L を除外
|
|
89
|
+
code = "".join(secrets.choice(OTP_ALPHABET) for _ in range(length))
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`0` と `O`、`1` と `I` と `L` はフォントによって区別しにくい。
|
|
93
|
+
人間が読む OTP では除外が必須。`secrets.choice()` は毎回独立した乱数を引くため分布が均等。
|
|
94
|
+
|
|
95
|
+
### 観察4: `token_hex(n)` の文字列長は `2n`
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
secrets.token_hex(32) # → 64文字の hex 文字列
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`n` バイト = `2n` 文字(1 バイト = 2 hex 文字)。
|
|
102
|
+
ドキュメントに書いてあるが初心者が `len(token) == nbytes` と誤解しやすい。
|
|
103
|
+
|
|
104
|
+
### 観察5: `token_urlsafe(n)` の長さは可変(Base64 パディング依存)
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
secrets.token_urlsafe(32) # → 43文字(32バイトを Base64url でエンコード)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Base64url は 3 バイトごとに 4 文字になるためパディングで長さが変わる。
|
|
111
|
+
DB カラムのサイズ設計時は `len(token_urlsafe(n))` を実測すること。
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## nene2-python フレームワークとの統合
|
|
116
|
+
|
|
117
|
+
- `auth/local_verifier.py` がすでに `secrets.compare_digest()` を使用:正しい実装
|
|
118
|
+
- `security/webhook.py` が `hmac.compare_digest()` を使用:正しい実装
|
|
119
|
+
- `random` モジュールはソース全体で使用ゼロ:PASS
|
|
120
|
+
- トークン生成ヘルパーを `nene2.security` に追加するとユーザーが安全なデフォルトを使いやすくなる
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Developer Experience (DX) Review
|
|
125
|
+
|
|
126
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
127
|
+
|
|
128
|
+
セキュリティモジュールについて「とりあえず動くもの」を求める段階。
|
|
129
|
+
`random.token_hex()` が存在しないことを知らず、`random.random()` でトークンを作ってしまうリスクが高い。
|
|
130
|
+
|
|
131
|
+
**ドキュメント理解**: `secrets` vs `random` の使い分けがドキュメントにないと区別できない。
|
|
132
|
+
「API キー生成には必ず secrets モジュールを使う」という1文があれば十分。
|
|
133
|
+
|
|
134
|
+
**事故リスク**: **高**。`random.getrandbits(128)` で作ったトークンは予測可能。
|
|
135
|
+
攻撃者はシードを推測して有効なセッショントークンを列挙できる(実際の攻撃例あり)。
|
|
136
|
+
|
|
137
|
+
**規約の使いやすさ**: `secrets.token_urlsafe()` のシグネチャは直感的。
|
|
138
|
+
「`n` バイト → `2n` 文字」だけ補足があれば使える。
|
|
139
|
+
|
|
140
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
141
|
+
|
|
142
|
+
`uuid.uuid4()` をトークンとして使っているケースが多い(UUID v4 は 122 bit のエントロピーがあるが、
|
|
143
|
+
ライブラリによっては擬似乱数源を使う)。
|
|
144
|
+
|
|
145
|
+
**コピペ可能性**: `secrets.token_urlsafe(32)` のワンライナーはコピペしやすい。
|
|
146
|
+
サンプルに「これが正しいやり方」と明示されていれば従う。
|
|
147
|
+
|
|
148
|
+
**拡張時の罠**: タイミング攻撃が理解されていない。
|
|
149
|
+
`if user_token == db_token:` という `==` 比較を「動いているから OK」と思いがち。
|
|
150
|
+
`hmac.compare_digest` が必要な理由を 1 行コメントで説明する価値がある。
|
|
151
|
+
|
|
152
|
+
**セキュリティ的な事故リスク**: **高**。タイミング攻撃によるトークン列挙は実際の攻撃手法。
|
|
153
|
+
|
|
154
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
155
|
+
|
|
156
|
+
Node.js の `crypto.randomBytes()` / `crypto.timingSafeEqual()` と同等の概念を理解している。
|
|
157
|
+
|
|
158
|
+
**Python 固有概念の学習コスト**: `hmac.compare_digest` が `secrets.compare_digest` と違うモジュールにあることが混乱点。
|
|
159
|
+
Python 3.14+ では `secrets.compare_digest` も使える。
|
|
160
|
+
|
|
161
|
+
**事故リスク**: 低。暗号トークンの概念は既知。
|
|
162
|
+
|
|
163
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
164
|
+
|
|
165
|
+
Django の `get_random_string()` / `constant_time_compare()` との対応がわかれば即座に使える。
|
|
166
|
+
|
|
167
|
+
**他フレームワークとの差異**: Django は `django.utils.crypto.constant_time_compare()` でラップ済み。
|
|
168
|
+
nene2-python は `hmac.compare_digest()` を直接使う必要がある(より透明性が高い)。
|
|
169
|
+
|
|
170
|
+
**本番投入可能性**: 問題なし。`secrets` モジュールは stdlib で外部依存なし。
|
|
171
|
+
|
|
172
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
173
|
+
|
|
174
|
+
**コードレビューチェックポイント**:
|
|
175
|
+
- [ ] トークン生成に `random` / `uuid.uuid4()` ではなく `secrets` を使っているか
|
|
176
|
+
- [ ] トークン比較に `==` ではなく `hmac.compare_digest` / `secrets.compare_digest` を使っているか
|
|
177
|
+
- [ ] `nbytes` が最低 16 以上(128 bit)か
|
|
178
|
+
- [ ] OTP のアルファベットに紛らわしい文字(0/O/1/I/L)が含まれていないか
|
|
179
|
+
- [ ] `token_hex(n)` の長さが `n` ではなく `2n` 文字であることをカラムサイズ設計に反映しているか
|
|
180
|
+
|
|
181
|
+
**チームでの安全なパターン**: `nene2.security.generate_token()` ヘルパーに最低ビット強度を組み込むことで、
|
|
182
|
+
チームメンバーが誤って短いトークンを生成するリスクを排除できる。
|
|
183
|
+
|
|
184
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
185
|
+
|
|
186
|
+
**ポリシー達成度**: 高(`secrets` モジュール使用はCLAUDE.mdで必須化済み)
|
|
187
|
+
|
|
188
|
+
**「初心者でも安全な API」達成度**: 中
|
|
189
|
+
- `nene2.security` に最低 128 bit を保証するトークン生成ヘルパーがあれば「高」になる
|
|
190
|
+
- 現状は「`secrets` を使え」というポリシーだけで、安全なデフォルトが提供されていない
|
|
191
|
+
|
|
192
|
+
**設計上の負債**: `nene2.security` に `generate_api_key()`、`generate_session_token()` などの
|
|
193
|
+
安全なデフォルトを持つ高レベルヘルパーが未実装。
|
|
194
|
+
|
|
195
|
+
**Follow-up Issue 候補**: `feat: nene2.security に generate_token() ヘルパーを追加(最低 128 bit 保証)`
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## セキュリティ診断(FT165 — 165 % 3 = 0)
|
|
200
|
+
|
|
201
|
+
> **診断方針**: Django・FastAPI・SQLAlchemy 本体でも CVE が報告されてきたレベルの
|
|
202
|
+
> 攻撃ベクターを対象とする。「動いているから安全」は不正解。
|
|
203
|
+
|
|
204
|
+
### 1. OWASP API Security Top 10 (2023)
|
|
205
|
+
|
|
206
|
+
#### API1: BOLA / IDOR
|
|
207
|
+
- ユーザー所有リソースがないため対象外(トークン生成 API)
|
|
208
|
+
- **結果**: ✅ 対象外
|
|
209
|
+
|
|
210
|
+
#### API2: 認証の破損
|
|
211
|
+
- [ ] `auth/local_verifier.py:36` で `secrets.compare_digest()` 使用確認 → **✅ PASS**
|
|
212
|
+
- [ ] 保護エンドポイント: FT165 アプリは認証なしの公開 API のため対象外
|
|
213
|
+
- **結果**: ✅
|
|
214
|
+
|
|
215
|
+
#### API3: Mass Assignment
|
|
216
|
+
- 実測: `POST /secrets/compare` に `{"a":"x","b":"y","is_admin":true}` → 200、レスポンスに `is_admin` なし
|
|
217
|
+
- Pydantic が extra フィールドを無視(デフォルト動作)
|
|
218
|
+
- **結果**: ✅ PASS
|
|
219
|
+
|
|
220
|
+
#### API4: 無制限リソース消費
|
|
221
|
+
- 実測: `a="x"*513` → 422(`max_length=512` で拒否)
|
|
222
|
+
- 実測: `upper=1_000_001` → 422(`le=1_000_000` で拒否)
|
|
223
|
+
- `ThrottleMiddleware`: FT165 アプリは未設定(本番では必須)
|
|
224
|
+
- **結果**: ✅ バリデーション境界は機能。スロットリングは本番設定が必要
|
|
225
|
+
|
|
226
|
+
#### API5: 機能レベルの認可不備
|
|
227
|
+
- FT165 は認証不要の公開 API のため対象外
|
|
228
|
+
- **結果**: ✅ 対象外
|
|
229
|
+
|
|
230
|
+
#### API6: SSRF
|
|
231
|
+
- URL を受け取るフィールドなし
|
|
232
|
+
- **結果**: ✅ 対象外
|
|
233
|
+
|
|
234
|
+
#### API7: セキュリティの設定ミス
|
|
235
|
+
- 実測: `X-Request-Id`, `X-Content-Type-Options`, `X-Frame-Options` → ✅ 全レスポンスに付与
|
|
236
|
+
- 実測: 422 エラーレスポンスにスタックトレース含まれず → ✅
|
|
237
|
+
- CORS ワイルドカード: `grep allow_origins="*"` → ✅ PASS
|
|
238
|
+
- **結果**: ✅
|
|
239
|
+
|
|
240
|
+
#### API8〜10
|
|
241
|
+
- バージョン管理・デバッグエンドポイント・外部 API 消費: FT165 アプリでは対象なし
|
|
242
|
+
- **結果**: ✅ 対象外
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### 2. インジェクション攻撃
|
|
247
|
+
|
|
248
|
+
#### SQL インジェクション
|
|
249
|
+
- `grep -rn 'f".*SELECT|INSERT|UPDATE|DELETE'` → **PASS(0 件)**
|
|
250
|
+
- FT165 アプリに DB 操作なし
|
|
251
|
+
- **結果**: ✅
|
|
252
|
+
|
|
253
|
+
#### コマンドインジェクション
|
|
254
|
+
- `grep -rn "shell=True\|os\.system"` → **PASS(0 件)**
|
|
255
|
+
- `random_module_usage_check()` 内の `subprocess.run()` は固定引数のみ(ユーザー入力なし)
|
|
256
|
+
- **結果**: ✅
|
|
257
|
+
|
|
258
|
+
#### パストラバーサル
|
|
259
|
+
- FT165 アプリにファイル操作なし
|
|
260
|
+
- **結果**: ✅ 対象外
|
|
261
|
+
|
|
262
|
+
#### SSTI
|
|
263
|
+
- テンプレートエンジン使用なし
|
|
264
|
+
- **結果**: ✅ 対象外
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### 3. 認証・認可
|
|
269
|
+
|
|
270
|
+
- `random` モジュール使用: **PASS(0 件)**
|
|
271
|
+
- `secrets.compare_digest` / `hmac.compare_digest` の使用: **✅ 両方で正しく実装済み**
|
|
272
|
+
- `auth/local_verifier.py:36`: `secrets.compare_digest(token, allowed)`
|
|
273
|
+
- `security/webhook.py:29`: `hmac.compare_digest(expected, signature)`
|
|
274
|
+
- `SecretStr` 使用箇所: 2 件(設定クラス内)
|
|
275
|
+
- **結果**: ✅ 全 PASS
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### 4. 入力バリデーション
|
|
280
|
+
|
|
281
|
+
- `CompareBody.a/b`: `max_length=512` → 実測で 413 文字超で 422 ✅
|
|
282
|
+
- `nbytes`: `ge=1, le=64` で境界チェック ✅
|
|
283
|
+
- `upper`: `ge=2, le=1_000_000` で境界チェック ✅
|
|
284
|
+
- タイプエラー: `nbytes="bad"` → 422(スタックトレースなし)✅
|
|
285
|
+
- **結果**: ✅
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### 5. 情報漏洩
|
|
290
|
+
|
|
291
|
+
- 422 エラーに `traceback` / `File "` 含まれず ✅
|
|
292
|
+
- `SecretStr` でパスワード系フィールドを保護(2 箇所)✅
|
|
293
|
+
- セキュリティヘッダー全レスポンスに付与 ✅
|
|
294
|
+
- **結果**: ✅
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### 6. Python / FastAPI 固有の攻撃ベクター
|
|
299
|
+
|
|
300
|
+
#### ReDoS
|
|
301
|
+
- `middleware/request_id.py` の UUID 正規表現: `^[0-9a-f]{8}-...-[0-9a-f]{12}$`
|
|
302
|
+
- 実測(悪意ある入力 `"a"*40+"!"`): **0.00ms** — バックトラッキング爆発なし ✅
|
|
303
|
+
- **結果**: ✅
|
|
304
|
+
|
|
305
|
+
#### pickle / yaml / eval
|
|
306
|
+
- `grep eval\|exec\|pickle.loads\|yaml.load` → **PASS(0 件)** ✅
|
|
307
|
+
- **結果**: ✅
|
|
308
|
+
|
|
309
|
+
#### 非同期レースコンディション・型強制攻撃
|
|
310
|
+
- FT165 はステートレスな計算 API のため共有状態なし ✅
|
|
311
|
+
- Pydantic: `int` フィールドに文字列 `"bad"` → 422(型強制失敗でエラー返却)✅
|
|
312
|
+
- **結果**: ✅
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
### 7. 依存関係の脆弱性スキャン
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
uv run pip-audit
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**結果**:
|
|
323
|
+
|
|
324
|
+
| Package | Version | ID | Fix Versions |
|
|
325
|
+
|---|---|---|---|
|
|
326
|
+
| pyjwt | 2.12.1 | PYSEC-2025-183 | (未記載) |
|
|
327
|
+
|
|
328
|
+
**CRITICAL: 1 件**
|
|
329
|
+
|
|
330
|
+
**詳細分析**:
|
|
331
|
+
- PyJWT 2.12.1 は `PYSEC-2025-183` の影響を受ける
|
|
332
|
+
- `pip-audit` の Fix Versions 欄が空 → 修正版未リリースの可能性
|
|
333
|
+
- **実際の影響**: nene2-python のソースコード全体を検索した結果、`import jwt` / `import pyjwt` が実コードに存在しない(コメントのみ)
|
|
334
|
+
- PyJWT は `pyproject.toml` に直接依存として宣言されていたが **デッド依存** → 本 FT で削除済み
|
|
335
|
+
- **推移的依存として残存**: `mcp>=1.0` パッケージが `pyjwt[crypto]` を推移的依存として使用しているため、pip-audit では引き続き検出される
|
|
336
|
+
|
|
337
|
+
**対応方針**:
|
|
338
|
+
1. `pyproject.toml` からの直接依存宣言: **本 FT で削除済み** (`uv remove pyjwt`)
|
|
339
|
+
2. 推移的依存(mcp 経由): mcp 側の修正を待つ。Fix Versions が空のため修正版未リリース。Issue で追跡する。
|
|
340
|
+
3. nene2-python のコードが PyJWT を直接使わないため、実際の攻撃面はゼロ。
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
### 診断サマリー
|
|
345
|
+
|
|
346
|
+
| カテゴリ | 結果 | 最重要発見 |
|
|
347
|
+
|---|---|---|
|
|
348
|
+
| OWASP API Security Top 10 | ✅ | スロットリング未設定(本番要対応) |
|
|
349
|
+
| SQL インジェクション | ✅ | — |
|
|
350
|
+
| コマンドインジェクション | ✅ | — |
|
|
351
|
+
| パストラバーサル | ✅ | — |
|
|
352
|
+
| SSTI | ✅ | — |
|
|
353
|
+
| 認証・認可 | ✅ | compare_digest 正しく実装 |
|
|
354
|
+
| 入力バリデーション | ✅ | — |
|
|
355
|
+
| 情報漏洩 | ✅ | — |
|
|
356
|
+
| ReDoS | ✅ | 0ms — 安全 |
|
|
357
|
+
| pickle / yaml / eval | ✅ | — |
|
|
358
|
+
| 非同期レースコンディション | ✅ | — |
|
|
359
|
+
| 型強制攻撃 | ✅ | — |
|
|
360
|
+
| 依存関係 CVE | ❌ | **PyJWT 2.12.1 PYSEC-2025-183(デッド依存)** |
|
|
361
|
+
|
|
362
|
+
**総合評価**: **条件付き合格**(PyJWT CVE を次 PR で対処すること)
|
|
363
|
+
|
|
364
|
+
**発見した脆弱性**: 1 件(CRITICAL: 1 — PyJWT デッド依存 CVE)
|
|
365
|
+
|
|
366
|
+
**新規セキュリティ Issue**: PyJWT 依存削除または更新
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Follow-up Issues
|
|
371
|
+
|
|
372
|
+
| 優先度 | タイトル | 種別 |
|
|
373
|
+
|---|---|---|
|
|
374
|
+
| 高 | `fix: pyproject.toml から未使用の pyjwt 直接依存を削除(PYSEC-2025-183)` ← **本 FT で対処済み** | security |
|
|
375
|
+
| 高 | `track: mcp の推移的依存 pyjwt PYSEC-2025-183 修正版リリース待ち` | security |
|
|
376
|
+
| 中 | `feat: nene2.security に generate_token() ヘルパーを追加(最低 128 bit 保証)` | feat |
|
|
377
|
+
| 低 | `docs: secrets vs random の使い分けを how-to に追加` | docs |
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## まとめ
|
|
382
|
+
|
|
383
|
+
`secrets` モジュールは暗号学的安全なトークン生成・タイミング安全比較の標準手段。
|
|
384
|
+
32 テスト全通過、摩擦ゼロで実装完了。
|
|
385
|
+
|
|
386
|
+
セキュリティ診断では nene2-python のソースコード全体を横断的に確認した。
|
|
387
|
+
`compare_digest` の正しい使用・`random` モジュール不使用・型バリデーション・セキュリティヘッダーはすべて合格。
|
|
388
|
+
**唯一の発見: PyJWT 2.12.1 のデッド依存 CVE (PYSEC-2025-183)**。
|
|
389
|
+
実際に使われていない依存が CVE を持つことはそれ自体がリスクのため、即時削除 PR を作成する。
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# FT166: functools モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: `functools` モジュール — `lru_cache`・`cached_property`・`partial`・`wraps`・`reduce`・`cache`
|
|
5
|
+
**セキュリティ診断**: なし(166 % 3 = 1)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
Python 標準ライブラリの `functools` モジュールを nene2-python フレームワーク上で検証した。
|
|
12
|
+
`functools` は高階関数・デコレーター・メモ化のユーティリティを提供し、
|
|
13
|
+
nene2-python の DI パターン・レスポンスキャッシュ・ミドルウェア実装に直結する。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 実装したサンプルアプリ
|
|
18
|
+
|
|
19
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft166-functools/`
|
|
20
|
+
|
|
21
|
+
### 主要機能
|
|
22
|
+
|
|
23
|
+
| 関数/クラス | 概要 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `fibonacci(n)` | `@lru_cache(maxsize=128)` でメモ化された再帰 fibonacci |
|
|
26
|
+
| `expensive_computation(key)` | `@lru_cache(maxsize=32)` で文字列キーのキャッシュ |
|
|
27
|
+
| `HeavyComputer` | `@cached_property` で total / average を遅延計算・キャッシュ |
|
|
28
|
+
| `double / triple` | `functools.partial` で multiply の引数を固定 |
|
|
29
|
+
| `greet_hello / greet_hi` | `partial` でテンプレートと記号を固定したあいさつ生成 |
|
|
30
|
+
| `timing_decorator` | `@functools.wraps` でデコレーターが元の関数名・docstring を保持 |
|
|
31
|
+
| `product_reduce(numbers)` | `functools.reduce` で積を計算 |
|
|
32
|
+
| `flatten_reduce(nested)` | `reduce` でネストしたリストをフラット化 |
|
|
33
|
+
| `collatz_steps(n)` | `@functools.cache`(unbounded)でコラッツ数列ステップ数をメモ化 |
|
|
34
|
+
|
|
35
|
+
### HTTP エンドポイント
|
|
36
|
+
|
|
37
|
+
| メソッド | パス | 概要 |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| GET | `/functools/fibonacci` | lru_cache 付き fibonacci(キャッシュ統計返却) |
|
|
40
|
+
| GET | `/functools/lru-cache` | 任意キーのキャッシュ統計デモ |
|
|
41
|
+
| GET | `/functools/cached-property` | HeavyComputer の計算回数デモ |
|
|
42
|
+
| GET | `/functools/partial` | double / triple / greet デモ |
|
|
43
|
+
| GET | `/functools/wraps` | slow_add の __name__ 保持確認 |
|
|
44
|
+
| POST | `/functools/reduce-product` | 整数リストの積 |
|
|
45
|
+
| POST | `/functools/reduce-flatten` | ネストリストのフラット化 |
|
|
46
|
+
| GET | `/functools/cache-collatz` | unbounded cache でコラッツ数列 |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## テスト結果
|
|
51
|
+
|
|
52
|
+
**34 passed(摩擦ゼロ)**
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
34 passed in 0.83s
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 摩擦ポイント
|
|
61
|
+
|
|
62
|
+
**今回の FT では実装上の摩擦はゼロだった。**
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 観察点
|
|
67
|
+
|
|
68
|
+
### 観察1: `@lru_cache` はグローバル状態 — テスト間でキャッシュを `clear` する必要がある
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
@pytest.fixture(autouse=True)
|
|
72
|
+
def clear_caches() -> None:
|
|
73
|
+
fibonacci.cache_clear()
|
|
74
|
+
expensive_computation.cache_clear()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`@lru_cache` の対象関数はプロセス共有のキャッシュを持つ。
|
|
78
|
+
テスト間でキャッシュが汚染されるため `autouse=True` で毎回クリアする必要がある。
|
|
79
|
+
nene2-python でキャッシュを使う UseCase は `cache_clear()` を lifespan の shutdown で呼ぶか、
|
|
80
|
+
または `@lru_cache` をクラスメソッドに適用してインスタンス単位でスコープを制御する。
|
|
81
|
+
|
|
82
|
+
### 観察2: `@cached_property` はインスタンス単位 — `lru_cache` との使い分け
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
class HeavyComputer:
|
|
86
|
+
@functools.cached_property
|
|
87
|
+
def total(self) -> int:
|
|
88
|
+
self.compute_count += 1
|
|
89
|
+
return sum(self._data)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`@cached_property` はインスタンスの `__dict__` に値を保存する。
|
|
93
|
+
同じインスタンスで 2 回目のアクセスは計算なし。新しいインスタンスは再計算。
|
|
94
|
+
`@lru_cache` はプロセス全体で共有・引数でキー管理、`@cached_property` はインスタンスで管理。
|
|
95
|
+
nene2-python での使い分け:
|
|
96
|
+
- UseCase / Repository インスタンスの設定値計算 → `@cached_property`
|
|
97
|
+
- 引数ベースの重い計算(ページネーション計算など)→ `@lru_cache`
|
|
98
|
+
|
|
99
|
+
### 観察3: `@functools.cache` = `@lru_cache(maxsize=None)` でメモリ無制限
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
@functools.cache # Python 3.9+ / unbounded
|
|
103
|
+
def collatz_steps(n: int) -> int: ...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`@cache` は `@lru_cache(maxsize=None)` の略記で、キャッシュサイズ制限なし。
|
|
107
|
+
再帰的な数学関数に適しているが、引数の値域が大きい場合はメモリ枯渇に注意。
|
|
108
|
+
nene2-python での用途: 設定値の解析結果など、引数の種類が少ない場合に使う。
|
|
109
|
+
|
|
110
|
+
### 観察4: `functools.partial` で DI 設定をカリー化できる
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
double = functools.partial(multiply, y=2)
|
|
114
|
+
greet_hello = functools.partial(make_greeting, "Hello")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
FastAPI の `Depends` と組み合わせる場合、`partial` でリポジトリに設定を注入できる:
|
|
118
|
+
```python
|
|
119
|
+
get_repo = functools.partial(NoteRepository, db_url=settings.db_url)
|
|
120
|
+
Depends(get_repo)
|
|
121
|
+
```
|
|
122
|
+
ただし `Depends` に渡す関数はシグネチャが重要なため、`partial` が残す引数を確認すること。
|
|
123
|
+
|
|
124
|
+
### 観察5: `@functools.wraps` なしのデコレーターは `__name__` を上書きする
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
def timing_decorator(func):
|
|
128
|
+
# @functools.wraps(func) がないと:
|
|
129
|
+
def wrapper(*args, **kwargs): ...
|
|
130
|
+
return wrapper # → slow_add.__name__ == "wrapper" になる
|
|
131
|
+
|
|
132
|
+
def timing_decorator(func):
|
|
133
|
+
@functools.wraps(func) # ← これがないと OpenAPI ルート名が壊れる
|
|
134
|
+
def wrapper(*args, **kwargs): ...
|
|
135
|
+
return wrapper
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
FastAPI はルート関数の `__name__` を operationId に使う。
|
|
139
|
+
カスタムミドルウェア・デコレーターに `@functools.wraps` がないと OpenAPI スキーマが壊れる。
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## nene2-python フレームワークとの統合
|
|
144
|
+
|
|
145
|
+
- `@lru_cache` は `TtlCache`(FT100 で追加済み)と役割が異なる(TTL なし vs TTL あり)
|
|
146
|
+
- `@cached_property` は Repository の設定パース結果のキャッシュに最適
|
|
147
|
+
- `@functools.wraps` は nene2-python の `BearerTokenMiddleware` 等のデコレーターで使用が推奨される
|
|
148
|
+
- `functools.reduce` は UseCase の集計ロジックに使えるが、`sum()` / リスト内包表記の方が可読性が高い場合が多い
|
|
149
|
+
- `@functools.cache` は設定値の解析・静的ルックアップに適している
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Developer Experience (DX) Review
|
|
154
|
+
|
|
155
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
156
|
+
|
|
157
|
+
`@lru_cache` は「デコレーターをつけるとキャッシュされる」という概念は分かりやすいが、
|
|
158
|
+
`cache_clear()` が必要なことや、グローバル共有であることは見落としやすい。
|
|
159
|
+
|
|
160
|
+
**ドキュメント理解**: `@lru_cache` のサンプルはフィボナッチが定番だが、
|
|
161
|
+
「Web API の UseCase でいつ使うか」の例がないと「難しそうなやつ」で止まる。
|
|
162
|
+
|
|
163
|
+
**事故リスク**: 中。`@lru_cache` をテストで使うときにキャッシュをクリアし忘れ、
|
|
164
|
+
他のテストのキャッシュが漏れ込んでテストが通ったり失敗したりするフレーキーなテストが生まれる。
|
|
165
|
+
|
|
166
|
+
**規約の使いやすさ**: `@functools.wraps` の必要性は理解しにくい。
|
|
167
|
+
「デコレーターを作るときは必ず `@functools.wraps` をつける」という規則を覚えれば十分。
|
|
168
|
+
|
|
169
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
170
|
+
|
|
171
|
+
`functools` は名前を知っていても使いこなせていないことが多い。
|
|
172
|
+
`reduce` は「forループで書いた方が分かる」と言って使わない傾向がある。
|
|
173
|
+
|
|
174
|
+
**コピペ可能性**: `@lru_cache(maxsize=128)` と `@functools.wraps(func)` はそのままコピペできる。
|
|
175
|
+
|
|
176
|
+
**拡張時の罠**: `@lru_cache` の引数が変わったときにキャッシュが古い値を返し続ける。
|
|
177
|
+
引数の型が mutable(`list`, `dict`)だと `TypeError: unhashable type` が発生することに気づかない。
|
|
178
|
+
|
|
179
|
+
**セキュリティ的な事故リスク**: 低。ただし `@lru_cache` に認証情報を含む引数を渡すと
|
|
180
|
+
別ユーザーのキャッシュが混入するリスクがある(BOLA の一形態)。
|
|
181
|
+
|
|
182
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
183
|
+
|
|
184
|
+
`useMemo` / `useCallback` と `@lru_cache` / `@cached_property` は概念が近い。
|
|
185
|
+
「同じ入力なら再計算しない」という理解はすでにある。
|
|
186
|
+
|
|
187
|
+
**Python 固有概念の学習コスト**: `partial` は JS の `bind` や `curry` に近い概念。
|
|
188
|
+
`reduce` は `Array.prototype.reduce` と同じ。TypeScript ユーザーには直感的。
|
|
189
|
+
|
|
190
|
+
**事故リスク**: 低。
|
|
191
|
+
|
|
192
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
193
|
+
|
|
194
|
+
Django の `@method_decorator` / `cache_page` との差異を理解している。
|
|
195
|
+
`@functools.wraps` は既知。`@lru_cache` の eviction ポリシー(LRU)を正確に理解している。
|
|
196
|
+
|
|
197
|
+
**他フレームワークとの差異**: Django は `cache.get/set` で明示的にキャッシュを管理する。
|
|
198
|
+
nene2-python で `@lru_cache` を使う場合、TTL がないことを意識する必要がある。
|
|
199
|
+
TTL が必要なら FT100 で追加した `TtlCache` を使う。
|
|
200
|
+
|
|
201
|
+
**本番投入可能性**: 問題なし。ただしキャッシュキーのサイズとメモリ消費量の設計が必要。
|
|
202
|
+
|
|
203
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
204
|
+
|
|
205
|
+
**コードレビューチェックポイント**:
|
|
206
|
+
- [ ] `@lru_cache` / `@cache` の引数に mutable 型(list, dict)が渡されていないか(`TypeError` になる)
|
|
207
|
+
- [ ] `@lru_cache` をリクエストスコープで使っていないか(プロセス共有で期待外の値が返る)
|
|
208
|
+
- [ ] `@lru_cache` に認証情報・ユーザーIDが引数に含まれる場合、BOLA にならないか
|
|
209
|
+
- [ ] デコレーターに `@functools.wraps` がついているか(OpenAPI operationId の破損防止)
|
|
210
|
+
- [ ] `@cache`(unbounded)を使う場合、引数の値域が有限か(メモリリーク防止)
|
|
211
|
+
|
|
212
|
+
**チームでの安全なパターン**: キャッシュが必要な UseCase は `cache_clear()` を公開メソッドとして持ち、
|
|
213
|
+
lifespan の shutdown / テストの teardown で明示的にクリアする規約を設ける。
|
|
214
|
+
|
|
215
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
216
|
+
|
|
217
|
+
**ポリシー達成度**: 高
|
|
218
|
+
|
|
219
|
+
**「初心者でも安全な API」達成度**: 中
|
|
220
|
+
- `@lru_cache` のキャッシュ汚染リスク(テスト・BOLA)をドキュメントが説明していない
|
|
221
|
+
- `@functools.wraps` の必要性が CLAUDE.md に明記されていない(FastAPI との相性問題)
|
|
222
|
+
|
|
223
|
+
**設計上の負債・ドキュメント不足**:
|
|
224
|
+
- nene2-python のカスタムデコレーター作成時の `@functools.wraps` 使用がポリシー化されていない
|
|
225
|
+
- `@lru_cache` の「リクエストスコープ不可・認証情報を引数に含めない」ルールが未文書
|
|
226
|
+
|
|
227
|
+
**Follow-up Issue 候補**: `docs: functools.wraps をカスタムデコレーター規約に追加`
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Follow-up Issues
|
|
232
|
+
|
|
233
|
+
| 優先度 | タイトル | 種別 |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| 中 | `docs: @functools.wraps をデコレーター作成規約として CLAUDE.md に追加` | docs |
|
|
236
|
+
| 低 | `docs: @lru_cache のリクエストスコープ不可・認証情報禁止をキャッシュ how-to に記載` | docs |
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## まとめ
|
|
241
|
+
|
|
242
|
+
`functools` は nene2-python の DI・キャッシュ・デコレーター実装に広く関連するモジュール。
|
|
243
|
+
34 テスト全通過、摩擦ゼロ。
|
|
244
|
+
`@cached_property` がインスタンス単位、`@lru_cache` がプロセス単位という使い分けが重要。
|
|
245
|
+
`@functools.wraps` なしのデコレーターは FastAPI の operationId を破壊するため、
|
|
246
|
+
カスタムデコレーター規約への追記が推奨される。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nene2-python"
|
|
3
|
-
version = "1.8.
|
|
3
|
+
version = "1.8.37"
|
|
4
4
|
description = "NENE2 Python — minimal API framework following NENE2's design philosophy"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -32,7 +32,6 @@ dependencies = [
|
|
|
32
32
|
"mcp>=1.0",
|
|
33
33
|
"pyyaml>=6.0",
|
|
34
34
|
"httpx>=0.27",
|
|
35
|
-
"pyjwt>=2.12.0",
|
|
36
35
|
]
|
|
37
36
|
|
|
38
37
|
[project.urls]
|