nene2-python 1.8.50__tar.gz → 1.8.51__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.50 → nene2_python-1.8.51}/PKG-INFO +1 -1
- nene2_python-1.8.51/docs/field-trials/2026-05-field-trial-180.md +475 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/INDEX.md +5 -4
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/todo/current.md +9 -6
- {nene2_python-1.8.50 → nene2_python-1.8.51}/pyproject.toml +1 -1
- {nene2_python-1.8.50 → nene2_python-1.8.51}/.env.example +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/.gitignore +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/AGENTS.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/CHANGELOG.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/CLAUDE.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/Dockerfile +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/LICENSE +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/README.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/alembic/README +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/alembic/env.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/alembic.ini +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/compose.yaml +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/de/index.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-168.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-169.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-170.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-171.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-172.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-173.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-174.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-175.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-176.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-177.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-178.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-179.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/fr/index.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/index.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/index.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/reference/api.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/roadmap.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/zh/index.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/package-lock.json +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/package.json +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/__main__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/app.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/mcp.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/schema.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/conftest.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/tests/scripts/test_export_openapi.py +0 -0
- {nene2_python-1.8.50 → nene2_python-1.8.51}/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.51
|
|
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,475 @@
|
|
|
1
|
+
# FT180: xml モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: XML パース・構造検証・XXE/エンティティ展開爆弾防御
|
|
5
|
+
**セキュリティ診断**: **あり**(180 % 3 = 0)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
Python 標準ライブラリの `xml.etree.ElementTree` と、セキュリティ強化ライブラリ `defusedxml` を検証する。
|
|
12
|
+
単純な XML パースにとどまらず、XXE (XML External Entity) インジェクション、
|
|
13
|
+
エンティティ展開爆弾(Billion Laughs 攻撃)への対策、
|
|
14
|
+
XML 構造検証、安全な XML 構築、RSS フィードパースまで網羅する。
|
|
15
|
+
|
|
16
|
+
FT180 は 180 % 3 = 0(セキュリティ診断)かつ 180 % 4 = 0(クラッカーペンテスト)の最も重い回。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 実装したサンプルアプリ
|
|
21
|
+
|
|
22
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft180-xml/`
|
|
23
|
+
|
|
24
|
+
### 主要機能
|
|
25
|
+
|
|
26
|
+
| 関数/クラス | 概要 |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `parse_xml(content)` | defusedxml で安全にパース(XXE/展開爆弾防御) |
|
|
29
|
+
| `extract_elements(content, tag)` | 指定タグを全て抽出 |
|
|
30
|
+
| `xml_to_dict(content)` | XML を dict に変換 |
|
|
31
|
+
| `validate_structure(content, required_tags)` | 必須タグの存在検証 |
|
|
32
|
+
| `build_xml(data, root_tag)` | dict から XML を安全に構築(NCName 検証 + 自動エスケープ) |
|
|
33
|
+
| `prettify_xml(content)` | XML の整形(ET.indent) |
|
|
34
|
+
| `parse_rss_feed(content)` | RSS 2.0 フィードをパース |
|
|
35
|
+
| `is_safe_xml(content)` | XML の安全性チェック |
|
|
36
|
+
| `detect_entity_expansion(content)` | DTD/ENTITY 宣言の検出 |
|
|
37
|
+
|
|
38
|
+
### HTTP エンドポイント
|
|
39
|
+
|
|
40
|
+
| メソッド | パス | 概要 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| POST | `/parse` | XML を安全にパース |
|
|
43
|
+
| POST | `/extract` | 指定タグの要素を抽出 |
|
|
44
|
+
| POST | `/to-dict` | XML を dict に変換 |
|
|
45
|
+
| POST | `/validate` | 必須タグの存在検証 |
|
|
46
|
+
| POST | `/build` | dict から XML を構築 |
|
|
47
|
+
| POST | `/prettify` | XML を整形 |
|
|
48
|
+
| POST | `/rss` | RSS フィードをパース |
|
|
49
|
+
| POST | `/safe-check` | XML 安全性チェック |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## テスト結果
|
|
54
|
+
|
|
55
|
+
**56 passed**(初回 54 通過 → F-1 修正後 56 全通過)
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
56 passed in 0.48s
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
mypy: Success / ruff: All checks passed / pip-audit: PYSEC-2025-183(継続監視)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 摩擦ポイント
|
|
66
|
+
|
|
67
|
+
### F-1: `ET.Element()` が不正な XML タグ名を受け入れる(深刻度: 高)
|
|
68
|
+
|
|
69
|
+
**事象**: `build_xml({"a": "b"}, root_tag="<inject/>")` を呼ぶと
|
|
70
|
+
`ET.Element("<inject/>")` がエラーなしに実行され、`<<inject/>><a>b</a></<inject/>>` という
|
|
71
|
+
壊れた XML 文字列が生成された。
|
|
72
|
+
|
|
73
|
+
**原因**: `xml.etree.ElementTree.Element()` はタグ名に対してバリデーションを行わない。
|
|
74
|
+
不正なタグ名を渡しても例外を raise せず、そのまま文字列として使用する。
|
|
75
|
+
生成された XML は再パースできない(`ET.fromstring()` が `ParseError` を raise する)が、
|
|
76
|
+
API レスポンスとして返されてしまう。
|
|
77
|
+
|
|
78
|
+
**対応**:
|
|
79
|
+
```python
|
|
80
|
+
_VALID_XML_NAME_RE = re.compile(r"^[a-zA-Z_][\w\-\.]*$")
|
|
81
|
+
|
|
82
|
+
def build_xml(data: dict[str, str], root_tag: str = "root") -> str | None:
|
|
83
|
+
if not _VALID_XML_NAME_RE.match(root_tag):
|
|
84
|
+
return None # 不正なタグ名は拒否
|
|
85
|
+
...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
HTTP エンドポイントでは `None` → 400 Bad Request を返すように修正済み。
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 観察点
|
|
93
|
+
|
|
94
|
+
### 観察1: `xml.etree.ElementTree` は XXE に対してデフォルトで脆弱
|
|
95
|
+
|
|
96
|
+
Python の標準ライブラリ `xml.etree.ElementTree` は、Python 3.8 以降は
|
|
97
|
+
一部の外部エンティティ展開を制限しているが、完全ではない。
|
|
98
|
+
`defusedxml` はより包括的な防御を提供する。
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# 標準ライブラリ: XXE に脆弱(バージョンにより挙動が異なる)
|
|
102
|
+
import xml.etree.ElementTree as ET
|
|
103
|
+
ET.fromstring(xxe_payload) # → /etc/passwd の内容が展開される可能性
|
|
104
|
+
|
|
105
|
+
# defusedxml: XXE を明示的に拒否
|
|
106
|
+
import defusedxml.ElementTree as safe_ET
|
|
107
|
+
safe_ET.fromstring(xxe_payload) # → DefusedXmlException を raise
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
セキュリティ上の理由から、外部データの XML パースには必ず `defusedxml` を使用すること。
|
|
111
|
+
|
|
112
|
+
### 観察2: Billion Laughs 攻撃はエンティティ展開爆弾
|
|
113
|
+
|
|
114
|
+
エンティティを再帰的に参照させることで指数関数的にメモリを消費させる攻撃。
|
|
115
|
+
数KB の入力が解凍後に GB 規模のメモリを要求する。
|
|
116
|
+
|
|
117
|
+
```xml
|
|
118
|
+
<!DOCTYPE lolz [
|
|
119
|
+
<!ENTITY lol "lol">
|
|
120
|
+
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
|
|
121
|
+
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
|
|
122
|
+
...
|
|
123
|
+
]>
|
|
124
|
+
<root>&lolN;</root>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`defusedxml` は DTD/ENTITY 宣言を含む XML を拒否するため、この攻撃を無効化できる。
|
|
128
|
+
|
|
129
|
+
### 観察3: `ET.SubElement` による自動エスケープ
|
|
130
|
+
|
|
131
|
+
XML を文字列フォーマットで構築するとインジェクション脆弱性になる。
|
|
132
|
+
`ET.SubElement` はテキストコンテンツを自動エスケープするため安全。
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
# 危険: 文字列フォーマットでの XML 構築
|
|
136
|
+
f"<root><comment>{user_input}</comment></root>"
|
|
137
|
+
# user_input = "</comment><evil/><comment>" の場合: 構造が破壊される
|
|
138
|
+
|
|
139
|
+
# 安全: ET.SubElement を使う(自動エスケープ)
|
|
140
|
+
root = ET.Element("root")
|
|
141
|
+
child = ET.SubElement(root, "comment")
|
|
142
|
+
child.text = user_input # < > & が自動的にエスケープされる
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 観察4: CDATA セクションはそのままテキストとして扱われる
|
|
146
|
+
|
|
147
|
+
XML の `<![CDATA[...]]>` セクション内のコンテンツは ElementTree によって
|
|
148
|
+
そのままテキストとして展開される。これ自体は安全だが、
|
|
149
|
+
CDATA 内の `<evil>` タグなどがテキストとして返されることを API クライアントが知っておく必要がある。
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
result = parse_xml("<root><![CDATA[<evil>not xml</evil>]]></root>")
|
|
153
|
+
# result.text == "<evil>not xml</evil>"(エスケープなし — テキストとして展開済み)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## nene2-python フレームワークとの統合
|
|
159
|
+
|
|
160
|
+
- `defusedxml` は `pip install defusedxml` で追加(軽量、メンテ活発)
|
|
161
|
+
- `parse_xml()` / `parse_rss_feed()` は XML を受け取る API エンドポイントの入力処理として使える
|
|
162
|
+
- `build_xml()` の NCName バリデーション(`_VALID_XML_NAME_RE`)は tag 名入力を受ける全関数で必要
|
|
163
|
+
- `MAX_XML_BYTES = 1MB` + Pydantic `max_length=2MB` で DoS 対策済み
|
|
164
|
+
- `MAX_ELEMENTS = 10_000` + `MAX_DEPTH = 50` で構造爆発(大量ネスト)も防御
|
|
165
|
+
- `APIRouter` + `create_app()` パターンを最初から適用済み
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Developer Experience (DX) Review
|
|
170
|
+
|
|
171
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
172
|
+
|
|
173
|
+
SOAP API や RSS フィードを扱う API を実装しようとしている。
|
|
174
|
+
|
|
175
|
+
**ドキュメント理解**: `import xml.etree.ElementTree as ET` → `ET.parse()` は直感的。
|
|
176
|
+
しかし「なぜ `defusedxml` を使うのか」の説明がなければ標準ライブラリのみで実装してしまう。
|
|
177
|
+
**事故リスク**: 高。`xml.etree.ElementTree` で外部データをパースする実装は XXE に脆弱になりうる。
|
|
178
|
+
公式ドキュメントにも警告はあるが、見落としやすい。
|
|
179
|
+
**規約の使いやすさ**: `defusedxml` の `fromstring()` は標準ライブラリとほぼ同じ API なので移行コストは低い。
|
|
180
|
+
|
|
181
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
182
|
+
|
|
183
|
+
既存の XML パースコードをコピーして API に組み込もうとしている。
|
|
184
|
+
|
|
185
|
+
**コピペ可能性**: `ET.parse()` / `ET.fromstring()` のサンプルはネットに多いが、
|
|
186
|
+
`defusedxml` を使うサンプルは少ない。コピペすると脆弱なコードになる。
|
|
187
|
+
**拡張時の罠**: `parse_xml()` を `ET.fromstring()` に置き換えると XXE 防御が消える。
|
|
188
|
+
「同じ API だし速いから」という判断で変更する人がいる。
|
|
189
|
+
**セキュリティ的な事故リスク**: 高。XXE 経由で内部ファイル読み取りが可能になる。
|
|
190
|
+
本番環境でサーバー設定ファイルや秘密鍵が読まれる可能性がある。
|
|
191
|
+
|
|
192
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
193
|
+
|
|
194
|
+
JS の `DOMParser` に慣れており、Python で同じことをしようとしている。
|
|
195
|
+
|
|
196
|
+
**エラーレスポンスの質**: 400 Bad Request に具体的なメッセージが返るのは良い。
|
|
197
|
+
XXE と単純な XML 不正の区別が API レスポンスからできない(どちらも 400)が、
|
|
198
|
+
セキュリティ上は意図的な設計。
|
|
199
|
+
**Python 固有概念の学習コスト**: `ET.Element` のツリー操作は `DOM` に近い。
|
|
200
|
+
`root.iter()` / `root.find()` の違いは JS の `querySelectorAll` と `querySelector` に相当する。
|
|
201
|
+
**事故リスク**: 低。HTTP 境界での Pydantic バリデーションが充実。
|
|
202
|
+
|
|
203
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
204
|
+
|
|
205
|
+
他のフレームワークで XML を扱った経験があり、`lxml` を好む。
|
|
206
|
+
|
|
207
|
+
**他フレームワークとの差異**: Django は XML サポートを標準で持たない。
|
|
208
|
+
`lxml` の `etree.XMLParser(resolve_entities=False)` でも XXE を防御できるが、
|
|
209
|
+
`defusedxml` の方が意図が明確で読みやすい。
|
|
210
|
+
**nene2-python の薄さへの評価**: `defusedxml` の採用判断が明示的でドキュメント化されている点は良い。
|
|
211
|
+
`lxml` ほど高機能でないが、標準ライブラリとの API 互換性が高く学習コストが低い。
|
|
212
|
+
**本番投入可能性**: `defusedxml` + `MAX_ELEMENTS` + `MAX_DEPTH` の多層防御は本番品質。
|
|
213
|
+
|
|
214
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
215
|
+
|
|
216
|
+
**コードレビューチェックポイント**:
|
|
217
|
+
- [x] `import xml.etree.ElementTree` を直接使っていないか(`defusedxml` が必要)
|
|
218
|
+
- [x] `ET.Element(user_input)` のようにユーザー入力をタグ名に使っていないか(F-1 の罠)
|
|
219
|
+
- [x] 文字列フォーマットで XML を構築していないか(`ET.SubElement` を使うこと)
|
|
220
|
+
- [x] `MAX_XML_BYTES` / `MAX_ELEMENTS` / `MAX_DEPTH` の三重チェックがあるか
|
|
221
|
+
|
|
222
|
+
**チームでの安全な共有パターン**: `parse_xml()` を社内ユーティリティとして共有し、
|
|
223
|
+
直接 `ET.fromstring()` や `safe_ET.fromstring()` を呼ぶ実装を禁止にすることで
|
|
224
|
+
チーム全体で一貫したセキュリティレベルを維持できる。
|
|
225
|
+
**ツール追加の必要性**: `bandit` (B314〜B320) で `xml.etree.ElementTree` の危険な使用を検出できる。
|
|
226
|
+
ruff には相当するルールがなく、コードレビューチェックリストに追加すべき。
|
|
227
|
+
|
|
228
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
229
|
+
|
|
230
|
+
**ポリシー達成度**: 高
|
|
231
|
+
**「初心者でも安全な API」達成度**: 中
|
|
232
|
+
— 標準ライブラリの `ET.fromstring()` を直接呼ぶと再発する。
|
|
233
|
+
`defusedxml` を必須とすることをドキュメントに明記する価値がある。
|
|
234
|
+
**設計上の負債**: `_VALID_XML_NAME_RE` は `build_xml()` でのみ使われるが、
|
|
235
|
+
タグ名を受け取る他の関数(存在する場合)でも必要になる。
|
|
236
|
+
共通バリデーターとして明示しておく価値がある。
|
|
237
|
+
**Follow-up Issue 候補**: `defusedxml` を XML 処理の必須依存として CLAUDE.md に追記する
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## セキュリティ診断(FT番号が3の倍数のときのみ実施)
|
|
242
|
+
|
|
243
|
+
> **診断方針**: XML 処理固有の攻撃ベクターを中心に実施。
|
|
244
|
+
|
|
245
|
+
### 1. OWASP API Security Top 10 (2023)
|
|
246
|
+
|
|
247
|
+
#### API1: オブジェクトレベルの認可不備 (BOLA / IDOR)
|
|
248
|
+
- XML パース API は ID ベースのアクセス制御を持たない構造。認可不備 N/A。
|
|
249
|
+
- **結果**: ✅ 対象外
|
|
250
|
+
|
|
251
|
+
#### API3: オブジェクトプロパティレベルの認可不備 (Mass Assignment)
|
|
252
|
+
- Pydantic Body で `content`, `tag`, `required_tags` 等の明示的フィールドのみ受け入れ。
|
|
253
|
+
- 余分なフィールドは自動的に無視される(Pydantic のデフォルト動作)。
|
|
254
|
+
- **結果**: ✅ 問題なし
|
|
255
|
+
|
|
256
|
+
#### API4: 無制限リソース消費 (Unrestricted Resource Consumption)
|
|
257
|
+
- `MAX_XML_BYTES = 1MB`、Pydantic `max_length=2MB` の二重チェック。
|
|
258
|
+
- `MAX_ELEMENTS = 10_000`、`MAX_DEPTH = 50` で構造爆発も防御。
|
|
259
|
+
- `required_tags: list[str] = Field(max_length=50)` でリスト長も制限。
|
|
260
|
+
- **結果**: ✅ 問題なし
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### 2. XML 固有のインジェクション攻撃
|
|
265
|
+
|
|
266
|
+
#### XXE (XML External Entity) インジェクション
|
|
267
|
+
```xml
|
|
268
|
+
<?xml version="1.0"?>
|
|
269
|
+
<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
|
|
270
|
+
<root>&xxe;</root>
|
|
271
|
+
```
|
|
272
|
+
- `defusedxml.ElementTree.fromstring()` が `DefusedXmlException` を raise → `None` 返却
|
|
273
|
+
- **結果**: ✅ 防御済み
|
|
274
|
+
|
|
275
|
+
#### エンティティ展開爆弾(Billion Laughs 攻撃)
|
|
276
|
+
```xml
|
|
277
|
+
<!DOCTYPE lolz [
|
|
278
|
+
<!ENTITY lol "lol">
|
|
279
|
+
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
|
|
280
|
+
...
|
|
281
|
+
]>
|
|
282
|
+
<root>&lol4;</root>
|
|
283
|
+
```
|
|
284
|
+
- `defusedxml` が DTD を含む XML を拒否 → `None` 返却
|
|
285
|
+
- **結果**: ✅ 防御済み
|
|
286
|
+
|
|
287
|
+
#### XML インジェクション(構築時)
|
|
288
|
+
- `build_xml()` は `ET.SubElement` でテキストを自動エスケープ
|
|
289
|
+
- `<script>` → `<script>`、`&` → `&`
|
|
290
|
+
- **結果**: ✅ 防御済み
|
|
291
|
+
|
|
292
|
+
#### タグ名インジェクション(F-1 発見・修正済み)
|
|
293
|
+
```python
|
|
294
|
+
build_xml({"a": "b"}, root_tag="<inject/>")
|
|
295
|
+
# 修正前: <<inject/>><a>b</a></<inject/>> を返していた
|
|
296
|
+
# 修正後: None を返し、HTTP エンドポイントは 400 を返す
|
|
297
|
+
```
|
|
298
|
+
- `_VALID_XML_NAME_RE = re.compile(r"^[a-zA-Z_][\w\-\.]*$")` で NCName 検証
|
|
299
|
+
- **結果**: ✅ 修正後は問題なし(ペンテストで発見)
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
### 3. 入力バリデーション
|
|
304
|
+
|
|
305
|
+
- 全フィールドに `max_length` 制限あり
|
|
306
|
+
- `content` フィールドは長さ 2MB(hex 換算で約 1MB のバイナリ相当)
|
|
307
|
+
- `required_tags: list[str]` は `max_length=50` でリスト長を制限
|
|
308
|
+
- Null バイト (`\x00`) を含む XML は `defusedxml` がパースエラーとして拒否
|
|
309
|
+
- **結果**: ✅ 問題なし
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
### 4. 情報漏洩
|
|
314
|
+
|
|
315
|
+
- XXE 経由でファイル内容がレスポンスに含まれないことを確認
|
|
316
|
+
- `parse_xml(xxe_payload)` → `None` → HTTP 400(ファイル内容は返さない)
|
|
317
|
+
- エラーレスポンスに内部パスや例外詳細は含まれない
|
|
318
|
+
- **結果**: ✅ 問題なし
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### 5. 依存関係の脆弱性スキャン
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
Found 1 known vulnerability in 1 package
|
|
326
|
+
pyjwt 2.12.1 PYSEC-2025-183 (fix version: なし)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- `defusedxml==0.7.1`: pip-audit 問題なし(設計上の安全性に特化したライブラリ)
|
|
330
|
+
- **対応方針**: PYSEC-2025-183 は mcp 経由の推移的依存。修正版リリース待ち(継続監視)
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### 診断サマリー
|
|
335
|
+
|
|
336
|
+
| カテゴリ | 結果 | 最重要発見 |
|
|
337
|
+
|---|---|---|
|
|
338
|
+
| OWASP API Security Top 10 | ✅ 全通過 | - |
|
|
339
|
+
| XXE インジェクション | ✅ defusedxml で防御 | - |
|
|
340
|
+
| エンティティ展開爆弾 | ✅ defusedxml で防御 | - |
|
|
341
|
+
| XML インジェクション(構築時) | ✅ ET.SubElement が自動エスケープ | - |
|
|
342
|
+
| タグ名インジェクション | ✅ NCName 検証で防御(F-1 修正済み) | ET.Element() の無バリデーション |
|
|
343
|
+
| 入力バリデーション | ✅ 問題なし | - |
|
|
344
|
+
| 情報漏洩 | ✅ 問題なし | - |
|
|
345
|
+
| 依存関係 CVE | ⚠️ PYSEC-2025-183 継続監視 | mcp 経由 PyJWT |
|
|
346
|
+
|
|
347
|
+
**総合評価**: 合格(F-1 をペンテストで発見し修正済み)
|
|
348
|
+
**発見した脆弱性**: 1件(HIGH: タグ名インジェクション → 修正済み)
|
|
349
|
+
**新規セキュリティ Issue**: なし(修正済みのため)
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## クラッカーペンテスト(FT180 — FT172, 176, 180... のみ実施)
|
|
354
|
+
|
|
355
|
+
> **実施方針**: HTTP エンドポイントに攻撃ペイロードを送り込み、耐えられるかを試験する。
|
|
356
|
+
|
|
357
|
+
### フェーズ1: 構造推測(攻撃者の視点)
|
|
358
|
+
|
|
359
|
+
- `/parse` エンドポイントのレスポンス `element_count` から内部ツリー構造が推測可能
|
|
360
|
+
- `/extract` の `tag` フィールドは XPath インジェクションではなく単純なタグ名マッチング
|
|
361
|
+
- エラーレスポンスに Python の例外クラス名・パスは含まれない
|
|
362
|
+
|
|
363
|
+
### フェーズ2: 攻撃実行ログ
|
|
364
|
+
|
|
365
|
+
#### A. Pydantic バイパス攻撃
|
|
366
|
+
```python
|
|
367
|
+
# required_tags に数値を送る
|
|
368
|
+
POST /validate {"content": "<root/>", "required_tags": [1, 2, 3]}
|
|
369
|
+
# → 422 Validation Error(string_type エラー)✅
|
|
370
|
+
|
|
371
|
+
# null コンテンツ
|
|
372
|
+
POST /parse {"content": null}
|
|
373
|
+
# → 422 ✅
|
|
374
|
+
```
|
|
375
|
+
**結果**: 耐えた(Pydantic が全て 422 で拒否)
|
|
376
|
+
|
|
377
|
+
#### B. XML 固有攻撃
|
|
378
|
+
```python
|
|
379
|
+
# XXE 攻撃
|
|
380
|
+
POST /parse {"content": "<?xml?><!DOCTYPE r [<!ENTITY x SYSTEM \"file:///etc/passwd\">]><r>&x;</r>"}
|
|
381
|
+
# → 400 ✅
|
|
382
|
+
|
|
383
|
+
# Billion Laughs
|
|
384
|
+
POST /parse {"content": "<!DOCTYPE lolz [<!ENTITY lol \"lol\">...]><root>&lol4;</root>"}
|
|
385
|
+
# → 400 ✅
|
|
386
|
+
|
|
387
|
+
# RSS フィード内 XXE
|
|
388
|
+
POST /rss {"content": "<?xml?><!DOCTYPE rss [<!ENTITY x SYSTEM \"file:///etc/passwd\">]><rss>..."}
|
|
389
|
+
# → 400 ✅
|
|
390
|
+
```
|
|
391
|
+
**結果**: 耐えた(defusedxml が全て拒否)
|
|
392
|
+
|
|
393
|
+
#### C. タグ名インジェクション(F-1 発見)
|
|
394
|
+
```python
|
|
395
|
+
# root_tag に XML 特殊文字
|
|
396
|
+
POST /build {"data": {"a": "b"}, "root_tag": "<inject/>"}
|
|
397
|
+
# → 修正前: 200 + 壊れた XML
|
|
398
|
+
# → 修正後: 400 Bad Request ✅
|
|
399
|
+
|
|
400
|
+
POST /build {"data": {"a": "b"}, "root_tag": "123invalid"}
|
|
401
|
+
# → 修正後: 400 ✅
|
|
402
|
+
```
|
|
403
|
+
**結果**: 突破(修正前) → 修正後は耐えた
|
|
404
|
+
|
|
405
|
+
#### D. 境界値・エッジケース
|
|
406
|
+
```python
|
|
407
|
+
# 深いネスト(MAX_DEPTH=50 ちょうど)
|
|
408
|
+
parse_xml("<a>" * 51 + "x" + "</a>" * 51)
|
|
409
|
+
# → ParseResult(depth=50、境界値)✅
|
|
410
|
+
|
|
411
|
+
# 要素数上限(MAX_ELEMENTS=10000)
|
|
412
|
+
parse_xml("<root>" + "<x/>" * 10001 + "</root>")
|
|
413
|
+
# → None(上限超過で拒否)✅
|
|
414
|
+
|
|
415
|
+
# CDATA セクション
|
|
416
|
+
POST /parse {"content": "<root><![CDATA[<evil/>]]></root>"}
|
|
417
|
+
# → 200、text="<evil/>"(エスケープ済み文字列として返却)✅
|
|
418
|
+
|
|
419
|
+
# Unicode タグ名
|
|
420
|
+
POST /extract {"content": "<root><日本語>test</日本語></root>", "tag": "日本語"}
|
|
421
|
+
# → 200(XML 名前空間仕様では日本語も有効)✅
|
|
422
|
+
```
|
|
423
|
+
**結果**: 耐えた(F-1 は build_xml の root_tag のみ)
|
|
424
|
+
|
|
425
|
+
#### E. DoS 試み
|
|
426
|
+
```python
|
|
427
|
+
# 大量 required_tags(max_length=50 超え)
|
|
428
|
+
POST /validate {"content": "<root/>", "required_tags": ["tag"] * 51}
|
|
429
|
+
# → 422 ✅
|
|
430
|
+
|
|
431
|
+
# 長大属性値(50000文字)
|
|
432
|
+
POST /parse {"content": f"<root attr=\"{'x' * 50000}\"/>"}
|
|
433
|
+
# → 200(defusedxml はパース。属性値は text_length でトリム)
|
|
434
|
+
```
|
|
435
|
+
**結果**: 耐えた
|
|
436
|
+
|
|
437
|
+
### フェーズ3: 攻撃まとめ
|
|
438
|
+
|
|
439
|
+
| 攻撃カテゴリ | 試みた攻撃数 | 突破 | 耐えた | 予期しない動作 |
|
|
440
|
+
|---|---|---|---|---|
|
|
441
|
+
| Pydantic バイパス | 4 | 0 | 4 | 0 |
|
|
442
|
+
| XML 固有攻撃(XXE/展開爆弾) | 5 | 0 | 5 | 0 |
|
|
443
|
+
| タグ名インジェクション | 2 | 1 → 修正 | 1 | 0 |
|
|
444
|
+
| 境界値/エッジ | 5 | 0 | 5 | 0 |
|
|
445
|
+
| DoS | 2 | 0 | 2 | 0 |
|
|
446
|
+
|
|
447
|
+
**攻撃耐性評価**: 軽微な問題あり(F-1 発見・修正済み)
|
|
448
|
+
**発見した弱点**: `ET.Element()` が不正なタグ名を受け入れる — NCName バリデーションで修正
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Follow-up Issues
|
|
453
|
+
|
|
454
|
+
| 優先度 | タイトル | 種別 |
|
|
455
|
+
|---|---|---|
|
|
456
|
+
| 中 | `defusedxml` を XML 処理の必須依存として CLAUDE.md の禁止事項に追記 | docs |
|
|
457
|
+
| 低 | `build_xml()` の key(子タグ名)にも NCName バリデーションを追加 | fix |
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## まとめ
|
|
462
|
+
|
|
463
|
+
FT180 では `xml.etree.ElementTree` + `defusedxml` を検証した。
|
|
464
|
+
56 テストが全通過し、mypy/ruff も問題なし。
|
|
465
|
+
|
|
466
|
+
セキュリティ診断とクラッカーペンテストで 1 件の脆弱性(F-1: `ET.Element()` のタグ名バリデーション不足)を発見・修正した。
|
|
467
|
+
XXE とエンティティ展開爆弾は `defusedxml` が完全に防御しており、
|
|
468
|
+
標準ライブラリの `xml.etree.ElementTree` を直接使う実装との差が明確になった。
|
|
469
|
+
|
|
470
|
+
最大の発見: `xml.etree.ElementTree.Element()` はタグ名に対してバリデーションを行わず、
|
|
471
|
+
`<inject/>` のような文字列をタグ名として受け入れて壊れた XML を生成してしまう。
|
|
472
|
+
XML を構築する API で `root_tag` 等をユーザー入力から受け取る場合は、
|
|
473
|
+
必ず XML NCName 正規表現でバリデーションすること。
|
|
474
|
+
|
|
475
|
+
v1.8.51 としてリリース。
|
|
@@ -213,19 +213,20 @@
|
|
|
213
213
|
| [FT177](2026-05-field-trial-177.md) | hashlib モジュール — PBKDF2 / scrypt / Blake2 | 🔒 | [#501](https://github.com/hideyukiMORI/nene2-python/issues/501) |
|
|
214
214
|
| [FT178](2026-05-field-trial-178.md) | base64 モジュール — エンコード・URL セーフ・データ URI | | |
|
|
215
215
|
| [FT179](2026-05-field-trial-179.md) | zlib モジュール — 圧縮・解凍・展開爆弾対策・チェックサム | | |
|
|
216
|
+
| [FT180](2026-05-field-trial-180.md) | xml モジュール — XXE/展開爆弾防御・構造検証・RSS パース | 🔒🔍 | [#506](https://github.com/hideyukiMORI/nene2-python/issues/506) [#507](https://github.com/hideyukiMORI/nene2-python/issues/507) |
|
|
216
217
|
|
|
217
218
|
---
|
|
218
219
|
|
|
219
220
|
## セキュリティ診断実施済み一覧(🔒)
|
|
220
221
|
|
|
221
|
-
FT3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 121, 124, 127, 130, 133, 136, 139, 142, 145, 148, 151, 154, 157, 160, 163, 166, 169, 172, 174, 177
|
|
222
|
+
FT3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 121, 124, 127, 130, 133, 136, 139, 142, 145, 148, 151, 154, 157, 160, 163, 166, 169, 172, 174, 177, 180
|
|
222
223
|
|
|
223
|
-
合計: **
|
|
224
|
+
合計: **61件**(180 FT 中 約 34%)
|
|
224
225
|
|
|
225
226
|
## クラッカーペンテスト実施済み一覧(🔍)
|
|
226
227
|
|
|
227
|
-
FT172, FT176
|
|
228
|
+
FT172, FT176, FT180
|
|
228
229
|
|
|
229
230
|
---
|
|
230
231
|
|
|
231
|
-
*最終更新: 2026-05-21 (
|
|
232
|
+
*最終更新: 2026-05-21 (FT180 / v1.8.51)*
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# TODO — current
|
|
2
2
|
|
|
3
3
|
最終更新: 2026-05-21
|
|
4
|
-
現状: **v1.8.
|
|
4
|
+
現状: **v1.8.51 安定版 / フィールドトライアルループ継続中(FT180 完了)**
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## 状態サマリー
|
|
9
9
|
|
|
10
|
-
v1.8.
|
|
11
|
-
フィールドトライアルループは
|
|
10
|
+
v1.8.51 完了済み。FT180(xml / XXE防御・展開爆弾対策・RSS パース)を含む FT180 件を実施済み。
|
|
11
|
+
フィールドトライアルループは FT181 以降も継続中。
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -22,6 +22,8 @@ v1.8.50 完了済み。FT179(zlib / 圧縮・解凍・展開爆弾対策・チ
|
|
|
22
22
|
|
|
23
23
|
| Issue | 内容 | 優先度 |
|
|
24
24
|
|---|---|---|
|
|
25
|
+
| [#507](https://github.com/hideyukiMORI/nene2-python/issues/507) | [FT180] build_xml() の子タグ名にも NCName バリデーションを追加 | 低 |
|
|
26
|
+
| [#506](https://github.com/hideyukiMORI/nene2-python/issues/506) | [FT180] defusedxml を XML 処理の必須依存として CLAUDE.md に追記 | 中 |
|
|
25
27
|
| [#501](https://github.com/hideyukiMORI/nene2-python/issues/501) | [FT177] FastAPI アプリファクトリで APIRouter パターンを標準化 | 中 |
|
|
26
28
|
| [#500](https://github.com/hideyukiMORI/nene2-python/issues/500) | [FT176] parse_decimal_safe() の Unicode 全角数字受け入れ挙動を文書化 | 低 |
|
|
27
29
|
| [#499](https://github.com/hideyukiMORI/nene2-python/issues/499) | [FT176] calculate_tax/discount にビジネスロジックバリデーション追加 | 中 |
|
|
@@ -32,6 +34,7 @@ v1.8.50 完了済み。FT179(zlib / 圧縮・解凍・展開爆弾対策・チ
|
|
|
32
34
|
|
|
33
35
|
| バージョン | 主な内容 |
|
|
34
36
|
|---|---|
|
|
37
|
+
| v1.8.51 | FT180: xml — XXE/展開爆弾防御・RSS パース(診断+ペンテスト) |
|
|
35
38
|
| v1.8.50 | FT179: zlib — 圧縮・解凍・展開爆弾対策・CRC32/Adler-32 |
|
|
36
39
|
| v1.8.49 | FT178: base64 — エンコード・URL セーフ・データ URI・HTTP Basic Auth |
|
|
37
40
|
| v1.8.48 | FT177: hashlib — PBKDF2 / scrypt / Blake2 キー付きハッシュ |
|
|
@@ -45,13 +48,13 @@ v1.8.50 完了済み。FT179(zlib / 圧縮・解凍・展開爆弾対策・チ
|
|
|
45
48
|
|
|
46
49
|
## フィールドトライアル進捗
|
|
47
50
|
|
|
48
|
-
**実施済み**: FT1〜
|
|
51
|
+
**実施済み**: FT1〜FT180(全 180 件)
|
|
49
52
|
|
|
50
53
|
索引: [`docs/field-trials/INDEX.md`](../field-trials/INDEX.md)
|
|
51
54
|
|
|
52
55
|
**次のアクション**:
|
|
53
|
-
-
|
|
54
|
-
-
|
|
56
|
+
- FT181 以降を継続(FT184 は 184 % 4 = 0 → クラッカーペンテスト対象)
|
|
57
|
+
- FT183 は 183 % 3 = 0 → セキュリティ診断も実施
|
|
55
58
|
|
|
56
59
|
---
|
|
57
60
|
|
|
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.50 → nene2_python-1.8.51}/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
|