nene2-python 1.8.44__tar.gz → 1.8.45__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.44 → nene2_python-1.8.45}/PKG-INFO +1 -1
- nene2_python-1.8.45/docs/field-trials/2026-05-field-trial-174.md +436 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/pyproject.toml +1 -1
- {nene2_python-1.8.44 → nene2_python-1.8.45}/.env.example +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/.gitignore +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/AGENTS.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/CHANGELOG.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/CLAUDE.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/Dockerfile +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/LICENSE +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/README.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/alembic/README +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/alembic/env.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/alembic.ini +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/compose.yaml +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/de/index.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-168.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-169.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-170.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-171.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-172.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-173.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/fr/index.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/index.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/index.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/reference/api.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/roadmap.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/todo/current.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/zh/index.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/package-lock.json +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/package.json +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/__main__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/app.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/mcp.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/schema.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/conftest.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/tests/scripts/test_export_openapi.py +0 -0
- {nene2_python-1.8.44 → nene2_python-1.8.45}/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.45
|
|
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,436 @@
|
|
|
1
|
+
# FT174: itertools モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: `itertools` による無限イテレーター・組み合わせ・グルーピング・安全な消費
|
|
5
|
+
**セキュリティ診断**: **あり**(FT174: 174 % 3 = 0)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
Python 標準ライブラリの `itertools` モジュールを検証する。
|
|
12
|
+
無限イテレーター(`count`, `cycle`, `repeat`)、フィルター(`takewhile`, `dropwhile`, `compress`)、
|
|
13
|
+
グルーピング(`groupby`)、組み合わせ論的操作(`combinations`, `permutations`, `product`)、
|
|
14
|
+
累積(`accumulate`)を実装し、特に **組み合わせ爆発と無限ループによる DoS** への防御パターンを確認する。
|
|
15
|
+
|
|
16
|
+
このFTで確認する点:
|
|
17
|
+
- `islice` による無限イテレーターの安全な消費
|
|
18
|
+
- `groupby` の「ソート済み入力が必要」という仕様上の落とし穴
|
|
19
|
+
- `combinations` / `permutations` / `product` の組み合わせ爆発 DoS 防止
|
|
20
|
+
- `MAX_TAKE` / `MAX_COMBO_INPUT` / `MAX_COMBO_OUTPUT` による多層防御
|
|
21
|
+
- Pydantic `ge` / `le` による HTTP 境界でのサニタイズ
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 実装したサンプルアプリ
|
|
26
|
+
|
|
27
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft174-itertools/`
|
|
28
|
+
|
|
29
|
+
### 主要機能
|
|
30
|
+
|
|
31
|
+
| 関数/クラス | 概要 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `take(n, iterable)` | 汎用 islice ラッパー(上限 MAX_TAKE) |
|
|
34
|
+
| `count_range(start, step, limit)` | `itertools.count` から limit 件取り出す |
|
|
35
|
+
| `cycle_values(values, limit)` | `itertools.cycle` で繰り返す(limit 件) |
|
|
36
|
+
| `repeat_value(value, times)` | `itertools.repeat` 有限版 |
|
|
37
|
+
| `chain_lists(*lists)` | 複数リストをフラット結合 |
|
|
38
|
+
| `sliding_window(seq, size)` | スライディングウィンドウ(`pairwise` 活用) |
|
|
39
|
+
| `compress_by_mask(data, mask)` | マスクフィルター |
|
|
40
|
+
| `takewhile_positive(numbers)` | 正の数が続く間だけ取り出す |
|
|
41
|
+
| `dropwhile_negative(numbers)` | 負の数をスキップして以降を取り出す |
|
|
42
|
+
| `group_by_first_char(words)` | 先頭文字でグルーピング |
|
|
43
|
+
| `group_consecutive(numbers)` | 連続整数をグループ化 |
|
|
44
|
+
| `combinations_safe(items, r)` | DoS 防止付き組み合わせ |
|
|
45
|
+
| `permutations_safe(items, r)` | DoS 防止付き順列 |
|
|
46
|
+
| `product_safe(items, repeat)` | DoS 防止付きデカルト積(repeat 上限4) |
|
|
47
|
+
| `running_total(numbers)` | `accumulate` で累積和 |
|
|
48
|
+
| `running_max(numbers)` | `accumulate(max)` で累積最大値 |
|
|
49
|
+
| `pairwise_diffs(numbers)` | `pairwise` で差分リスト |
|
|
50
|
+
| `flatten_once(nested)` | `chain.from_iterable` で1段展開 |
|
|
51
|
+
| `batched_items(items, size)` | バッチ分割 |
|
|
52
|
+
| `roundrobin(*iterables)` | ラウンドロビン消費 |
|
|
53
|
+
|
|
54
|
+
DoS 防止定数:
|
|
55
|
+
|
|
56
|
+
| 定数 | 値 | 用途 |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `MAX_TAKE` | 10,000 | 無限イテレーターからの最大取り出し件数 |
|
|
59
|
+
| `MAX_COMBO_INPUT` | 20 | 組み合わせ系関数への入力リスト上限サイズ |
|
|
60
|
+
| `MAX_COMBO_OUTPUT` | 5,000 | 組み合わせ系関数の出力件数上限(islice) |
|
|
61
|
+
|
|
62
|
+
### HTTP エンドポイント
|
|
63
|
+
|
|
64
|
+
| メソッド | パス | 概要 |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| GET | `/itertools/count` | count イテレーター(limit 付き) |
|
|
67
|
+
| GET | `/itertools/cycle` | cycle イテレーター(limit 付き) |
|
|
68
|
+
| GET | `/itertools/repeat` | repeat(times 付き) |
|
|
69
|
+
| POST | `/itertools/chain` | リスト結合 |
|
|
70
|
+
| POST | `/itertools/compress` | マスクフィルター |
|
|
71
|
+
| GET | `/itertools/takewhile` | 正の数フィルター |
|
|
72
|
+
| GET | `/itertools/dropwhile` | 負の数スキップ |
|
|
73
|
+
| POST | `/itertools/group-words` | 先頭文字グルーピング |
|
|
74
|
+
| POST | `/itertools/group-consecutive` | 連続整数グルーピング |
|
|
75
|
+
| POST | `/itertools/combinations` | 組み合わせ |
|
|
76
|
+
| POST | `/itertools/permutations` | 順列 |
|
|
77
|
+
| POST | `/itertools/product` | デカルト積 |
|
|
78
|
+
| POST | `/itertools/accumulate` | 累積和・累積最大値 |
|
|
79
|
+
| POST | `/itertools/pairwise-diffs` | 差分リスト |
|
|
80
|
+
| POST | `/itertools/flatten` | 1段展開 |
|
|
81
|
+
| POST | `/itertools/batch` | バッチ分割 |
|
|
82
|
+
| POST | `/itertools/roundrobin` | ラウンドロビン |
|
|
83
|
+
| GET | `/itertools/limits` | DoS 防止定数の公開 |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## テスト結果
|
|
88
|
+
|
|
89
|
+
**50 passed**
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
50 passed in 0.49s
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 摩擦ポイント
|
|
98
|
+
|
|
99
|
+
### F-1: `itertools.groupby` はソート済み入力が必要(深刻度: 中)
|
|
100
|
+
|
|
101
|
+
**事象**: `groupby(["banana", "apple", "cherry"], key=lambda w: w[0])` は3グループを返すが、
|
|
102
|
+
`groupby(["apple", "banana", "apple"], key=lambda w: w[0])` は `"apple"` が2グループになる。
|
|
103
|
+
|
|
104
|
+
**原因**: `groupby` は「連続する同じキー」でグループを作る。ソートしていないと断片化する。
|
|
105
|
+
|
|
106
|
+
**対応**: `group_by_first_char()` の実装で `sorted(words)` を前処理として追加。
|
|
107
|
+
ドキュメントに「ソート済み前提」を明記した。
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 観察点
|
|
112
|
+
|
|
113
|
+
### 観察1: `islice` による多層防御パターン
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
MAX_TAKE = 10_000
|
|
117
|
+
MAX_COMBO_INPUT = 20
|
|
118
|
+
MAX_COMBO_OUTPUT = 5_000
|
|
119
|
+
|
|
120
|
+
def combinations_safe(items: list[str], r: int) -> list[tuple[str, ...]]:
|
|
121
|
+
items = items[:MAX_COMBO_INPUT] # レイヤー1: 入力サイズ制限
|
|
122
|
+
r = max(1, min(r, len(items))) # レイヤー2: r のクランプ
|
|
123
|
+
return list(itertools.islice( # レイヤー3: 出力件数制限
|
|
124
|
+
itertools.combinations(items, r), MAX_COMBO_OUTPUT
|
|
125
|
+
))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
HTTP 境界では Pydantic が `le=MAX_COMBO_INPUT` でも弾く(4層目)。
|
|
129
|
+
`P(20,10) = 670,442,572,800` という天文学的順列数が 5,000 件・7ms で完了するのは
|
|
130
|
+
`islice` がジェネレーターを遅延評価するから。全件生成せずに先頭だけ取る。
|
|
131
|
+
|
|
132
|
+
### 観察2: `itertools.pairwise` (Python 3.10+)
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
def pairwise_diffs(numbers: list[int]) -> list[int]:
|
|
136
|
+
return [b - a for a, b in itertools.pairwise(numbers)]
|
|
137
|
+
# [1, 4, 9, 16] → [3, 5, 7]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Python 3.10 で追加された `pairwise` は、スライディングウィンドウ size=2 の標準実装。
|
|
141
|
+
`zip(lst, lst[1:])` の冗長な書き方が不要になった。
|
|
142
|
+
|
|
143
|
+
### 観察3: `group_consecutive` の enumeration トリック
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
def group_consecutive(numbers: list[int]) -> list[list[int]]:
|
|
147
|
+
for _, group in itertools.groupby(enumerate(numbers), key=lambda t: t[1] - t[0]):
|
|
148
|
+
result.append([v for _, v in group])
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`enumerate([1, 2, 3, 7, 8])` → `[(0,1),(1,2),(2,3),(3,7),(4,8)]`
|
|
152
|
+
`value - index` = `[1, 1, 1, 4, 4]` → 同じ値が連続する = 連続した整数
|
|
153
|
+
|
|
154
|
+
### 観察4: `chain_lists(*lists)` の `*` アンパック
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
def chain_lists(*lists: list[str]) -> list[str]:
|
|
158
|
+
return list(itertools.chain(*lists))
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`itertools.chain` は複数のイテラブルを引数に取る。
|
|
162
|
+
`itertools.chain.from_iterable(lists)` と等価だが、
|
|
163
|
+
`chain(*lists)` の方が意図が明確。`*lists` のアンパックがリストのリストを展開する。
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## nene2-python フレームワークとの統合
|
|
168
|
+
|
|
169
|
+
- `nene2.middleware` の3ミドルウェアを LIFO 順序で追加、`test_security_headers` でヘッダー確認
|
|
170
|
+
- 全 POST エンドポイントで Pydantic モデルに `max_length` を設定(リスト要素数の上限)
|
|
171
|
+
- `get_cycle` の `values: list[str] = Query(max_length=50)` は Query パラメーターのリスト制限
|
|
172
|
+
- 定数(`MAX_TAKE`, `MAX_COMBO_INPUT`, `MAX_COMBO_OUTPUT`)を `/itertools/limits` で公開し、クライアントが安全な入力サイズを事前確認できるよう設計
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Developer Experience (DX) Review
|
|
177
|
+
|
|
178
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
179
|
+
|
|
180
|
+
`itertools` はドキュメントが豊富で、各関数の用途は例が見れば理解できる。
|
|
181
|
+
ただし「なぜ `groupby` にソートが必要なのか」は一度ハマらないと気づけない。
|
|
182
|
+
|
|
183
|
+
**ドキュメント理解**: `islice` の「n件で切り取る」という役割は直感的。
|
|
184
|
+
`accumulate` の `func` 引数(第2引数に `max` を渡せること)はドキュメントを読まないと気づかない。
|
|
185
|
+
**事故リスク**: 高。`itertools.cycle` を `islice` なしで呼ぶと無限ループになる。
|
|
186
|
+
このFTでは `MAX_TAKE` による安全なラッパーを提供しているが、
|
|
187
|
+
直接 `cycle` を使う初心者は必ずハマる。
|
|
188
|
+
**規約の使いやすさ**: `combinations_safe()` のような命名で「Safe=islice 制限付き」を
|
|
189
|
+
明示するパターンはわかりやすい。
|
|
190
|
+
|
|
191
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
192
|
+
|
|
193
|
+
`itertools` を「なんとなく chain だけ使う」スタイルの人には、
|
|
194
|
+
このFTの `MAX_COMBO_INPUT` / `MAX_COMBO_OUTPUT` の設計は学習材料になる。
|
|
195
|
+
|
|
196
|
+
**コピペ可能性**: `combinations_safe()` の3層防御パターン(入力制限・r クランプ・islice)は
|
|
197
|
+
コピペで正しく動く。
|
|
198
|
+
**拡張時の罠**: 「もっとたくさん返してほしい」と言われて `MAX_COMBO_OUTPUT` を増やすと
|
|
199
|
+
大きな `r` で応答時間が急増する(二項係数は急速に増大)。
|
|
200
|
+
**セキュリティ的な事故リスク**: 中。直接 `combinations(range(1000), 100)` を書けば
|
|
201
|
+
サーバーがハングする。安全なラッパーなしの実装は危険。
|
|
202
|
+
|
|
203
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
204
|
+
|
|
205
|
+
`/itertools/limits` エンドポイントで制限値を公開する設計は API クライアント視点で親切。
|
|
206
|
+
「送れる最大サイズ」を事前に取得して UI 側でバリデーションできる。
|
|
207
|
+
|
|
208
|
+
**エラーレスポンスの質**: 422 時に Pydantic が `loc`, `msg`, `type` を返すため
|
|
209
|
+
クライアントはどのフィールドが問題かを識別できる。
|
|
210
|
+
**Python 固有概念の学習コスト**: ジェネレーター・遅延評価の概念は Node.js の
|
|
211
|
+
Generator とほぼ対応するため理解しやすい。`islice` はほぼ `Array.from({length: n}, ...)` と同等。
|
|
212
|
+
**事故リスク**: 低。HTTP 境界の制限が Pydantic + 定数で二重に守られている。
|
|
213
|
+
|
|
214
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
215
|
+
|
|
216
|
+
`itertools` の組み合わせ爆発 DoS は過去に本番事故を見たことがある人なら即座に理解できる。
|
|
217
|
+
多層防御(Pydantic + クランプ + islice)の設計は好意的に評価されるはず。
|
|
218
|
+
|
|
219
|
+
**他フレームワークとの差異**: Django の `Paginator` は内部で全件 count するが、
|
|
220
|
+
`islice` は遅延評価なので計算量が O(output_count) で済む点が優れている。
|
|
221
|
+
**nene2-python の薄さへの評価**: `MAX_TAKE`, `MAX_COMBO_INPUT` を定数として明示し、
|
|
222
|
+
`/limits` で公開する設計は「薄い HTTP 層でドメインロジックを分離」の良い例。
|
|
223
|
+
**本番投入可能性**: チームで `combinations` を使う際は必ず `combinations_safe()` 経由を
|
|
224
|
+
コーディング規約に追加することを推奨する。
|
|
225
|
+
|
|
226
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
227
|
+
|
|
228
|
+
組み合わせ爆発 DoS の多層防御は適切。ただし `roundrobin()` の実装に気になる点がある。
|
|
229
|
+
|
|
230
|
+
**コードレビューチェックポイント**:
|
|
231
|
+
- [x] `itertools.cycle()` が必ず `islice` でラップされているか — `cycle_values()` で確認
|
|
232
|
+
- [x] `combinations`, `permutations`, `product` に入力・出力の両方に上限があるか — OK
|
|
233
|
+
- [ ] `roundrobin()` が `while nexts` ループで動作しているが、全イテレーターが空のとき正しく終了するか — `StopIteration` 処理が正しい
|
|
234
|
+
- [ ] 文字列要素の max_length が `list[str]` の要素レベルで制限されていない — `max_length=1000` はリスト長の制限であり、各文字列の長さは無制限
|
|
235
|
+
|
|
236
|
+
**チームでの安全な共有パターン**: `_safe` サフィックスの命名規約でラッパー関数を明示するパターンはチーム内で徹底しやすい。
|
|
237
|
+
**ツール追加の必要性**: `itertools` の無限イテレーター直接使用を ruff で禁止する設定は標準では存在しないため、チーム規約として文書化が必要。
|
|
238
|
+
|
|
239
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
240
|
+
|
|
241
|
+
CLAUDE.md の「文字列フィールドには長さ制限」ポリシーについて、
|
|
242
|
+
`list[str]` の**要素文字列**のサイズ制限が未実装。リスト長は `max_length` で制限しているが、
|
|
243
|
+
各要素が 1MB の文字列でも通過してしまう。
|
|
244
|
+
|
|
245
|
+
**ポリシー達成度**: 中(リスト要素の文字列長が未制限)
|
|
246
|
+
**「初心者でも安全な API」達成度**: 高(`combinations_safe` / `cycle_values` のラッパーが防衛線)
|
|
247
|
+
**設計上の負債・ドキュメント不足**: `list[str]` 要素の `max_length` 制限方法が
|
|
248
|
+
Pydantic v2 では `Annotated[list[Annotated[str, Field(max_length=200)]], Field(max_length=1000)]`
|
|
249
|
+
という冗長な構文になるため、プロジェクト共通の型エイリアス定義が必要
|
|
250
|
+
**Follow-up Issue 候補**: `list[str]` 要素レベルの文字列長制限パターンをフレームワークに追加
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## セキュリティ診断(FT174: 174 % 3 = 0)
|
|
255
|
+
|
|
256
|
+
> **診断方針**: 組み合わせ爆発・無限ループ・型強制による DoS を主要な攻撃ベクターとして検証する。
|
|
257
|
+
> itertools は純粋計算のみでファイル・DB・外部通信を行わないため、
|
|
258
|
+
> SQLi / SSRF / パストラバーサルは対象外。
|
|
259
|
+
|
|
260
|
+
### 1. OWASP API Security Top 10 (2023)
|
|
261
|
+
|
|
262
|
+
#### API1: オブジェクトレベルの認可不備 (BOLA / IDOR)
|
|
263
|
+
- ✅ 対象外: このFTはステートレス計算のみ。ユーザー固有リソースなし
|
|
264
|
+
|
|
265
|
+
#### API2: 認証の破損 (Broken Authentication)
|
|
266
|
+
- ✅ 対象外: 認証不要のサンドボックス
|
|
267
|
+
|
|
268
|
+
#### API3: オブジェクトプロパティレベルの認可不備 (Mass Assignment)
|
|
269
|
+
- [x] extra フィールド(`"extra": "injected"`)を POST しても無視される
|
|
270
|
+
- **結果**: ✅ Pydantic がデフォルトで追加フィールドを無視。内部モデルに漏洩なし
|
|
271
|
+
- **注記**: `model_config = ConfigDict(extra="forbid")` を追加すれば 422 で明示的に拒否可能
|
|
272
|
+
|
|
273
|
+
#### API4: 無制限リソース消費 (Unrestricted Resource Consumption)
|
|
274
|
+
実際に攻撃的な値を試験した:
|
|
275
|
+
```
|
|
276
|
+
GET /itertools/count?limit=99999 → 422 (le=MAX_TAKE=10000)
|
|
277
|
+
POST /product {"items": ["a","b"], "repeat": 100} → 422 (le=4)
|
|
278
|
+
POST /combinations {"items": 20個, "r": 10} C(20,10)=184756
|
|
279
|
+
→ count: 5000 (islice 上限), elapsed: 7ms
|
|
280
|
+
POST /permutations {"items": 20個, "r": 10} P(20,10)=670,442,572,800
|
|
281
|
+
→ count: 5000 (islice 上限), elapsed: 7ms ← 遅延評価が機能
|
|
282
|
+
```
|
|
283
|
+
- **結果**: ✅ 3層防御(Pydantic `le` → 入力クランプ → `islice`)が有効。670兆通りの順列も7msで返す
|
|
284
|
+
|
|
285
|
+
#### API5: 機能レベルの認可不備 (Broken Function Level Authorization)
|
|
286
|
+
- ✅ 対象外: 管理者エンドポイントなし
|
|
287
|
+
|
|
288
|
+
#### API6: SSRF
|
|
289
|
+
- ✅ 対象外: URL を受け取るエンドポイントなし
|
|
290
|
+
|
|
291
|
+
#### API7: セキュリティの設定ミス
|
|
292
|
+
- [x] `SecurityHeadersMiddleware` が全レスポンスに `x-content-type-options: nosniff`, `x-frame-options: DENY` を付与
|
|
293
|
+
- [x] `RequestIdMiddleware` が `x-request-id` を付与
|
|
294
|
+
- [x] 422 エラー時にスタックトレース非公開を確認
|
|
295
|
+
- **結果**: ✅ 全通過
|
|
296
|
+
|
|
297
|
+
#### API8〜API10
|
|
298
|
+
- ✅ 対象外または適用なし
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### 2. インジェクション攻撃
|
|
303
|
+
|
|
304
|
+
#### SQL インジェクション
|
|
305
|
+
- ✅ 対象外: DB 操作なし
|
|
306
|
+
|
|
307
|
+
#### コマンドインジェクション
|
|
308
|
+
- ✅ 対象外: `subprocess`, `os.system` 使用なし。`itertools` は純粋 Python
|
|
309
|
+
|
|
310
|
+
#### パストラバーサル
|
|
311
|
+
- ✅ 対象外: ファイル操作なし
|
|
312
|
+
|
|
313
|
+
#### SSTI
|
|
314
|
+
- ✅ 対象外: テンプレートエンジン使用なし
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### 3. 認証・認可
|
|
319
|
+
- ✅ 対象外: 認証不要のサンドボックス
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
### 4. 入力バリデーション
|
|
324
|
+
|
|
325
|
+
テスト結果:
|
|
326
|
+
```python
|
|
327
|
+
# Pydantic 型強制テスト
|
|
328
|
+
r="2" (string → int): 200, count=3 → 文字列がintに変換される(Pydantic v2 laxモード)
|
|
329
|
+
r=1.9 (float → int): 422 → 小数点以下がある float は拒否 ✅
|
|
330
|
+
r=-1 (negative int): 422 → ge=1 で拒否 ✅
|
|
331
|
+
batch_size=0: 422 → ge=1 で拒否 ✅
|
|
332
|
+
limit=99999: 422 → le=10000 で拒否 ✅
|
|
333
|
+
```
|
|
334
|
+
- **発見**: `r="2"` (文字列 "2") は Pydantic v2 lax モードで `int` に変換されて通過する。
|
|
335
|
+
これは許容動作(JSON で数値を文字列として送る慣習があるため)だが、
|
|
336
|
+
`ConfigDict(strict=True)` を使えば明示的に拒否できる。
|
|
337
|
+
- **ヌルバイト**: `"\x00evil"` は計算処理のみなので通過。DB 書き込みや OS 処理がないため無害。
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
### 5. 情報漏洩
|
|
342
|
+
|
|
343
|
+
- [x] `limit=notanint` → 422、Pydantic エラー詳細のみ返却、スタックトレースなし
|
|
344
|
+
- [x] セキュリティヘッダーに `Server:` ヘッダーなし
|
|
345
|
+
- **結果**: ✅ 安全
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
### 6. Python / FastAPI 固有の攻撃ベクター
|
|
350
|
+
|
|
351
|
+
#### 組み合わせ爆発 DoS(itertools 固有)
|
|
352
|
+
itertools は数学的に爆発するリソース消費の源泉になりえる:
|
|
353
|
+
```python
|
|
354
|
+
# 保護なし実装での理論的影響:
|
|
355
|
+
list(itertools.combinations(range(1000), 100)) # 天文学的 → メモリ枯渇
|
|
356
|
+
list(itertools.permutations(range(20))) # 20! = 2.43京 → メモリ・CPU 枯渇
|
|
357
|
+
list(itertools.cycle([1])) # 無限ループ → プロセス停止
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**保護結果**:
|
|
361
|
+
- `islice` による遅延評価で天文学的組み合わせを O(output_count) で処理 ✅
|
|
362
|
+
- Pydantic `le` + クランプ + `islice` の3層防御 ✅
|
|
363
|
+
- HTTP 境界でのリスト長制限(`max_length=20`) ✅
|
|
364
|
+
|
|
365
|
+
#### ReDoS
|
|
366
|
+
- ✅ 対象外: 正規表現使用なし
|
|
367
|
+
|
|
368
|
+
#### pickle / yaml / marshal インジェクション
|
|
369
|
+
- ✅ 対象外: シリアライゼーション処理なし
|
|
370
|
+
|
|
371
|
+
#### 非同期レースコンディション
|
|
372
|
+
- ✅ 対象外: 非同期処理・グローバル状態共有なし
|
|
373
|
+
|
|
374
|
+
#### 型強制攻撃(Pydantic Type Coercion)
|
|
375
|
+
- `r="2"` → `int(2)`: lax モードで変換 ⚠️ 許容動作だが `strict=True` で厳格化可能
|
|
376
|
+
- `r=1.9` → `422`: 小数点付き float は int フィールドで拒否 ✅
|
|
377
|
+
- **結論**: 許容できる型強制のみ。数値的に有効な範囲なので悪用不可
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
### 7. 依存関係の脆弱性スキャン
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
uv run pip-audit
|
|
385
|
+
Found 1 known vulnerability in 1 package
|
|
386
|
+
Name Version ID Fix Versions
|
|
387
|
+
pyjwt 2.12.1 PYSEC-2025-183 (なし)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- **PYSEC-2025-183**: PyJWT の既知 CVE。`mcp>=1.0` の推移的依存(FT165 で調査済み)。
|
|
391
|
+
直接依存として追加していないため修正不可。mcp のアップデートを待つ。
|
|
392
|
+
- **スキャン結果**: CRITICAL: 0件 / HIGH: 0件 / MEDIUM: 0件 / LOW: 1件(対応待ち)
|
|
393
|
+
- **対応方針**: 許容(mcp の更新を待つ。このFTは JWT を直接使用しない)
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
### 診断サマリー
|
|
398
|
+
|
|
399
|
+
| カテゴリ | 結果 | 最重要発見 |
|
|
400
|
+
|---|---|---|
|
|
401
|
+
| OWASP API Security Top 10 | ✅ 全通過 | API4: 3層防御が有効(C/P 爆発も7msで処理) |
|
|
402
|
+
| SQL インジェクション | ✅ 対象外 | - |
|
|
403
|
+
| コマンドインジェクション | ✅ 対象外 | - |
|
|
404
|
+
| パストラバーサル | ✅ 対象外 | - |
|
|
405
|
+
| SSTI | ✅ 対象外 | - |
|
|
406
|
+
| 認証・認可 | ✅ 対象外 | - |
|
|
407
|
+
| 入力バリデーション | ⚠️ 軽微 | `r="2"` が lax モードで通過(無害) |
|
|
408
|
+
| 情報漏洩 | ✅ 全通過 | スタックトレース非公開 |
|
|
409
|
+
| 組み合わせ爆発 DoS | ✅ 全防御 | P(20,10)=670兆通りも5000件・7ms |
|
|
410
|
+
| 型強制攻撃 | ⚠️ 軽微 | float→int(整数部のみ)は許容。`le`で範囲制限済み |
|
|
411
|
+
| 依存関係 CVE | ⚠️ 対応待ち | PYSEC-2025-183 (PyJWT, mcp の推移的依存) |
|
|
412
|
+
|
|
413
|
+
**総合評価**: 合格(組み合わせ爆発 DoS 防御が適切に実装されている)
|
|
414
|
+
**発見した脆弱性**: 0件(CRITICAL/HIGH/MEDIUM なし)
|
|
415
|
+
**軽微な指摘**: 2件(型強制・要素文字列長未制限)
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Follow-up Issues
|
|
420
|
+
|
|
421
|
+
| 優先度 | タイトル | 種別 |
|
|
422
|
+
|---|---|---|
|
|
423
|
+
| 中 | `list[str]` 要素レベルの文字列長制限パターンをフレームワークに追加(`Annotated` 型エイリアス) | feat |
|
|
424
|
+
| 低 | `groupby` の「ソート済み前提」をドキュメント / 型システムで強制する方法を検討 | docs |
|
|
425
|
+
| 低 | PYSEC-2025-183 (PyJWT): mcp の更新を監視する | security |
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## まとめ
|
|
430
|
+
|
|
431
|
+
FT174 では `itertools` の主要関数を実装し、組み合わせ爆発 DoS への多層防御パターン(Pydantic `le` → 入力クランプ → `islice` 遅延評価)が有効であることを実証した。
|
|
432
|
+
P(20,10) = 670兆通りの順列を7ms・5000件で安全に返せることを確認。
|
|
433
|
+
`itertools.cycle` / `count` / `repeat` の無限イテレーターは `islice` ラッパーなしでは
|
|
434
|
+
即座に DoS になるため、チーム規約として `_safe` サフィックスパターンの徹底が重要。
|
|
435
|
+
|
|
436
|
+
次の FT175 は 175 % 3 = 1 → セキュリティ診断なし。175 % 4 ≠ 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
|
|
File without changes
|
{nene2_python-1.8.44 → nene2_python-1.8.45}/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
|