nene2-python 1.8.45__tar.gz → 1.8.47__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.45 → nene2_python-1.8.47}/PKG-INFO +1 -1
- nene2_python-1.8.47/docs/field-trials/2026-05-field-trial-175.md +271 -0
- nene2_python-1.8.47/docs/field-trials/2026-05-field-trial-176.md +341 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/pyproject.toml +1 -1
- {nene2_python-1.8.45 → nene2_python-1.8.47}/.env.example +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/.gitignore +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/AGENTS.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/CHANGELOG.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/CLAUDE.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/Dockerfile +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/LICENSE +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/README.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/alembic/README +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/alembic/env.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/alembic.ini +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/compose.yaml +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/de/index.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-168.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-169.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-170.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-171.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-172.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-173.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-174.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/fr/index.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/index.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/index.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/reference/api.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/roadmap.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/todo/current.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/zh/index.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/package-lock.json +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/package.json +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/__main__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/app.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/mcp.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/schema.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/conftest.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/tests/scripts/test_export_openapi.py +0 -0
- {nene2_python-1.8.45 → nene2_python-1.8.47}/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.47
|
|
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,271 @@
|
|
|
1
|
+
# FT175: logging モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: `logging` モジュール — センシティブデータマスキング・LoggerAdapter・dictConfig
|
|
5
|
+
**セキュリティ診断**: なし(FT176 で実施)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
Python 標準ライブラリの `logging` モジュールを検証する。
|
|
12
|
+
CLAUDE.md で「`print()` 禁止・`logging` モジュールのみ使用」と明示されており、
|
|
13
|
+
nene2-python の根幹ポリシーに直結するモジュールである。
|
|
14
|
+
|
|
15
|
+
このFTで確認する点:
|
|
16
|
+
- `logging.Filter` によるセンシティブデータのマスキング(パスワード・トークン・カード番号)
|
|
17
|
+
- `logging.LoggerAdapter` によるリクエストIDの全ログへの付与
|
|
18
|
+
- `setup_logger()` パターン(テスト用 StringIO へのキャプチャ)
|
|
19
|
+
- `parse_log_level()` によるログレベルの安全な変換
|
|
20
|
+
- `logging.config.dictConfig` による宣言的ログ設定
|
|
21
|
+
- `capture_logs()` / `release_capture()` テストユーティリティ
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 実装したサンプルアプリ
|
|
26
|
+
|
|
27
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft175-logging/`
|
|
28
|
+
|
|
29
|
+
### 主要機能
|
|
30
|
+
|
|
31
|
+
| 関数/クラス | 概要 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `mask_sensitive(message)` | パスワード・トークン・API鍵・カード番号をマスク |
|
|
34
|
+
| `SensitiveFilter` | `logging.Filter` サブクラス — LogRecord のメッセージを自動マスク |
|
|
35
|
+
| `RequestIdAdapter` | `LoggerAdapter[Logger]` — 全ログに `[request_id]` を付与 |
|
|
36
|
+
| `setup_logger(name, level, stream)` | テスト用 StringIO 出力ロガーを作成 |
|
|
37
|
+
| `parse_log_level(level_str)` | 文字列 → `logging.DEBUG` 等の定数、不明は `INFO` |
|
|
38
|
+
| `log_event(logger, level, message, extra)` | 構造化ログエントリ(辞書)を記録して返す |
|
|
39
|
+
| `is_level_enabled(logger, level)` | ログレベルが有効かを `bool` で返す |
|
|
40
|
+
| `effective_level_name(logger)` | 有効ログレベル名を文字列で返す |
|
|
41
|
+
| `LOGGING_CONFIG` | `dictConfig` 用設定辞書(SensitiveFilter 組み込み) |
|
|
42
|
+
| `apply_logging_config()` | dictConfig を適用する |
|
|
43
|
+
| `capture_logs(logger)` | テスト用キャプチャハンドラーを追加して `(StringIO, handler)` を返す |
|
|
44
|
+
| `release_capture(logger, handler)` | キャプチャハンドラーを解放する |
|
|
45
|
+
|
|
46
|
+
マスキングパターン:
|
|
47
|
+
|
|
48
|
+
| パターン | 置換後 |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `password=<4文字以上>` | `password=***` |
|
|
51
|
+
| `token: <4文字以上>` | `token: ***` |
|
|
52
|
+
| `api_key=<4文字以上>` | `api_key=***` |
|
|
53
|
+
| `\b\d{13,19}\b`(カード番号) | `****-****-****-****` |
|
|
54
|
+
|
|
55
|
+
### HTTP エンドポイント
|
|
56
|
+
|
|
57
|
+
| メソッド | パス | 概要 |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| GET | `/logging/mask` | センシティブデータをマスクした文字列を返す |
|
|
60
|
+
| GET | `/logging/level` | ロガーのレベル有効判定 |
|
|
61
|
+
| POST | `/logging/event` | イベントを記録(センシティブマスク付き) |
|
|
62
|
+
| GET | `/logging/events` | 記録済みイベント一覧 |
|
|
63
|
+
| DELETE | `/logging/events` | イベント一覧をクリア |
|
|
64
|
+
| GET | `/logging/parse-level` | 文字列をログレベル数値に変換 |
|
|
65
|
+
| GET | `/logging/buffer` | インメモリバッファのログ行一覧 |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## テスト結果
|
|
70
|
+
|
|
71
|
+
**32 passed**
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
32 passed in 0.34s
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 摩擦ポイント
|
|
80
|
+
|
|
81
|
+
### F-1: `logging.StreamHandler` のジェネリック型注釈(深刻度: 低)
|
|
82
|
+
|
|
83
|
+
**事象**: `logging.StreamHandler` は `StreamHandler[StringIO]` とジェネリック型注釈できるが、
|
|
84
|
+
mypy が `# type: ignore[type-arg]` なしでは警告を出すケースがある。
|
|
85
|
+
|
|
86
|
+
**原因**: `StreamHandler` は Python 3.12 でジェネリック対応済みだが、
|
|
87
|
+
`StreamHandler` の型スタブが `Generic[TextIO]` として定義されているため
|
|
88
|
+
`StreamHandler` 単独だと型引数省略警告が出ることがある。
|
|
89
|
+
|
|
90
|
+
**対応**: 関数シグネチャに `# type: ignore[type-arg]` を追記。
|
|
91
|
+
これは mypy の型スタブ側の制約であり実装ミスではないため、CLAUDE.md 規約に従いコード添付コメントで理由を明記した。
|
|
92
|
+
|
|
93
|
+
### F-2: `LoggerAdapter.extra` の型(深刻度: 低)
|
|
94
|
+
|
|
95
|
+
**事象**: `LoggerAdapter[Logger]` の `self.extra` の型が `Mapping[str, Any] | None` なので
|
|
96
|
+
`self.extra.get("request_id")` の前に `if self.extra` のガードが必要。
|
|
97
|
+
|
|
98
|
+
**原因**: `LoggerAdapter.__init__` の `extra` パラメーターが `Mapping[str, Any] | None` で
|
|
99
|
+
初期化できるため、mypy がnullチェックを要求する。
|
|
100
|
+
|
|
101
|
+
**対応**: `request_id = self.extra.get("request_id", "-") if self.extra else "-"` で対応。
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 観察点
|
|
106
|
+
|
|
107
|
+
### 観察1: `SensitiveFilter` によるパイプライン的マスキング
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
class SensitiveFilter(logging.Filter):
|
|
111
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
112
|
+
record.msg = mask_sensitive(str(record.msg))
|
|
113
|
+
if record.args:
|
|
114
|
+
if isinstance(record.args, dict):
|
|
115
|
+
record.args = {k: mask_sensitive(str(v)) for k, v in record.args.items()}
|
|
116
|
+
elif isinstance(record.args, tuple):
|
|
117
|
+
record.args = tuple(mask_sensitive(str(a)) for a in record.args)
|
|
118
|
+
return True
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`logging.Logger.info("user %s password=%s", username, password)` のように
|
|
122
|
+
フォーマット引数を分離して渡すケースでは `record.args` をマスクする必要がある。
|
|
123
|
+
`record.msg` だけマスクしても `%s` 置換後の最終文字列に平文が現れるためである。
|
|
124
|
+
|
|
125
|
+
### 観察2: `RequestIdAdapter` でコンテキストを注入する
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
class RequestIdAdapter(logging.LoggerAdapter[logging.Logger]):
|
|
129
|
+
def process(self, msg: str, kwargs: Any) -> tuple[str, Any]:
|
|
130
|
+
request_id = self.extra.get("request_id", "-") if self.extra else "-"
|
|
131
|
+
return f"[{request_id}] {msg}", kwargs
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
FastAPI のリクエストスコープで `RequestIdAdapter` インスタンスを生成し、
|
|
135
|
+
同一リクエスト内の全ログに `[req-xxxxxxxx]` プレフィックスを付与できる。
|
|
136
|
+
`logging.getLogger()` グローバルシングルトンと異なり、スコープごとに別インスタンスを作れる。
|
|
137
|
+
|
|
138
|
+
### 観察3: `parse_log_level` の安全な変換
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
def parse_log_level(level_str: str) -> int:
|
|
142
|
+
level = logging.getLevelName(level_str.upper())
|
|
143
|
+
if not isinstance(level, int): # 不明な文字列は int ではなく文字列を返す
|
|
144
|
+
return logging.INFO
|
|
145
|
+
return level
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`logging.getLevelName("UNKNOWN")` は `"Level UNKNOWN"` という文字列を返す(None ではない)。
|
|
149
|
+
`isinstance(level, int)` チェックでフォールバックを実装する必要がある。
|
|
150
|
+
|
|
151
|
+
### 観察4: `dictConfig` で `SensitiveFilter` を `()` 形式でインスタンス化
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
"filters": {
|
|
155
|
+
"sensitive": {
|
|
156
|
+
"()": SensitiveFilter, # クラスを直接参照してインスタンス化
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`logging.config.dictConfig` では `"()"` キーを使ってカスタムクラスのコンストラクターを呼べる。
|
|
162
|
+
`class` キーは `logging` 組み込みクラスのみに使われ、カスタムクラスには `()` を使う。
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## nene2-python フレームワークとの統合
|
|
167
|
+
|
|
168
|
+
- nene2-python の `nene2.log` には `structlog` ベースの設定が既に存在する。
|
|
169
|
+
`logging` モジュールの `SensitiveFilter` パターンは `structlog` の `processor` と
|
|
170
|
+
役割が対応する(`structlog` では `ProcessorFormatter` で同様のマスキングが可能)
|
|
171
|
+
- CLAUDE.md の「`logging` モジュールのみ使用(`print()` 禁止)」を実証する FT となった
|
|
172
|
+
- `RequestIdAdapter` と nene2 の `RequestIdMiddleware` が生成する `x-request-id` を
|
|
173
|
+
連動させることで、ログとHTTPレスポンスの追跡IDを統一できる
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Developer Experience (DX) Review
|
|
178
|
+
|
|
179
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
180
|
+
|
|
181
|
+
`logging.basicConfig(level=logging.DEBUG)` から入る人には、
|
|
182
|
+
`logging.Filter` のサブクラス化は最初のハードルになる。
|
|
183
|
+
|
|
184
|
+
**ドキュメント理解**: `filter()` が `True` を返すとログ通過、`False` で破棄という
|
|
185
|
+
Python の慣習は直感と逆に感じる場合がある(True = 「フィルターを通す」という意味)。
|
|
186
|
+
**事故リスク**: 高。`record.args` のマスクを忘れてフォーマット引数に平文パスワードが
|
|
187
|
+
残るパターンは気づきにくい。`SensitiveFilter` のような共通フィルターを強制するのが安全。
|
|
188
|
+
**規約の使いやすさ**: `setup_logger()` ファクトリ関数でテスト・本番の切り替えができる。
|
|
189
|
+
|
|
190
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
191
|
+
|
|
192
|
+
`print()` から `logging` への移行は「なぜ面倒なことをするのか」と感じやすい層。
|
|
193
|
+
センシティブデータの事故事例を見せることが動機付けに有効。
|
|
194
|
+
|
|
195
|
+
**コピペ可能性**: `setup_logger()` はそのままコピーして使える。
|
|
196
|
+
**拡張時の罠**: `logger.propagate = False` を忘れると親ロガーにも出力され、
|
|
197
|
+
二重ログ・センシティブデータ漏洩につながる。
|
|
198
|
+
**セキュリティ的な事故リスク**: 高。パスワードをログに書いて CloudWatch / Datadog に流れた
|
|
199
|
+
インシデントは実際に多数報告されている。`SensitiveFilter` は必須。
|
|
200
|
+
|
|
201
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
202
|
+
|
|
203
|
+
Node.js の `winston` / `pino` と比較すると Python の `logging` は低レベル API に感じるが、
|
|
204
|
+
`dictConfig` による宣言的設定で同様の構成が可能。
|
|
205
|
+
|
|
206
|
+
**エラーレスポンスの質**: `/logging/mask` エンドポイントで
|
|
207
|
+
「original_length(入力長)+ masked(マスク後)」を返す設計でクライアント側が動作確認しやすい。
|
|
208
|
+
**Python 固有概念の学習コスト**: `LogRecord` の `msg` と `args` の分離は
|
|
209
|
+
Python %-style フォーマットの知識が必要で、JS 開発者には説明が必要。
|
|
210
|
+
**事故リスク**: 中。HTTP API として使う分には Pydantic が入力を保護している。
|
|
211
|
+
|
|
212
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
213
|
+
|
|
214
|
+
Django の `LOGGING` 設定辞書と `dictConfig` はほぼ同じ形式なので即理解できる。
|
|
215
|
+
`SensitiveFilter` の `record.args` マスクは見落としがちなポイントで評価が高い。
|
|
216
|
+
|
|
217
|
+
**他フレームワークとの差異**: Django の `LOGGING` は `dictConfig` のラッパー。
|
|
218
|
+
FastAPI では自前で `logging.config.dictConfig()` を `lifespan` で呼ぶ必要がある。
|
|
219
|
+
**nene2-python の薄さへの評価**: `structlog` との共存方法を明確にするドキュメントがほしい。
|
|
220
|
+
**本番投入可能性**: `SensitiveFilter` + `RequestIdAdapter` のペアは本番環境でそのまま使える品質。
|
|
221
|
+
|
|
222
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
223
|
+
|
|
224
|
+
`record.args` のマスク実装が適切。ただし `%` フォーマット以外(`{}`形式)への対応が未検討。
|
|
225
|
+
|
|
226
|
+
**コードレビューチェックポイント**:
|
|
227
|
+
- [x] `SensitiveFilter.filter()` が `record.args` も処理しているか — OK
|
|
228
|
+
- [x] `mask_sensitive()` の正規表現が ReDoS リスクを持っていないか — `[^\s,\"'&;]{4,}` は量指定子が単純で爆発しない ✅
|
|
229
|
+
- [ ] `logging.LogRecord.args` が `dict` / `tuple` 以外の型(例: `int`)のとき `isinstance` チェックが抜ける — `else` ブロックなし(稀なケースで問題発生する可能性)
|
|
230
|
+
- [ ] `_LOGGED_EVENTS: list[dict]` がモジュールレベルのグローバル変数 — 複数 `create_app()` 呼び出し時に状態が混在する
|
|
231
|
+
|
|
232
|
+
**チームでの安全な共有パターン**: `SensitiveFilter` を `nene2.log` に組み込み、
|
|
233
|
+
デフォルトで全ロガーに適用される設計が理想。
|
|
234
|
+
**ツール追加の必要性**: `ruff S314` (print statement) は `print()` を禁止するが、
|
|
235
|
+
Python 3 では `print` は関数なので別アプローチ(プリコミットフック等)が必要。
|
|
236
|
+
|
|
237
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
238
|
+
|
|
239
|
+
CLAUDE.md の「`logging` モジュールのみ使用」ポリシーを、実際の使用パターンとして実証した。
|
|
240
|
+
|
|
241
|
+
**ポリシー達成度**: 高
|
|
242
|
+
**「初心者でも安全な API」達成度**: 中(`record.args` マスク忘れのリスクがある)
|
|
243
|
+
**設計上の負債・ドキュメント不足**:
|
|
244
|
+
- `nene2.log` の `structlog` 設定と標準 `logging` の共存について ADR が必要
|
|
245
|
+
- `SensitiveFilter` を `nene2-python` フレームワーク本体に組み込む価値がある
|
|
246
|
+
- `LOGGING_CONFIG` 辞書は `nene2.log` の設定と統一すべき
|
|
247
|
+
|
|
248
|
+
**Follow-up Issue 候補**: `SensitiveFilter` を `nene2.middleware` または `nene2.log` に追加
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Follow-up Issues
|
|
253
|
+
|
|
254
|
+
| 優先度 | タイトル | 種別 |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| 中 | `SensitiveFilter` を `nene2.log` または `nene2.middleware` に追加 | feat |
|
|
257
|
+
| 中 | `structlog` と標準 `logging` の共存 ADR を作成 | docs |
|
|
258
|
+
| 低 | `SensitiveFilter.filter()` の `record.args` が dict/tuple 以外のとき `str` として処理する | fix |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## まとめ
|
|
263
|
+
|
|
264
|
+
FT175 では `logging` モジュールの実践的パターンを実証した。
|
|
265
|
+
`SensitiveFilter` による `record.args` を含む完全マスキング、
|
|
266
|
+
`RequestIdAdapter` によるリクエストスコープのコンテキスト注入、
|
|
267
|
+
`dictConfig` による宣言的設定が核心。
|
|
268
|
+
`record.args` のマスク忘れは実際の本番事故につながるため、
|
|
269
|
+
`SensitiveFilter` を nene2-python フレームワーク本体に取り込むことを検討すべき。
|
|
270
|
+
|
|
271
|
+
次の FT176 は 176 % 3 = 2 → セキュリティ診断なし。176 % 4 = 0 → クラッカーペンテスト実施。
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# FT176: decimal モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: `decimal.Decimal` による精度の高い十進数演算・金融計算・丸め制御
|
|
5
|
+
**セキュリティ診断**: なし(FT177 で実施)
|
|
6
|
+
**クラッカーペンテスト**: **あり**(FT176: 172 + 4 = 176)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
Python 標準ライブラリの `decimal` モジュールを検証する。
|
|
13
|
+
`float` の浮動小数点誤差(`0.1 + 0.2 != 0.3`)を回避し、
|
|
14
|
+
金融計算で必要な「正確な十進数演算」を `Decimal` 型で実装する。
|
|
15
|
+
|
|
16
|
+
このFTで確認する点:
|
|
17
|
+
- `float` と `Decimal` の精度差(`0.1 + 0.2 == 0.3` の違い)
|
|
18
|
+
- `quantize()` による丸めモードの制御(ROUND_HALF_UP, ROUND_HALF_EVEN 等)
|
|
19
|
+
- 税計算・割引計算・割り勘といった金融計算パターン
|
|
20
|
+
- `Infinity`, `NaN`, 空文字列等の不正入力への防御
|
|
21
|
+
- `parse_decimal_safe()` によるバリデーション(`is_finite()` によるInf/NaN ブロック)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 実装したサンプルアプリ
|
|
26
|
+
|
|
27
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft176-decimal/`
|
|
28
|
+
|
|
29
|
+
### 主要機能
|
|
30
|
+
|
|
31
|
+
| 関数/クラス | 概要 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `decimal_add/sub/mul/div(a, b)` | 基本四則演算(ゼロ除算は `None`) |
|
|
34
|
+
| `round_decimal(value, places, mode)` | 指定モードで丸める |
|
|
35
|
+
| `truncate_decimal(value, places)` | `ROUND_FLOOR` で切り捨て |
|
|
36
|
+
| `ceil_decimal(value, places)` | `ROUND_CEILING` で切り上げ |
|
|
37
|
+
| `ROUNDING_MODES` | 6種の丸めモード辞書 |
|
|
38
|
+
| `calculate_tax(price, tax_rate)` | 税計算(ROUND_HALF_UP) |
|
|
39
|
+
| `calculate_discount(price, discount_percent)` | 割引計算 |
|
|
40
|
+
| `split_bill(total, num_people)` | 割り勘(ROUND_CEILING)|
|
|
41
|
+
| `float_precision_demo()` | float vs Decimal 精度比較 |
|
|
42
|
+
| `parse_decimal_safe(value)` | Infinity/NaN/長すぎる文字列をブロック |
|
|
43
|
+
| `is_valid_decimal(value)` | バリデーション bool |
|
|
44
|
+
| `compare_decimals(a, b)` | 大小比較(-1/0/1) |
|
|
45
|
+
|
|
46
|
+
### HTTP エンドポイント
|
|
47
|
+
|
|
48
|
+
| メソッド | パス | 概要 |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| POST | `/decimal/add` | 加算 |
|
|
51
|
+
| POST | `/decimal/sub` | 減算 |
|
|
52
|
+
| POST | `/decimal/mul` | 乗算 |
|
|
53
|
+
| POST | `/decimal/div` | 除算(ゼロ除算 422) |
|
|
54
|
+
| POST | `/decimal/round` | 丸め(truncated, ceiling も返す) |
|
|
55
|
+
| POST | `/decimal/tax` | 税計算 |
|
|
56
|
+
| POST | `/decimal/discount` | 割引計算 |
|
|
57
|
+
| POST | `/decimal/split-bill` | 割り勘 |
|
|
58
|
+
| GET | `/decimal/float-demo` | float 精度比較デモ |
|
|
59
|
+
| GET | `/decimal/validate` | Decimal バリデーション |
|
|
60
|
+
| POST | `/decimal/compare` | 大小比較 |
|
|
61
|
+
| GET | `/decimal/precision` | 現在の計算精度 |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## テスト結果
|
|
66
|
+
|
|
67
|
+
**42 passed**
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
42 passed in 0.37s
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 摩擦ポイント
|
|
76
|
+
|
|
77
|
+
### F-1: `ROUND_HALF_EVEN`(銀行家丸め)の挙動が直感と異なる(深刻度: 低)
|
|
78
|
+
|
|
79
|
+
**事象**: `round_decimal("2.5", 0, "ROUND_HALF_EVEN")` → `"2"`(偶数方向)。
|
|
80
|
+
Python の組み込み `round(2.5)` も `2` を返すが(banker's rounding)、
|
|
81
|
+
多くの現場では「4捨5入」を期待して `ROUND_HALF_UP` を使う。
|
|
82
|
+
|
|
83
|
+
**原因**: `ROUND_HALF_EVEN` は統計的偏りを最小化するため偶数方向に丸める。
|
|
84
|
+
**対応**: ドキュメントに丸めモードの違いを表で説明し、金融計算ではデフォルトを `ROUND_HALF_UP` に設定した。
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 観察点
|
|
89
|
+
|
|
90
|
+
### 観察1: `float` vs `Decimal` の精度差
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
0.1 + 0.2 # 0.30000000000000004
|
|
94
|
+
Decimal("0.1") + Decimal("0.2") # 0.3
|
|
95
|
+
|
|
96
|
+
0.1 + 0.2 == 0.3 # False
|
|
97
|
+
Decimal("0.1") + Decimal("0.2") == Decimal("0.3") # True
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`Decimal` は文字列から初期化する必要がある。`Decimal(0.1)` は float の誤差を引き継ぐ。
|
|
101
|
+
|
|
102
|
+
### 観察2: `quantize()` による金融計算の標準パターン
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
tax = (price * rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`quantize(Decimal("0.01"))` が「小数点以下2桁」を指定する慣用表現。
|
|
109
|
+
`Decimal(10) ** -2` と等価。`quantize` なしで演算すると桁数が増加する。
|
|
110
|
+
|
|
111
|
+
### 観察3: `Decimal("Infinity")` と `is_finite()` の組み合わせ
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
def parse_decimal_safe(value: str) -> Decimal | None:
|
|
115
|
+
result = Decimal(value)
|
|
116
|
+
if not result.is_finite(): # Infinity / -Infinity / NaN を拒否
|
|
117
|
+
return None
|
|
118
|
+
return result
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`Decimal("Infinity")`, `Decimal("NaN")`, `Decimal("sNaN")` は `InvalidOperation` を
|
|
122
|
+
投げずに正常に生成される。`is_finite()` チェックが必要な理由がここにある。
|
|
123
|
+
|
|
124
|
+
### 観察4: `split_bill` の ROUND_CEILING で全員が必ず払える金額に
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# 1000 / 3 = 333.333...
|
|
128
|
+
# ROUND_CEILING で 333.34 に切り上げ → 全員が333.34払うと 1000.02 になるが
|
|
129
|
+
# これは「端数は最初の人が多く払う」設計ではなく「全員同額で超えたら少し多い」設計
|
|
130
|
+
per_person = (total / num_people).quantize(Decimal("0.01"), rounding=ROUND_CEILING)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## nene2-python フレームワークとの統合
|
|
136
|
+
|
|
137
|
+
- `BinaryOpBody`, `RoundBody`, `TaxBody` 等の Pydantic モデルで `max_length=30` を設定
|
|
138
|
+
- `_validate_decimal()` ヘルパーが `parse_decimal_safe()` を呼び出し、不正入力に一貫した 422 を返す
|
|
139
|
+
- セキュリティヘッダーとリクエストIDが全レスポンスに付与されている
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Developer Experience (DX) Review
|
|
144
|
+
|
|
145
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
146
|
+
|
|
147
|
+
`Decimal` のコンストラクターに **文字列**を渡す必要がある点は最初のつまずき。
|
|
148
|
+
|
|
149
|
+
**ドキュメント理解**: `Decimal("0.1")` vs `Decimal(0.1)` の違いを説明する必要がある。
|
|
150
|
+
**事故リスク**: 高。`Decimal(0.1)` で float 誤差を引き継ぐコードを書きがち。
|
|
151
|
+
**規約の使いやすさ**: `parse_decimal_safe()` のファクトリ関数パターンは使いやすい。
|
|
152
|
+
|
|
153
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
154
|
+
|
|
155
|
+
既存コードの `float` を `Decimal` に置換するとき `str()` 経由が必要なことを知らない。
|
|
156
|
+
|
|
157
|
+
**コピペ可能性**: `calculate_tax()` パターンはそのままコピーして使える。
|
|
158
|
+
**拡張時の罠**: `quantize()` の `places` と `Decimal("0.01")` の関係が初見でわかりにくい。
|
|
159
|
+
**セキュリティ的な事故リスク**: 中。負の価格・税率のバリデーション欠如(ペンテストで発見)。
|
|
160
|
+
|
|
161
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
162
|
+
|
|
163
|
+
JS の `number` 型が浮動小数点演算なので、バックエンドが `Decimal` で正確に計算することの重要性を理解できる。
|
|
164
|
+
|
|
165
|
+
**エラーレスポンスの質**: 422 に `field_name` が含まれるため、フロント側のフォームバリデーションと対応しやすい。
|
|
166
|
+
**Python 固有概念の学習コスト**: `quantize` は JS には直接対応物がないが、「N桁に揃える」と説明すればわかる。
|
|
167
|
+
**事故リスク**: 低。HTTP 境界で `max_length` と `is_finite()` が守っている。
|
|
168
|
+
|
|
169
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
170
|
+
|
|
171
|
+
金融システムで `Decimal` は必須。`quantize(ROUND_HALF_UP)` パターンを見れば即理解できる。
|
|
172
|
+
|
|
173
|
+
**他フレームワークとの差異**: Django の `DecimalField` はモデル側で `decimal_places` を指定するが、
|
|
174
|
+
このFTでは演算ごとに `quantize()` を呼ぶ明示的スタイル。どちらも正しい。
|
|
175
|
+
**nene2-python の薄さへの評価**: ドメインロジック(金融計算)が HTTP 層から完全に独立している点が評価できる。
|
|
176
|
+
**本番投入可能性**: ビジネスロジックバリデーション(範囲チェック)を追加すれば本番品質。
|
|
177
|
+
|
|
178
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
179
|
+
|
|
180
|
+
ペンテストで発見されたビジネスロジック欠如(負の価格・100%超割引)は Issue 化が必要。
|
|
181
|
+
|
|
182
|
+
**コードレビューチェックポイント**:
|
|
183
|
+
- [x] `Infinity`, `NaN` が `is_finite()` でブロックされているか — OK
|
|
184
|
+
- [x] ゼロ除算が安全に処理されているか — `decimal_div()` で None 返却 ✅
|
|
185
|
+
- [ ] `calculate_tax()` の `tax_rate` に範囲制限がない — `0 <= tax_rate <= 2` 程度のバリデーションが必要
|
|
186
|
+
- [ ] `calculate_discount()` の `discount_percent` が 0〜100 のチェックがない
|
|
187
|
+
- [ ] Unicode 全角数字(`'123'`)が通過する — Decimal コンストラクターが Unicode digit を受け入れる
|
|
188
|
+
|
|
189
|
+
**チームでの安全な共有パターン**: `parse_decimal_safe()` を必ず経由するルールをチーム内で徹底することが必要。
|
|
190
|
+
**ツール追加の必要性**: ruff には `Decimal(float)` を禁止するルールがないため、コードレビューで手動確認が必要。
|
|
191
|
+
|
|
192
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
193
|
+
|
|
194
|
+
CLAUDE.md の「数値フィールドに `ge` / `le` / `gt` / `lt` 範囲制限があるか」ポリシーに対して、
|
|
195
|
+
`Decimal` 型は文字列で受け取るため Pydantic の数値制限が適用されない点が設計的な空白。
|
|
196
|
+
|
|
197
|
+
**ポリシー達成度**: 中(Pydantic で数値範囲制限できない文字列 Decimal の扱いが未定義)
|
|
198
|
+
**「初心者でも安全な API」達成度**: 中(Infinity/NaN は守られているがビジネスルール違反は通過)
|
|
199
|
+
**設計上の負債**: 文字列 Decimal の範囲バリデーションパターンをフレームワークに追加する必要がある
|
|
200
|
+
**Follow-up Issue 候補**: `Pydantic Annotated` で `DecimalStr` 型エイリアスを定義して範囲制限を組み込む
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## クラッカーペンテスト(FT176: 172 + 4 = 176)
|
|
205
|
+
|
|
206
|
+
> **実施方針**: 金融計算 API は「数値の正確さ」と「ビジネスロジックの整合性」の両面から攻撃できる。
|
|
207
|
+
> クラッカーは価格をマイナスにして不正な返金を引き出したり、税率を異常値にして計算を崩したりする。
|
|
208
|
+
|
|
209
|
+
### フェーズ1: 構造推測(攻撃者の視点)
|
|
210
|
+
|
|
211
|
+
- **OpenAPI から推測できる内部構造**:
|
|
212
|
+
- 全フィールドが `str` 型 → `Decimal(str)` を内部で使っていると推測
|
|
213
|
+
- `max_length=30` → 入力サイズ制限が文字数ベース(桁数ではない)
|
|
214
|
+
- `num_people: int` に `ge=1, le=1000` → Pydantic 数値制限あり
|
|
215
|
+
- `price`, `tax_rate` に数値範囲制限なし → バリデーション欠如の可能性
|
|
216
|
+
|
|
217
|
+
- **攻撃ベクターの仮説**:
|
|
218
|
+
1. `Infinity`, `NaN` を渡してランタイムエラーを引き起こす
|
|
219
|
+
2. 負の価格・100%超の税率でビジネスロジックを崩す
|
|
220
|
+
3. Unicode 文字を数値として送り込む
|
|
221
|
+
4. 科学表記(`1e100`)で予期しない巨大数を計算させる
|
|
222
|
+
5. 精度の高い計算を大量に送ってCPUを枯渇させる
|
|
223
|
+
|
|
224
|
+
### フェーズ2: 攻撃実行ログ
|
|
225
|
+
|
|
226
|
+
#### A. Pydantic バイパス・型強制攻撃
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
a='Infinity': 422 Invalid decimal: a='Infinity' ← ブロック ✅
|
|
230
|
+
a='-Infinity': 422 ← ブロック ✅
|
|
231
|
+
a='NaN': 422 ← ブロック ✅
|
|
232
|
+
a='sNaN': 422 ← ブロック ✅
|
|
233
|
+
a='inf': 422 ← ブロック ✅
|
|
234
|
+
a='-inf': 422 ← ブロック ✅
|
|
235
|
+
a='nan': 422 ← ブロック ✅
|
|
236
|
+
|
|
237
|
+
a='1e10': 200 result=10000000000 ← 通過(有限値として正当)
|
|
238
|
+
a='1E308': 200 result=1.000...E+308 ← 通過(有限値として正当)
|
|
239
|
+
a='1e100': 200 result=1.000...E+100 ← 通過(有限値として正当)
|
|
240
|
+
|
|
241
|
+
a=123 (int type): 422 string_type error ← Pydantic が str を要求 ✅
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**結果**: Infinity/NaN は全7種類ブロック。科学表記は有限値として通過(許容動作)。
|
|
245
|
+
|
|
246
|
+
#### B. ビジネスロジック攻撃
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
tax_rate=2.0 (200%超): 200 tax=2000.00, total=3000.00 ← 突破 ⚠️
|
|
250
|
+
price=-1000 (負の価格): 200 tax=-100.00, total=-1100.00 ← 突破 ⚠️
|
|
251
|
+
discount_percent=-10 (負割引): 200 → 値上がり ← 突破 ⚠️
|
|
252
|
+
discount_percent=150 (100%超): 200 discounted=-500.00 ← 突破 ⚠️
|
|
253
|
+
div by zero: 422 ← ブロック ✅
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**結果**: 数値範囲バリデーションが未実装のため、負の価格・異常税率が通過。
|
|
257
|
+
金融 API として使う場合はビジネスロジックレベルの制約が必要。
|
|
258
|
+
|
|
259
|
+
#### C. 境界値・エッジケース攻撃
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
a="" (空文字): 422 Invalid decimal ← ブロック ✅
|
|
263
|
+
len=30 (上限ちょうど): 200 ← 通過(正常) ✅
|
|
264
|
+
len=31 (上限超え): 422 string_too_long ← Pydantic ブロック ✅
|
|
265
|
+
28桁 all-9s + 1: 200 result=1E+28 ← 通過(正常) ✅
|
|
266
|
+
全角数字 '123' + 1: 200 result='124' ← 通過(予期しない動作)⚠️
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**発見**: Python の `Decimal` コンストラクターは Unicode の全角数字(`'123'`)を受け付け、
|
|
270
|
+
`123` と同じ値として扱う。`parse_decimal_safe()` は `InvalidOperation` が発生しないため通過する。
|
|
271
|
+
金融 API でユーザーが全角数字を入力した場合、期待通りに動作するが、
|
|
272
|
+
入力形式の正規化なしに通過することに開発者が気づいていない可能性がある。
|
|
273
|
+
|
|
274
|
+
#### D. 情報収集攻撃
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
Invalid mode 'HACKED': 422 Unknown rounding mode: 'HACKED' ← 安全なエラーメッセージ ✅
|
|
278
|
+
不正入力のエラー: 内部パス・スタックトレースなし ✅
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**結果**: エラーメッセージは適切に制御されている。
|
|
282
|
+
|
|
283
|
+
#### E. DoS 試み
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
100回 div(1/3): 0.321s (3.2ms/req) ← 正常速度 ✅
|
|
287
|
+
50回 mul(28桁×28桁): 0.163s (3.3ms/req) ← 正常速度 ✅
|
|
288
|
+
攻撃後の精度: 28 (不変) ← グローバル状態汚染なし ✅
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**結果**: `1e100 + 1e100` のような巨大数演算も正常速度。
|
|
292
|
+
`decimal.getcontext().prec` はスレッドローカルなため、攻撃による変更は他スレッドに影響しない。
|
|
293
|
+
|
|
294
|
+
### フェーズ3: 攻撃まとめ
|
|
295
|
+
|
|
296
|
+
| 攻撃カテゴリ | 試みた攻撃数 | 突破 | 耐えた | 予期しない動作 |
|
|
297
|
+
|---|---|---|---|---|
|
|
298
|
+
| Pydantic バイパス(Inf/NaN) | 7 | 0 | 7 | 0 |
|
|
299
|
+
| 型強制(int型フィールド) | 1 | 0 | 1 | 0 |
|
|
300
|
+
| ビジネスロジック(範囲) | 4 | 4 | 0 | 0 |
|
|
301
|
+
| 境界値(長さ・文字種) | 5 | 0 | 4 | 1 |
|
|
302
|
+
| 情報収集(エラー解析) | 2 | 0 | 2 | 0 |
|
|
303
|
+
| DoS(大量・高精度計算) | 3 | 0 | 3 | 0 |
|
|
304
|
+
|
|
305
|
+
**攻撃耐性評価**: 軽微な問題あり(ビジネスロジックバリデーション欠如)
|
|
306
|
+
|
|
307
|
+
**発見した弱点**:
|
|
308
|
+
1. **MEDIUM**: `calculate_tax()`, `calculate_discount()` に価格・税率・割引率の範囲制限なし
|
|
309
|
+
- 負の価格(`-1000`)→ 負の税額
|
|
310
|
+
- 200%の税率(`2.0`)→ 元価格の3倍
|
|
311
|
+
- 150%の割引(`150`)→ マイナス価格
|
|
312
|
+
|
|
313
|
+
2. **LOW**: Unicode 全角数字(`'123'`)が `Decimal` に通過する
|
|
314
|
+
- `parse_decimal_safe` は `is_finite()` で判定するが、Unicode digit は有限値なので通過
|
|
315
|
+
- 機能的には正しく動作するが、予期しない入力形式
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Follow-up Issues
|
|
320
|
+
|
|
321
|
+
| 優先度 | タイトル | 種別 |
|
|
322
|
+
|---|---|---|
|
|
323
|
+
| 高 | `calculate_tax()`, `calculate_discount()` に価格・税率・割引率の範囲バリデーションを追加 | fix |
|
|
324
|
+
| 中 | `parse_decimal_safe()` に ASCII 数字のみ許可するオプションを追加(Unicode digit の予期しない受け入れを防ぐ) | feat |
|
|
325
|
+
| 中 | 文字列 Decimal の `Annotated` 型エイリアス(`PositiveDecimalStr`, `TaxRateStr`)をフレームワークに追加 | feat |
|
|
326
|
+
| 低 | `Decimal(0.1)` を禁止するカスタム ruff ルールの検討 | chore |
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## まとめ
|
|
331
|
+
|
|
332
|
+
FT176 では `decimal.Decimal` による精度の高い金融計算を実装した。
|
|
333
|
+
`float` との精度差(`0.1 + 0.2 != 0.3` 問題)、`quantize()` による丸め制御、
|
|
334
|
+
`is_finite()` による `Infinity`/`NaN` ブロックを確認した。
|
|
335
|
+
|
|
336
|
+
クラッカーペンテストでは Infinity/NaN の全種類が正常にブロックされたが、
|
|
337
|
+
ビジネスロジックレベルのバリデーション(負の価格・100%超の税率)が欠如していることを発見した。
|
|
338
|
+
金融 API では「計算として正しい値」と「ビジネスとして許容できる値」の区別が重要で、
|
|
339
|
+
`parse_decimal_safe()` の技術的バリデーションだけでは不十分であることが確認された。
|
|
340
|
+
|
|
341
|
+
次の FT177 は 177 % 3 = 0 → セキュリティ診断が必要。
|
|
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
|