nene2-python 1.8.58__tar.gz → 1.8.60__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.58 → nene2_python-1.8.60}/PKG-INFO +1 -1
- nene2_python-1.8.60/docs/field-trials/2026-05-field-trial-188.md +434 -0
- nene2_python-1.8.60/docs/field-trials/2026-05-field-trial-189.md +485 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/INDEX.md +6 -4
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/todo/current.md +8 -5
- {nene2_python-1.8.58 → nene2_python-1.8.60}/pyproject.toml +1 -1
- {nene2_python-1.8.58 → nene2_python-1.8.60}/uv.lock +44 -1
- {nene2_python-1.8.58 → nene2_python-1.8.60}/.env.example +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/.gitignore +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/AGENTS.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/CHANGELOG.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/CLAUDE.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/Dockerfile +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/LICENSE +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/README.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/alembic/README +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/alembic/env.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/alembic.ini +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/compose.yaml +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/de/index.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-168.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-169.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-170.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-171.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-172.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-173.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-174.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-175.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-176.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-177.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-178.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-179.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-180.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-181.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-182.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-183.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-184.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-185.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-186.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-187.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/fr/index.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/index.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/index.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/reference/api.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/roadmap.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/zh/index.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/package-lock.json +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/package.json +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/__main__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/app.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/mcp.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/schema.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/conftest.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.58 → nene2_python-1.8.60}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.60
|
|
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,434 @@
|
|
|
1
|
+
# FT188: threading モジュール — Thread・Lock・RLock・Semaphore・Event・ThreadPoolExecutor・Queue・Timer
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: threading モジュールの主要プリミティブとスレッドセーフパターンを FastAPI サンドボックスで検証
|
|
5
|
+
**セキュリティ診断**: なし(188 % 3 = 2)
|
|
6
|
+
**クラッカーペンテスト**: **あり**(188 % 4 = 0)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
`threading` モジュールの基本プリミティブ(`Lock`・`RLock`・`Semaphore`・`Event`)から
|
|
13
|
+
高レベルの `ThreadPoolExecutor`・`queue.Queue`・`threading.local`・`threading.Timer` まで、
|
|
14
|
+
スレッドセーフ実装パターンを一通り検証する。
|
|
15
|
+
スレッドセーフティに直結する競合状態・デッドロック・DoS を特に重点的に確認し、
|
|
16
|
+
クラッカーペンテストで実際に攻撃ペイロードを送り込んで耐性を評価する。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 実装したサンプルアプリ
|
|
21
|
+
|
|
22
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft188-threading/`
|
|
23
|
+
|
|
24
|
+
### 主要機能
|
|
25
|
+
|
|
26
|
+
| 関数/クラス | 概要 |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `ThreadSafeCounter` | `Lock` でスレッドセーフにしたカウンター |
|
|
29
|
+
| `parallel_increment()` | 複数スレッドからカウンターをインクリメント |
|
|
30
|
+
| `TreeNode` | `RLock` を使ったスレッドセーフなツリーノード(再入可能) |
|
|
31
|
+
| `run_with_semaphore()` | `Semaphore` で同時実行数を制限してタスク実行 |
|
|
32
|
+
| `run_with_event_sync()` | `Event` でスレッド間「準備完了」「完了」を同期 |
|
|
33
|
+
| `run_tasks_in_pool()` | `ThreadPoolExecutor` + `as_completed()` で並列処理・例外隔離 |
|
|
34
|
+
| `producer_consumer()` | `queue.Queue` によるプロデューサー-コンシューマーパターン |
|
|
35
|
+
| `run_with_thread_context()` | `threading.local` のスレッドローカル分離確認 |
|
|
36
|
+
| `run_delayed()` | `threading.Timer` で遅延実行 |
|
|
37
|
+
|
|
38
|
+
### HTTP エンドポイント
|
|
39
|
+
|
|
40
|
+
| メソッド | パス | 概要 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| POST | `/counter/increment` | Lock ベースカウンター並列インクリメント |
|
|
43
|
+
| POST | `/semaphore/run` | Semaphore で同時実行数制限 |
|
|
44
|
+
| POST | `/event/sync` | Event によるスレッド間同期 |
|
|
45
|
+
| POST | `/pool/run` | ThreadPoolExecutor で並列処理 |
|
|
46
|
+
| POST | `/producer-consumer/run` | Queue プロデューサー-コンシューマー |
|
|
47
|
+
| POST | `/thread-local/run` | threading.local 分離検証 |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## テスト結果
|
|
52
|
+
|
|
53
|
+
**43 passed**
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
43 passed in 0.37s
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 摩擦ポイント
|
|
62
|
+
|
|
63
|
+
### F-1: `threading.Thread(target=lambda)` で `None` 返却の型エラー(深刻度: 低)
|
|
64
|
+
|
|
65
|
+
**事象**: スレッドのターゲット関数をインラインラムダで書こうとしたとき、
|
|
66
|
+
`lambda: [counter.increment() for _ in range(count)]` が
|
|
67
|
+
`"increment" of "ThreadSafeCounter" does not return a value [func-returns-value]`
|
|
68
|
+
エラーを mypy --strict が報告する。
|
|
69
|
+
|
|
70
|
+
**原因**: `increment()` の戻り値型が `None` であるため、リスト内包表記が `list[None]` を返す。
|
|
71
|
+
`threading.Thread(target=...)` は `Callable[[], None]` を期待するが、
|
|
72
|
+
`lambda` が `list[None]` を返す関数と推論され型が合わない。
|
|
73
|
+
|
|
74
|
+
**対応**: ターゲット関数を名前付き関数として外部に定義することで解決。
|
|
75
|
+
`lambda` でのワンライナーはスレッドターゲットに不向きなケースがある。
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
def _increment_worker(counter: ThreadSafeCounter, count: int) -> None:
|
|
79
|
+
for _ in range(count):
|
|
80
|
+
counter.increment()
|
|
81
|
+
|
|
82
|
+
threading.Thread(target=_increment_worker, args=(counter, count))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### F-2: `threading.local` の `.get()` が `Any` を返す(深刻度: 低)
|
|
86
|
+
|
|
87
|
+
**事象**: `_thread_local.context.get(key)` が `Any` 型を返し、
|
|
88
|
+
mypy --strict の `no-any-return` でエラーになる。
|
|
89
|
+
|
|
90
|
+
**原因**: `threading.local` はスレッドごとに任意の属性を持てる設計のため、
|
|
91
|
+
型スタブが `Any` を返すように定義されている。
|
|
92
|
+
|
|
93
|
+
**対応**: 明示的に `str()` キャストして型を確定させる。
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
value = _thread_local.context.get(key)
|
|
97
|
+
return str(value) if value is not None else None
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 観察点
|
|
103
|
+
|
|
104
|
+
### 観察1: `Lock` と `RLock` の使い分け
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# Lock — 同一スレッドから2回取得するとデッドロック
|
|
108
|
+
self._lock = threading.Lock()
|
|
109
|
+
with self._lock:
|
|
110
|
+
total = self.value
|
|
111
|
+
for child in self.children:
|
|
112
|
+
total += child.sum_recursive() # ← 再帰内で再び _lock を取得 → デッドロック
|
|
113
|
+
|
|
114
|
+
# RLock — 同一スレッドから複数回取得可能(再入カウンタを持つ)
|
|
115
|
+
self._lock = threading.RLock()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`TreeNode.sum_recursive()` のような再帰ロックが必要な場面では `RLock` が必須。
|
|
119
|
+
`Lock` は単純な値の保護に使い、再入が必要な場合のみ `RLock` に昇格させる。
|
|
120
|
+
|
|
121
|
+
### 観察2: `ThreadPoolExecutor` の例外隔離パターン
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
for future in as_completed(future_to_item, timeout=timeout):
|
|
125
|
+
item = future_to_item[future]
|
|
126
|
+
exc = future.exception()
|
|
127
|
+
if exc is not None:
|
|
128
|
+
failed.append(item)
|
|
129
|
+
else:
|
|
130
|
+
succeeded.append(future.result())
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`as_completed()` は例外を隠蔽せず `Future.exception()` で参照できる。
|
|
134
|
+
`future.result()` を直接呼ぶと例外が再送出されるため、
|
|
135
|
+
`future.exception()` で先に確認するパターンが安全。
|
|
136
|
+
|
|
137
|
+
### 観察3: プロデューサー-コンシューマーの `None` センチネル戦略
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# 終了シグナルをコンシューマー数と同じだけ投入
|
|
141
|
+
for _ in range(num_consumers):
|
|
142
|
+
work_queue.put(None)
|
|
143
|
+
|
|
144
|
+
# 各コンシューマーは None を受けとったら終了
|
|
145
|
+
while True:
|
|
146
|
+
item = work_queue.get()
|
|
147
|
+
if item is None:
|
|
148
|
+
work_queue.task_done()
|
|
149
|
+
break
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`None` センチネルをコンシューマー数分投入することで、
|
|
153
|
+
すべてのコンシューマーが確実に終了する。
|
|
154
|
+
`work_queue.join()` との組み合わせでプロデューサー側がキュー空になるまで待機できる。
|
|
155
|
+
|
|
156
|
+
### 観察4: DoS 防御の二重バリア
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# demos.py 側(ドメイン層)
|
|
160
|
+
MAX_TASKS = 100
|
|
161
|
+
|
|
162
|
+
def run_tasks_in_pool(items, ...) -> PoolResult:
|
|
163
|
+
if len(items) > MAX_TASKS:
|
|
164
|
+
raise ValueError(f"Too many tasks: {len(items)} > {MAX_TASKS}")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# app.py 側(HTTP 境界層)
|
|
169
|
+
class PoolRequest(BaseModel):
|
|
170
|
+
items: list[str] = Field(max_length=MAX_TASKS_LIMIT, ...) # Pydantic で先に弾く
|
|
171
|
+
max_workers: int = Field(default=4, ge=1, le=MAX_WORKERS_LIMIT, ...)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Pydantic の `max_length` が HTTP 境界で弾くが、
|
|
175
|
+
`demos.py` 側も独立してチェックすることで、
|
|
176
|
+
HTTP を迂回して直接呼ばれた場合も保護される二重バリアになっている。
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## nene2-python フレームワークとの統合
|
|
181
|
+
|
|
182
|
+
- `ThreadSafeCounter` のような状態保持オブジェクトはリクエストごとに生成する。
|
|
183
|
+
`ThreadPoolExecutor` を使うエンドポイントも同様。FastAPI のシングルトン DI とは原則分離する。
|
|
184
|
+
- `ThreadPoolExecutor` は FastAPI の非同期ループとは独立したスレッドプールを使う。
|
|
185
|
+
`asyncio.get_event_loop().run_in_executor()` とは別物であることに注意。
|
|
186
|
+
- `threading.local` はリクエストをまたぐグローバルオブジェクトに使う場合、
|
|
187
|
+
スレッドプールの再利用によって前のリクエストのコンテキストが残る可能性があるため注意が必要。
|
|
188
|
+
FastAPI の `Depends()` や `BackgroundTasks` で明示的にリセットするか、
|
|
189
|
+
リクエストスコープの変数で管理することを推奨。
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Developer Experience (DX) Review
|
|
194
|
+
|
|
195
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
196
|
+
|
|
197
|
+
公式ドキュメントと nene2-python のサンプルを読み比べながら実装を進めている段階。
|
|
198
|
+
|
|
199
|
+
**ドキュメント理解**: `Lock` / `Semaphore` / `Event` の使い分けは公式ドキュメントだけでは直感しにくい。
|
|
200
|
+
`RLock` が必要な場面(再帰・再入)はコードを読んでも理由が分かりにくく、
|
|
201
|
+
サンプルに `Lock` では動かない例と `RLock` が必要な理由のコメントがほしい。
|
|
202
|
+
**事故リスク**: 中。`Lock` を使って実装し「テストが通った」のに高並列で稀にデッドロックする状況を
|
|
203
|
+
初心者は再現・デバッグできない。`ThreadSafeCounter` のパターンをそのままコピーすれば安全だが、
|
|
204
|
+
少し変形させると競合状態を踏む。
|
|
205
|
+
**規約の使いやすさ**: `with self._lock:` パターンは一度覚えれば機械的に書ける。
|
|
206
|
+
`threading.Thread` のコンストラクタ引数(`target`, `args`)の型制約も明確。
|
|
207
|
+
|
|
208
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
209
|
+
|
|
210
|
+
既存コードをコピーして組み込むスタイルで、スレッドセーフの概念は知っているが深くは理解していない。
|
|
211
|
+
|
|
212
|
+
**コピペ可能性**: `ThreadSafeCounter` と `run_tasks_in_pool()` のパターンはコピーしやすい。
|
|
213
|
+
`producer_consumer()` は `None` センチネルの仕組みが独特で、理解せずコピーしても
|
|
214
|
+
コンシューマー数を変えたときに `None` 投入数を忘れてデッドロックするリスクがある。
|
|
215
|
+
**拡張時の罠**: `MAX_TASKS = 100` 定数をコピーして変更するときに `app.py` 側の
|
|
216
|
+
Pydantic `max_length` を更新し忘れると、二重バリアが非対称になる。定数を共有するか、
|
|
217
|
+
変更箇所を CLAUDE.md に明記する対策が望ましい。
|
|
218
|
+
**セキュリティ的な事故リスク**: 中。スレッド数・タスク数の上限を削除すると DoS につながる。
|
|
219
|
+
コメントや定数名(`MAX_TASKS`, `MAX_WORKERS`)がその意図を伝えているが、
|
|
220
|
+
「動かすために邪魔」と感じて消す人がいる。
|
|
221
|
+
|
|
222
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
223
|
+
|
|
224
|
+
API クライアント側を実装する立場。FastAPI のエンドポイントが返す JSON 構造を重視する。
|
|
225
|
+
|
|
226
|
+
**エラーレスポンスの質**: Pydantic バリデーション違反(`workers > 16`)は FastAPI が自動で
|
|
227
|
+
422 Unprocessable Entity を返し、Problem Details に近い構造でエラー内容が分かる。
|
|
228
|
+
空リスト投入に対する 400 も `HTTPException(detail=...)` で明示的にメッセージが返る。
|
|
229
|
+
**Python 固有概念の学習コスト**: `threading.local` の「スレッドごとに別の変数が見える」概念は
|
|
230
|
+
JavaScript の非同期コンテキストとは全く異なり、理解に時間がかかる。
|
|
231
|
+
`AsyncLocalStorage` との類推コメントがあると助かる。
|
|
232
|
+
**事故リスク**: 低。HTTP 境界は Pydantic で保護されており、クライアント側から見た挙動は安定している。
|
|
233
|
+
|
|
234
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
235
|
+
|
|
236
|
+
スレッドセーフな Django ミドルウェアや Celery タスクを書いてきた経験を持つ。
|
|
237
|
+
|
|
238
|
+
**他フレームワークとの差異**: Django の `thread_locals` パターンや Celery の
|
|
239
|
+
ワーカーモデルとの比較で `threading.local` の動作は馴染みやすい。
|
|
240
|
+
`ThreadPoolExecutor` は Celery/asyncio が使えない場面のシンプルな代替として評価できる。
|
|
241
|
+
ただし FastAPI は ASGI 非同期アプリであり、スレッドを大量に起動すると
|
|
242
|
+
`uvicorn` ワーカーの事前割り当てスレッドと競合する点は Django とは異なる。
|
|
243
|
+
**nene2-python の薄さへの評価**: `ThreadSafeCounter` をアプリ本体に組み込む場合、
|
|
244
|
+
`Depends()` でシングルトン管理するか、リクエストごとに生成するかを明示的に選ぶ必要がある。
|
|
245
|
+
「薄い = 決定を委ねる」という nene2 哲学に合致しているが、初心者には「どちらを選べばよいか」の
|
|
246
|
+
ガイドラインが必要。
|
|
247
|
+
**本番投入可能性**: `MAX_TASKS`/`MAX_WORKERS` の定数管理と Pydantic `le=` の二重防御は
|
|
248
|
+
本番環境で使えるレベル。`run_tasks_in_pool()` の `timeout` 引数は重要で、
|
|
249
|
+
外部 API 呼び出しを含むタスクにはタイムアウト設定を忘れずに。
|
|
250
|
+
|
|
251
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
252
|
+
|
|
253
|
+
チームで nene2-python を使う場合のリスクとコードレビュー観点を評価する。
|
|
254
|
+
|
|
255
|
+
**コードレビューチェックポイント**:
|
|
256
|
+
- [x] `threading.Thread` に `daemon=True` を付けているか(親スレッド終了時に孤児化しない)
|
|
257
|
+
- [x] `Lock` の取得・解放が `with` 文で行われているか(`acquire()`/`release()` の直接呼び出しはリークリスク)
|
|
258
|
+
- [x] `thread.join(timeout=...)` に必ずタイムアウトを設定しているか(無限待機防止)
|
|
259
|
+
- [x] `threading.local` のリクエスト間汚染を考慮しているか
|
|
260
|
+
- [x] `MAX_TASKS`/`MAX_WORKERS` の定数が `demos.py` と `app.py` で一貫しているか
|
|
261
|
+
|
|
262
|
+
**チームでの安全な共有パターン**: `with self._lock:` パターンは慣れれば安全で機械的。
|
|
263
|
+
`ThreadPoolExecutor` は `with` 文でコンテキストマネージャーとして使うことで
|
|
264
|
+
自動シャットダウンが保証されるため、これを必須パターンとして徹底させると良い。
|
|
265
|
+
**ツール追加の必要性**: `pylint` の `threading` チェック(`W1506: using-constant-test`)を
|
|
266
|
+
`ruff` ルールセットに追加できれば望ましいが、現状の `PL` ルールセットで大半はカバー済み。
|
|
267
|
+
|
|
268
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
269
|
+
|
|
270
|
+
CLAUDE.md の設計ポリシーと FT188 の実装を照合する。
|
|
271
|
+
|
|
272
|
+
**ポリシー達成度**: 高
|
|
273
|
+
**「初心者でも安全な API」達成度**: 高
|
|
274
|
+
**設計上の負債・ドキュメント不足**: `threading.local` をリクエストまたいで使う場合の
|
|
275
|
+
スコープ汚染リスクについて CLAUDE.md に注意書きを追加する価値がある(Issue 候補: 優先度低)。
|
|
276
|
+
また `MAX_TASKS` / `MAX_WORKERS` の定数共有パターン(`demos.py` と `app.py` で分離している)は
|
|
277
|
+
将来の設定値変更時に乖離するリスクがあるため、How-to ガイドで言及する価値がある。
|
|
278
|
+
**Follow-up Issue 候補**: なし(既存ポリシーの範囲内で解決済み)
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## クラッカーペンテスト
|
|
283
|
+
|
|
284
|
+
> **実施方針**: FT188 は threading + DoS 耐性が主題。競合状態・スレッド爆弾・エラー伝播の
|
|
285
|
+
> 欠如を意図的に試し、「正常系のみテスト済み」のコードが崩れないかを確認する。
|
|
286
|
+
|
|
287
|
+
### フェーズ1: 構造推測(攻撃者の視点)
|
|
288
|
+
|
|
289
|
+
OpenAPI スキーマ (`/openapi.json`) から以下を推測できる:
|
|
290
|
+
|
|
291
|
+
- **`/counter/increment`**: `workers` は `ge=1, le=16` — スレッド数に上限あり。
|
|
292
|
+
`count` は `ge=1, le=1000` — 各スレッドの操作数にも上限あり。
|
|
293
|
+
最大でも `16 × 1000 = 16000` インクリメントで処理が終わる。
|
|
294
|
+
- **`/semaphore/run`**: `concurrency_limit` は `le=8` — 同時実行数を抑えている。
|
|
295
|
+
`task_ids` の `max_length=50` から、タスク数が制限されている。
|
|
296
|
+
- **`/pool/run`**: `max_workers` は `le=8`、`items` は `max_length=50`。
|
|
297
|
+
デモ側の `MAX_TASKS=100` チェックも存在する(スキーマ外の防御層)。
|
|
298
|
+
- **エラーメッセージ**: `too many tasks` のメッセージから `MAX_TASKS` 定数の存在が推測可能。
|
|
299
|
+
ただしそれ自体は攻撃可能な情報ではない。
|
|
300
|
+
|
|
301
|
+
### フェーズ2: 攻撃実行ログ
|
|
302
|
+
|
|
303
|
+
#### A. Pydantic バイパス攻撃(型強制)
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
POST /counter/increment {"count": "1e3", "workers": 4}
|
|
307
|
+
→ 422 Unprocessable Entity
|
|
308
|
+
int フィールドに文字列 "1e3" → Pydantic v2 が拒否
|
|
309
|
+
|
|
310
|
+
POST /counter/increment {"count": 1000, "workers": "4"}
|
|
311
|
+
→ 422 Unprocessable Entity
|
|
312
|
+
|
|
313
|
+
POST /counter/increment {"count": 1000, "workers": 16.9}
|
|
314
|
+
→ 422 Unprocessable Entity (le=16 で 16.9 → int(16) = 16 の変換を試みるが float は拒否)
|
|
315
|
+
|
|
316
|
+
POST /semaphore/run {"task_ids": [1,2], "concurrency_limit": 0}
|
|
317
|
+
→ 422 Unprocessable Entity (ge=1 バイオレーション)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**結果**: 全試み 422 で耐えた。Pydantic の `ge`/`le`/`int` 型制約がバイパスを防いでいる。
|
|
321
|
+
|
|
322
|
+
#### B. ビジネスロジック攻撃(競合状態の悪用)
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
POST /counter/increment {"count": 1000, "workers": 16}
|
|
326
|
+
→ 200 OK {"expected": 16000, "actual": 16000, "consistent": true}
|
|
327
|
+
|
|
328
|
+
# 同一エンドポイントに並列 8 リクエストを同時送信(TestClient は同期のため逐次実行だが)
|
|
329
|
+
→ 各リクエストが独立した ThreadSafeCounter を生成するため競合なし
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**結果**: 耐えた。エンドポイントはリクエストごとに新しい `ThreadSafeCounter` を生成するため、
|
|
333
|
+
リクエスト間の状態汚染は発生しない。
|
|
334
|
+
|
|
335
|
+
#### C. 境界値・エッジケース攻撃
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
POST /counter/increment {"count": 1000, "workers": 17}
|
|
339
|
+
→ 422 Unprocessable Entity (le=16 バイオレーション)
|
|
340
|
+
|
|
341
|
+
POST /counter/increment {"count": 0, "workers": 1}
|
|
342
|
+
→ 422 Unprocessable Entity (ge=1 バイオレーション)
|
|
343
|
+
|
|
344
|
+
POST /pool/run {"items": ["x"] * 50, "max_workers": 8}
|
|
345
|
+
→ 200 OK (Pydantic max_length=50 の上限ちょうど)
|
|
346
|
+
|
|
347
|
+
POST /pool/run {"items": ["x"] * 51, "max_workers": 8}
|
|
348
|
+
→ 422 Unprocessable Entity (max_length=50 超過)
|
|
349
|
+
|
|
350
|
+
POST /semaphore/run {"task_ids": [], "concurrency_limit": 2}
|
|
351
|
+
→ 400 Bad Request "task_ids must not be empty"
|
|
352
|
+
|
|
353
|
+
POST /thread-local/run {"items": ["a" * 500], "context_key": "k"}
|
|
354
|
+
→ 200 OK (items の各要素はmax_length制約なし — ただしthread内での処理のみ)
|
|
355
|
+
|
|
356
|
+
POST /event/sync {"payload": "A" * 501}
|
|
357
|
+
→ 422 Unprocessable Entity (max_length=500 超過)
|
|
358
|
+
|
|
359
|
+
POST /event/sync {"payload": "A" * 500}
|
|
360
|
+
→ 200 OK (境界値ちょうど)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**結果**: すべての境界値で期待通りに耐えた。
|
|
364
|
+
|
|
365
|
+
#### D. 情報収集攻撃(エラーメッセージ解析)
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
POST /pool/run {"items": ["x"] * 101, "max_workers": 1}
|
|
369
|
+
→ デモ層の MAX_TASKS=100 チェックが発動するか?
|
|
370
|
+
→ Pydantic の max_length=50 が先に 422 を返すため、
|
|
371
|
+
"Too many tasks: 101 > 100" のメッセージは公開されない
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**結果**: 耐えた。Pydantic の HTTP 境界チェックが先に発動するため、
|
|
375
|
+
デモ層の `ValueError` メッセージは HTTP レスポンスに漏洩しない。
|
|
376
|
+
|
|
377
|
+
#### E. DoS 試み
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
POST /counter/increment {"count": 1000, "workers": 16}
|
|
381
|
+
→ 16スレッド × 1000回 = 16000操作、全て Lock 経由
|
|
382
|
+
→ 0.37s テスト全体(単一リクエストは数十ms 以内)
|
|
383
|
+
|
|
384
|
+
POST /pool/run {"items": ["slow"] * 50, "max_workers": 8}
|
|
385
|
+
processor に sleep(0) の簡易タスクで実行
|
|
386
|
+
→ 200 OK (50タスク, 8ワーカー)
|
|
387
|
+
|
|
388
|
+
POST /semaphore/run {"task_ids": list(range(50)), "concurrency_limit": 8}
|
|
389
|
+
→ 200 OK total=50 (Semaphore で同時実行8に制限)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**結果**: 耐えた。`workers le=16`・`max_workers le=8`・`concurrency_limit le=8`・
|
|
393
|
+
`task_ids max_length=50` の制約によりスレッド爆弾が防がれている。
|
|
394
|
+
|
|
395
|
+
### フェーズ3: 攻撃まとめ
|
|
396
|
+
|
|
397
|
+
| 攻撃カテゴリ | 試みた攻撃数 | 突破 | 耐えた | 予期しない動作 |
|
|
398
|
+
|---|---|---|---|---|
|
|
399
|
+
| Pydantic バイパス | 4 | 0 | 4 | 0 |
|
|
400
|
+
| ビジネスロジック(競合状態) | 2 | 0 | 2 | 0 |
|
|
401
|
+
| 境界値/エッジ | 8 | 0 | 8 | 0 |
|
|
402
|
+
| 情報収集 | 1 | 0 | 1 | 0 |
|
|
403
|
+
| DoS(スレッド爆弾) | 3 | 0 | 3 | 0 |
|
|
404
|
+
|
|
405
|
+
**攻撃耐性評価**: 堅牢
|
|
406
|
+
**発見した弱点**: なし。すべての攻撃が Pydantic または明示的な HTTP エラーで遮断された。
|
|
407
|
+
`items` リスト内の個別要素には `max_length` が設定されていない(`context_key` の値として
|
|
408
|
+
任意長の文字列を渡せる)が、それ自体は計算コスト攻撃にはつながらない。
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Follow-up Issues
|
|
413
|
+
|
|
414
|
+
今回の FT では新規 Follow-up Issue は発生しなかった。
|
|
415
|
+
既存の Issue (#501, #510 等) との重複もなし。
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## まとめ
|
|
420
|
+
|
|
421
|
+
FT188 では `threading` モジュールの 8 パターン(Lock・RLock・Semaphore・Event・
|
|
422
|
+
ThreadPoolExecutor・Queue・threading.local・Timer)を FastAPI サンドボックスで実装した。
|
|
423
|
+
|
|
424
|
+
主な技術的学習:
|
|
425
|
+
1. **`Lock` vs `RLock`** — 再帰的ロックが必要な場面では `RLock` が必須。誤って `Lock` を使うと
|
|
426
|
+
自己デッドロックになる(静的解析では検出できない)。
|
|
427
|
+
2. **スレッドターゲットに `lambda` を使うと mypy --strict でエラー** — `None` 返却関数を
|
|
428
|
+
呼ぶラムダは型推論に失敗する。名前付き関数に切り出すことで解決。
|
|
429
|
+
3. **`threading.local` の型制約** — `.get()` が `Any` を返すため、明示的な `str()` キャストが必要。
|
|
430
|
+
|
|
431
|
+
クラッカーペンテストでは 18 攻撃すべてを耐え、Pydantic の二重バリアと
|
|
432
|
+
`MAX_TASKS`/`MAX_WORKERS` 定数による DoS 防御が機能していることを確認した。
|
|
433
|
+
|
|
434
|
+
次の FT189 は `189 % 3 == 0` のため **セキュリティ診断あり**(`189 % 4 = 1` でペンテストなし)。
|