nene2-python 1.8.47__tar.gz → 1.8.48__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.47 → nene2_python-1.8.48}/PKG-INFO +1 -1
- nene2_python-1.8.48/docs/field-trials/2026-05-field-trial-177.md +416 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/pyproject.toml +1 -1
- {nene2_python-1.8.47 → nene2_python-1.8.48}/.env.example +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/.gitignore +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/AGENTS.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/CHANGELOG.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/CLAUDE.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/Dockerfile +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/LICENSE +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/README.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/alembic/README +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/alembic/env.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/alembic.ini +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/compose.yaml +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/de/index.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-168.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-169.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-170.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-171.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-172.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-173.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-174.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-175.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-176.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/fr/index.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/index.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/index.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/reference/api.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/roadmap.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/todo/current.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/zh/index.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/package-lock.json +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/package.json +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/__main__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/app.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/mcp.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/schema.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/conftest.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/tests/scripts/test_export_openapi.py +0 -0
- {nene2_python-1.8.47 → nene2_python-1.8.48}/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.48
|
|
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,416 @@
|
|
|
1
|
+
# FT177: hashlib モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: 安全なハッシュ・パスワード保護・ファイル整合性検証(PBKDF2 / scrypt / Blake2)
|
|
5
|
+
**セキュリティ診断**: **あり**(177 % 3 = 0)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
Python 標準の `hashlib` モジュールを nene2-python 上で検証する。
|
|
12
|
+
単なる SHA-256 だけでなく、パスワード保護 (PBKDF2・scrypt)、MAC 生成 (Blake2 キー付き)、
|
|
13
|
+
ファイル整合性検証(チャンク読み込み)まで網羅し、暗号的に安全な実装パターンを確立する。
|
|
14
|
+
FT177 は 3 の倍数のためセキュリティ診断も実施する。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 実装したサンプルアプリ
|
|
19
|
+
|
|
20
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft177-hashlib/`
|
|
21
|
+
|
|
22
|
+
### 主要機能
|
|
23
|
+
|
|
24
|
+
| 関数/定数 | 概要 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `SECURE_ALGORITHMS` | 許可リスト (`sha256`, `sha512`, `sha3_256`, `sha3_512`, `blake2b`, `blake2s`) |
|
|
27
|
+
| `INSECURE_ALGORITHMS` | 拒否リスト (`md5`, `sha1`, `sha224`) |
|
|
28
|
+
| `hash_text(text, algorithm)` | テキストをハッシュ化(非推奨アルゴリズムは `None` 返却) |
|
|
29
|
+
| `hash_bytes(data, algorithm)` | バイト列をハッシュ化 |
|
|
30
|
+
| `hash_streaming(chunks, algorithm)` | イテレータからチャンク単位でハッシュ化 |
|
|
31
|
+
| `available_algorithms()` | 安全/非推奨/その他に分類した一覧 |
|
|
32
|
+
| `generate_salt()` | `secrets.token_hex(32)` — 256-bit 暗号論的乱数 salt |
|
|
33
|
+
| `hash_password(password, salt, iterations)` | PBKDF2-HMAC-SHA256(最低 200,000 イテレーション) |
|
|
34
|
+
| `verify_password(password, stored_hash, salt)` | `hmac.compare_digest` によるタイミングセーフ検証 |
|
|
35
|
+
| `scrypt_hash(password, salt)` | scrypt (N=16384, r=8, p=1) — メモリハード KDF |
|
|
36
|
+
| `blake2_keyed_hash(data, key)` | Blake2b キー付きハッシュで MAC 生成 |
|
|
37
|
+
| `verify_blake2_mac(data, key, expected_mac)` | Blake2b MAC のタイミングセーフ検証 |
|
|
38
|
+
| `hash_file_content(content, algorithm)` | 64 KB チャンク処理でファイルをハッシュ化 |
|
|
39
|
+
| `verify_file_integrity(content, expected_hash)` | ファイル整合性のタイミングセーフ検証 |
|
|
40
|
+
| `generate_token(length)` | SHA-256 ベースのセキュアトークン生成 |
|
|
41
|
+
| `derive_key_for_purpose(master_key, purpose)` | 用途別鍵導出(PBKDF2 で purpose を salt に使用) |
|
|
42
|
+
|
|
43
|
+
### HTTP エンドポイント
|
|
44
|
+
|
|
45
|
+
| メソッド | パス | 概要 |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| GET | `/algorithms` | 安全/非推奨アルゴリズム一覧 |
|
|
48
|
+
| POST | `/hash` | テキストをハッシュ化 |
|
|
49
|
+
| POST | `/password/hash` | PBKDF2 パスワードハッシュ(salt 自動生成可) |
|
|
50
|
+
| POST | `/password/verify` | パスワード検証(タイミングセーフ) |
|
|
51
|
+
| POST | `/password/scrypt` | scrypt パスワードハッシュ |
|
|
52
|
+
| POST | `/mac/blake2` | Blake2b MAC 生成 |
|
|
53
|
+
| POST | `/mac/verify` | Blake2b MAC 検証(タイミングセーフ) |
|
|
54
|
+
| POST | `/file/verify` | ファイル整合性検証 |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## テスト結果
|
|
59
|
+
|
|
60
|
+
**62 passed**
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
62 passed in 2.04s
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
mypy: Success(3 ファイル、エラーゼロ)
|
|
67
|
+
ruff check: All checks passed
|
|
68
|
+
ruff format: 3 files reformatted → 再チェックで already formatted
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 摩擦ポイント
|
|
73
|
+
|
|
74
|
+
### F-1: `@app.post` デコレータが `create_app()` 返却インスタンスに適用されない(深刻度: 中)
|
|
75
|
+
|
|
76
|
+
**事象**: app.py でモジュールレベルに `app = create_app()` を作成し `@app.post(...)` でルートを定義した。
|
|
77
|
+
`TestClient(create_app())` が新しい FastAPI インスタンスを返すためルートが空になり、全エンドポイントで 404 が返る。
|
|
78
|
+
|
|
79
|
+
**原因**: デコレータは定義時の `app` オブジェクトにルートを登録する。
|
|
80
|
+
`create_app()` は呼ぶたびに新規インスタンスを返すので、モジュールレベルの `app` と別インスタンスになる。
|
|
81
|
+
|
|
82
|
+
**対応**: `APIRouter` を使用し、`create_app()` の中で `application.include_router(router)` する。
|
|
83
|
+
FT170 以降で繰り返し発生しているため、init-ft.sh のテンプレートまたは CLAUDE.md に「FastAPI アプリファクトリは `APIRouter` パターンを使う」旨を追記する。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 観察点
|
|
88
|
+
|
|
89
|
+
### 観察1: アルゴリズム許可リスト — 拒否側ではなく許可側で制御
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
SECURE_ALGORITHMS = frozenset({"sha256", "sha512", "sha3_256", "sha3_512", "blake2b", "blake2s"})
|
|
93
|
+
|
|
94
|
+
def hash_text(text: str, algorithm: str = "sha256") -> str | None:
|
|
95
|
+
if algorithm not in SECURE_ALGORITHMS:
|
|
96
|
+
return None
|
|
97
|
+
...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`if algorithm in INSECURE_ALGORITHMS: return None` ではなく許可リストで制御することで、
|
|
101
|
+
未知の新アルゴリズムが追加されても自動的に拒否される。セキュリティ設計の基本原則。
|
|
102
|
+
|
|
103
|
+
### 観察2: PBKDF2 最低イテレーション数の強制
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
MIN_PBKDF2_ITERATIONS = 200_000 # NIST SP 800-132 推奨最低値
|
|
107
|
+
|
|
108
|
+
def hash_password(password: str, salt: str, iterations: int = MIN_PBKDF2_ITERATIONS) -> str:
|
|
109
|
+
if iterations < MIN_PBKDF2_ITERATIONS:
|
|
110
|
+
iterations = MIN_PBKDF2_ITERATIONS
|
|
111
|
+
...
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
API 側で `ge=200_000` の Pydantic バリデーションも設けているが、`demos.py` 側でも下限を強制する二重防御。
|
|
115
|
+
ドメイン層がセキュリティ制約を持つことでHTTP 以外から呼ばれても安全を保てる。
|
|
116
|
+
|
|
117
|
+
### 観察3: `hmac.compare_digest` の徹底
|
|
118
|
+
|
|
119
|
+
パスワード検証・MAC 検証・ファイル整合性検証の全 3 か所で `==` 比較ではなく
|
|
120
|
+
`hmac.compare_digest` を使用。タイミングサイドチャネル攻撃(timing attack)への対策。
|
|
121
|
+
|
|
122
|
+
### 観察4: scrypt vs PBKDF2 の使い分け
|
|
123
|
+
|
|
124
|
+
scrypt はメモリハード(N=16384 では約 16 MB のメモリを消費)のため、
|
|
125
|
+
GPU/ASIC による総当たり攻撃に対して PBKDF2 より強い。
|
|
126
|
+
ただしサーバーリソースも多く消費するため、高負荷環境では PBKDF2 が現実的。
|
|
127
|
+
テストで両者が異なる出力を生成することを確認済み。
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## nene2-python フレームワークとの統合
|
|
132
|
+
|
|
133
|
+
- `APIRouter` + `create_app()` パターンは nene2-python の標準的な設計に沿う(F-1 の解決策)
|
|
134
|
+
- `hash_password` / `verify_password` は UseCase 層に組み込める純粋関数として設計
|
|
135
|
+
- Pydantic の `max_length` 制限で DoS 防止(`password` は 1,000文字、`text` は 10,000文字)
|
|
136
|
+
- HTTP 境界での入力バリデーション(`ge=200_000` など)とドメイン層の二重防御がポリシー準拠
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Developer Experience (DX) Review
|
|
141
|
+
|
|
142
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
143
|
+
|
|
144
|
+
認証機能を初めて実装しようとしており、「パスワードをハッシュ化する」という要件がある。
|
|
145
|
+
|
|
146
|
+
**ドキュメント理解**: `hash_password()` 関数の存在を見つけられれば使えるが、
|
|
147
|
+
「なぜ `iterations=200_000` が必要か」「salt とは何か」の説明がコード内コメントにないため、
|
|
148
|
+
コピペして使うとイテレーション数を削って「速くした」事故が起きやすい。
|
|
149
|
+
**事故リスク**: 中。`hash_password("secret", generate_salt(), iterations=1000)` は動作するが危険。
|
|
150
|
+
ドキュメントなしでは最低イテレーション数の意味が分からない。
|
|
151
|
+
**規約の使いやすさ**: `generate_salt()` → `hash_password()` → `verify_password()` の3点セットが分かりやすい。
|
|
152
|
+
型注釈があるので補完で辿れる。
|
|
153
|
+
|
|
154
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
155
|
+
|
|
156
|
+
既存の `hashlib.md5(password.encode()).hexdigest()` を使ったコードを見て「これでいいか」と思っている。
|
|
157
|
+
|
|
158
|
+
**コピペ可能性**: `hash_text("password", "md5")` が `None` を返すことで明示的に失敗する設計は良い。
|
|
159
|
+
ただし `None` チェックを怠ると `None.hex()` で AttributeError になり、デバッグが難しい場面もある。
|
|
160
|
+
**拡張時の罠**: `SECURE_ALGORITHMS` に直接 `"md5"` を追加してしまうと全防御が崩れる。
|
|
161
|
+
定数を immutable (`frozenset`) にしているのは良い。
|
|
162
|
+
**セキュリティ的な事故リスク**: 中。パスワードに `hash_text()` を使うと salt なし → 同じパスワードが同じハッシュになるという致命的なミスが可能。`hash_password()` を使わせるガイドが必要。
|
|
163
|
+
|
|
164
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
165
|
+
|
|
166
|
+
パスワード登録・ログイン API を TypeScript クライアントから叩く立場。
|
|
167
|
+
|
|
168
|
+
**エラーレスポンスの質**: `algorithm: "md5"` を送ると `400 Bad Request` が返り、
|
|
169
|
+
`"Insecure or unsupported algorithm: md5"` というメッセージが返る。クライアント実装には十分。
|
|
170
|
+
**Python 固有概念の学習コスト**: `bytes.fromhex(salt)` や `derived.hex()` はバイト⇔文字列変換の理解が必要。
|
|
171
|
+
TypeScript の `Buffer.from(hex, 'hex')` に相当するが、知らないと戸惑う。
|
|
172
|
+
**事故リスク**: 低。HTTP 境界での Pydantic バリデーションが充実しており、不正入力は400で弾かれる。
|
|
173
|
+
|
|
174
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
175
|
+
|
|
176
|
+
Django の `make_password()` / `check_password()` を使い慣れており、代替実装を評価する。
|
|
177
|
+
|
|
178
|
+
**他フレームワークとの差異**: Django は `pbkdf2_sha256$N$salt$hash` 形式で保存するが、
|
|
179
|
+
このFTでは hash と salt を別フィールドで管理するシンプルな形式。
|
|
180
|
+
移行時にはシリアライズ形式を揃える必要がある。
|
|
181
|
+
**nene2-python の薄さへの評価**: `hash_password` / `verify_password` が純粋関数なのは好評。
|
|
182
|
+
UseCase に組み込んでもHTTPの知識が不要なのは Django の `auth` モジュール依存より明快。
|
|
183
|
+
**本番投入可能性**: scrypt の `N=2**14` は WSL2 環境で問題ないが本番では `N=2**16` 以上を推奨。
|
|
184
|
+
テストが遅くなるのでテスト専用の低 N 設定が必要になる場面がある。
|
|
185
|
+
|
|
186
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
187
|
+
|
|
188
|
+
チームでこのコードをレビューする際のリスク評価。
|
|
189
|
+
|
|
190
|
+
**コードレビューチェックポイント**:
|
|
191
|
+
- [x] `hash_text()` をパスワードハッシュに使っていないか(salt なし → レインボーテーブル攻撃可能)
|
|
192
|
+
- [x] `iterations` パラメータが 200,000 未満にオーバーライドされていないか
|
|
193
|
+
- [x] `hmac.compare_digest` ではなく `==` でハッシュ比較していないか(timing attack)
|
|
194
|
+
- [x] salt が `generate_salt()` 以外(固定文字列など)で生成されていないか
|
|
195
|
+
|
|
196
|
+
**チームでの安全な共有パターン**: `hash_password` / `verify_password` の組み合わせで使うことを
|
|
197
|
+
ADR またはドキュメントで明文化すると良い。`hash_text` はファイル整合性など非パスワード用途専用と明示。
|
|
198
|
+
**ツール追加の必要性**: `ruff S303`(`hashlib.md5`/`sha1` を使用している場合の警告)が有効。
|
|
199
|
+
ただし `SECURE_ALGORITHMS` チェックがあるため検出は二重になる。
|
|
200
|
+
|
|
201
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
202
|
+
|
|
203
|
+
CLAUDE.md との整合性確認。
|
|
204
|
+
|
|
205
|
+
**ポリシー達成度**: 高
|
|
206
|
+
**「初心者でも安全な API」達成度**: 中
|
|
207
|
+
— `hash_text()` がパスワードハッシュに誤用される余地がある点でやや低い。
|
|
208
|
+
関数名を `hash_content()` に変え、パスワード用途は明示的に `hash_password()` のみにする設計も検討できる。
|
|
209
|
+
**設計上の負債・ドキュメント不足**:
|
|
210
|
+
- F-1 (APIRouter パターン) は CLAUDE.md または init-ft.sh テンプレートに追記が必要
|
|
211
|
+
- scrypt の N パラメータが環境依存なのでテスト時と本番時の設定分離ガイドが欲しい
|
|
212
|
+
**Follow-up Issue 候補**: F-1 の APIRouter パターン文書化 → Issue #501
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## セキュリティ診断
|
|
217
|
+
|
|
218
|
+
> **診断方針**: 今回は hashlib 自体がセキュリティモジュールのため、
|
|
219
|
+
> 「hashlib の誤用パターン」が主な検査対象となる。
|
|
220
|
+
|
|
221
|
+
### 1. OWASP API Security Top 10 (2023)
|
|
222
|
+
|
|
223
|
+
#### API1: オブジェクトレベルの認可不備 (BOLA / IDOR)
|
|
224
|
+
- 認証・認可が絡むエンドポイントなし(ハッシュ計算と検証のみ)
|
|
225
|
+
- **結果**: ✅ 該当なし
|
|
226
|
+
|
|
227
|
+
#### API2: 認証の破損 (Broken Authentication)
|
|
228
|
+
- 認証ミドルウェアなし(FT なので最小構成)
|
|
229
|
+
- パスワード検証ロジック自体は `hmac.compare_digest` でタイミングセーフ
|
|
230
|
+
- **結果**: ✅ 設計上の問題なし
|
|
231
|
+
|
|
232
|
+
#### API3: オブジェクトプロパティレベルの認可不備 (Mass Assignment)
|
|
233
|
+
- Pydantic の `BaseModel` はデフォルトで定義外フィールドを無視
|
|
234
|
+
- `{"password": "x", "is_admin": true}` を POST → `is_admin` は無視される
|
|
235
|
+
- **結果**: ✅ 問題なし
|
|
236
|
+
|
|
237
|
+
#### API4: 無制限リソース消費 (Unrestricted Resource Consumption)
|
|
238
|
+
- `password`: `max_length=1_000` / `text`: `max_length=10_000` / `content_hex`: `max_length=20_971_520`
|
|
239
|
+
- `demos.py` 側でも `MAX_INPUT_BYTES = 10 * 1024 * 1024` チェック
|
|
240
|
+
- scrypt は計算コストが高い(`N=16384` で約 50ms)— Pydantic 入力バリデーションでペイロードサイズは制限済み
|
|
241
|
+
- **結果**: ✅ DoS ベクター対策済み
|
|
242
|
+
|
|
243
|
+
#### API5〜API10
|
|
244
|
+
- SSRF: 外部 URL を受け取るエンドポイントなし ✅
|
|
245
|
+
- セキュリティヘッダー: FT 最小構成のため nene2 ミドルウェアなし(本番投入時に追加必須)
|
|
246
|
+
- デバッグエンドポイント: `/docs` は残存(FT 環境は許容)
|
|
247
|
+
- **結果**: ✅ FT スコープ内では問題なし
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### 2. インジェクション攻撃
|
|
252
|
+
|
|
253
|
+
#### SQL インジェクション
|
|
254
|
+
- DB アクセスなし
|
|
255
|
+
- **結果**: ✅ 該当なし
|
|
256
|
+
|
|
257
|
+
#### コマンドインジェクション
|
|
258
|
+
- `subprocess` / `os.system` 使用なし(ruff S602/S605 確認済み)
|
|
259
|
+
- **結果**: ✅
|
|
260
|
+
|
|
261
|
+
#### パストラバーサル
|
|
262
|
+
- ファイルパス操作なし(`content` は bytes として受け取る)
|
|
263
|
+
- **結果**: ✅ 該当なし
|
|
264
|
+
|
|
265
|
+
#### SSTI / HTTP ヘッダーインジェクション
|
|
266
|
+
- テンプレートエンジン使用なし / レスポンスヘッダーへのユーザー入力反映なし
|
|
267
|
+
- **結果**: ✅
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### 3. 認証・認可
|
|
272
|
+
|
|
273
|
+
- **パスワードハッシュ**: PBKDF2-HMAC-SHA256 (200,000 iterations) / scrypt (N=16384) 実装済み ✅
|
|
274
|
+
- **salt 生成**: `secrets.token_hex(32)` — 256-bit 暗号論的乱数 ✅
|
|
275
|
+
- **MD5・SHA-1 拒否**: `SECURE_ALGORITHMS` 許可リストで完全拒否 ✅
|
|
276
|
+
- **タイミング攻撃**: `hmac.compare_digest` を全検証箇所で使用 ✅
|
|
277
|
+
- **JWT**: 使用なし(PYSEC-2025-183 の影響なし)✅
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### 4. 入力バリデーション
|
|
282
|
+
|
|
283
|
+
実際の攻撃ペイロードを TestClient で送信:
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
# 巨大入力
|
|
287
|
+
POST /hash {"text": "A" * 100_001}
|
|
288
|
+
# → 422 Unprocessable Entity (max_length=10_000)
|
|
289
|
+
|
|
290
|
+
# 最低イテレーション未満
|
|
291
|
+
POST /password/hash {"password": "x", "iterations": 1000}
|
|
292
|
+
# → 422 (ge=200_000)
|
|
293
|
+
|
|
294
|
+
# 無効な hex
|
|
295
|
+
POST /file/verify {"content_hex": "ZZZZ", "expected_hash": "..."}
|
|
296
|
+
# → 400 "Invalid hex content"
|
|
297
|
+
|
|
298
|
+
# 非 ASCII algorithm
|
|
299
|
+
POST /hash {"text": "hello", "algorithm": "SHA-256"}
|
|
300
|
+
# → 400 "Insecure or unsupported algorithm: SHA-256"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- **結果**: ✅ 全ペイロードで適切なエラーが返る
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### 5. 情報漏洩
|
|
308
|
+
|
|
309
|
+
- hashlib の内部エラーは `except ValueError` でキャッチして `400` に変換
|
|
310
|
+
- スタックトレースは FastAPI のデフォルト動作で非公開(`APP_DEBUG` 未設定)
|
|
311
|
+
- `pip-audit` 結果: `PYSEC-2025-183` (PyJWT 2.12.1 / mcp 経由の推移的依存)
|
|
312
|
+
— 直接使用なし、Fix バージョン未提供のため**許容**(FT174 以降と同じ判断)
|
|
313
|
+
- **結果**: ⚠️ PYSEC-2025-183 は継続監視(修正版リリース待ち)
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### 6. Python / FastAPI 固有の攻撃ベクター
|
|
318
|
+
|
|
319
|
+
#### ReDoS
|
|
320
|
+
- 正規表現使用なし
|
|
321
|
+
- **結果**: ✅ 該当なし
|
|
322
|
+
|
|
323
|
+
#### pickle / yaml インジェクション
|
|
324
|
+
- `pickle` / `yaml.load` 使用なし
|
|
325
|
+
- **結果**: ✅
|
|
326
|
+
|
|
327
|
+
#### 非同期レースコンディション
|
|
328
|
+
- グローバルな mutable 状態なし(全関数が純粋関数)
|
|
329
|
+
- **結果**: ✅
|
|
330
|
+
|
|
331
|
+
#### 型強制攻撃 (Pydantic)
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
POST /password/hash {"password": "x", "iterations": "2e5"}
|
|
335
|
+
# → 200 OK (iterations = 200000 として型変換)
|
|
336
|
+
|
|
337
|
+
POST /password/hash {"password": "x", "iterations": 1.5}
|
|
338
|
+
# → 422 (int フィールドに float は不正)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`"2e5"` が 200000 に変換されるのは Pydantic v2 の仕様(許容範囲内、200,000 >= `ge=200_000`)。
|
|
342
|
+
**結果**: ✅ セキュリティ上の問題なし(`ge` 制約が通過するため)
|
|
343
|
+
|
|
344
|
+
#### hashlib 特有: アルゴリズム名インジェクション
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
POST /hash {"text": "hello", "algorithm": "sha256;DROP TABLE--"}
|
|
348
|
+
# → 400 "Insecure or unsupported algorithm: sha256;DROP TABLE--"
|
|
349
|
+
# SECURE_ALGORITHMS の frozenset チェックで即拒否
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
- **結果**: ✅ 許可リスト方式で任意文字列を `hashlib.new()` に渡さない
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
### 7. 依存関係の脆弱性スキャン
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
Found 1 known vulnerability in 1 package
|
|
360
|
+
Name Version ID Fix Versions
|
|
361
|
+
----- ------- -------------- ------------
|
|
362
|
+
pyjwt 2.12.1 PYSEC-2025-183
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
- **スキャン結果**: CRITICAL: 0件 / HIGH: 0件 / MEDIUM: 0件 / LOW: 1件(PyJWT 経由)
|
|
366
|
+
- **対応方針**: 直接使用なし・mcp の推移的依存・Fix バージョン未提供のため許容
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
### 診断サマリー
|
|
371
|
+
|
|
372
|
+
| カテゴリ | 結果 | 最重要発見 |
|
|
373
|
+
|---|---|---|
|
|
374
|
+
| OWASP API Security Top 10 | ✅ 全通過 | - |
|
|
375
|
+
| SQL インジェクション | ✅ 該当なし | - |
|
|
376
|
+
| コマンドインジェクション | ✅ | - |
|
|
377
|
+
| パストラバーサル | ✅ 該当なし | - |
|
|
378
|
+
| SSTI | ✅ 該当なし | - |
|
|
379
|
+
| 認証・認可 | ✅ | PBKDF2+scrypt+Blake2 全対応 |
|
|
380
|
+
| 入力バリデーション | ✅ | 全境界で max_length/ge/le 設定済み |
|
|
381
|
+
| 情報漏洩 | ⚠️ | PYSEC-2025-183(継続監視) |
|
|
382
|
+
| ReDoS | ✅ 該当なし | - |
|
|
383
|
+
| pickle / yaml | ✅ | - |
|
|
384
|
+
| 非同期レースコンディション | ✅ | - |
|
|
385
|
+
| 型強制攻撃 | ✅ | `"2e5"` 変換は仕様範囲内 |
|
|
386
|
+
| アルゴリズム名インジェクション | ✅ | 許可リスト方式で完全防御 |
|
|
387
|
+
| 依存関係 CVE | ⚠️ 1件 | PYSEC-2025-183(PyJWT/mcp 経由) |
|
|
388
|
+
|
|
389
|
+
**総合評価**: 条件付き合格(PYSEC-2025-183 を継続監視)
|
|
390
|
+
**発見した脆弱性**: 0件(CRITICAL: 0 / HIGH: 0 / MEDIUM: 0 / LOW: 0 ※CVE は推移的依存)
|
|
391
|
+
**新規セキュリティ Issue**: #501(APIRouter パターン文書化)
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Follow-up Issues
|
|
396
|
+
|
|
397
|
+
| 優先度 | タイトル | 種別 |
|
|
398
|
+
|---|---|---|
|
|
399
|
+
| 中 | F-1: FastAPI アプリファクトリで APIRouter パターンを CLAUDE.md または init-ft.sh に記載 | docs |
|
|
400
|
+
| 低 | scrypt の N パラメータをテスト/本番で分ける設定ガイドを追加 | docs |
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## まとめ
|
|
405
|
+
|
|
406
|
+
FT177 では `hashlib` を中心に、PBKDF2・scrypt・Blake2 キー付きハッシュという3種のセキュリティプリミティブを実装した。
|
|
407
|
+
62 テストが全通過し、mypy/ruff も問題なし。
|
|
408
|
+
|
|
409
|
+
セキュリティ診断では「hashlib そのものがセキュリティモジュール」という特性から
|
|
410
|
+
インジェクション系のリスクはほぼ皆無だったが、**アルゴリズム名インジェクション**に対する
|
|
411
|
+
許可リスト方式の有効性を実証でき、今後の設計パターンとして参照できる。
|
|
412
|
+
|
|
413
|
+
主要摩擦点は F-1 の `@app.post` と `create_app()` の分離問題(毎 FT で発生)で、
|
|
414
|
+
`APIRouter` パターンへの移行を次 FT 以降の標準とする。
|
|
415
|
+
|
|
416
|
+
v1.8.48 としてリリース。
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nene2_python-1.8.47 → nene2_python-1.8.48}/alembic/versions/001_create_notes_and_tags_tables.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|