nene2-python 1.8.60__tar.gz → 1.8.62__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.60 → nene2_python-1.8.62}/CLAUDE.md +42 -2
- {nene2_python-1.8.60 → nene2_python-1.8.62}/PKG-INFO +1 -1
- nene2_python-1.8.62/docs/field-trials/2026-05-field-trial-190.md +220 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/INDEX.md +2 -1
- nene2_python-1.8.62/docs/how-to/decimal-unicode-input.md +61 -0
- nene2_python-1.8.62/docs/how-to/email-address-parsing.md +57 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/todo/current.md +8 -20
- {nene2_python-1.8.60 → nene2_python-1.8.62}/pyproject.toml +1 -1
- {nene2_python-1.8.60 → nene2_python-1.8.62}/.env.example +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/.gitignore +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/AGENTS.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/CHANGELOG.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/Dockerfile +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/LICENSE +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/README.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/alembic/README +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/alembic/env.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/alembic.ini +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/compose.yaml +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/de/index.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-101.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-102.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-103.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-104.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-105.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-106.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-107.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-108.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-109.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-110.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-111.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-112.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-113.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-114.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-115.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-116.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-117.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-118.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-119.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-120.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-121.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-122.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-123.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-124.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-125.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-126.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-127.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-128.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-129.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-130.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-131.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-132.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-133.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-134.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-135.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-136.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-137.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-138.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-139.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-140.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-141.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-142.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-143.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-144.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-145.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-146.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-147.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-148.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-149.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-150.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-151.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-152.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-153.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-154.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-155.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-156.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-157.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-158.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-159.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-160.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-161.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-162.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-163.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-164.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-165.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-166.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-167.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-168.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-169.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-170.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-171.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-172.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-173.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-174.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-175.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-176.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-177.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-178.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-179.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-180.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-181.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-182.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-183.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-184.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-185.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-186.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-187.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-188.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-189.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/fr/index.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/api-versioning.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/custom-auth-middleware.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/dependency-injection.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/domain-events.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/lifespan-and-app-state.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/response-patterns.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/soft-delete.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/streaming.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/structured-logging.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/index.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/index.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/reference/api.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/roadmap.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/templates/field-trial-report.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/zh/index.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/package-lock.json +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/package.json +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/__main__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/app.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/mcp.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/schema.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/conftest.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/tests/scripts/test_export_openapi.py +0 -0
- {nene2_python-1.8.60 → nene2_python-1.8.62}/uv.lock +0 -0
|
@@ -130,6 +130,11 @@ print(sensitive_data) # logging モジュールを使う
|
|
|
130
130
|
- **セキュリティヘッダーをミドルウェアで付与**(X-Content-Type-Options, X-Frame-Options, etc.)
|
|
131
131
|
- **SQL はパラメータ化クエリのみ**。文字列フォーマット禁止
|
|
132
132
|
- **ファイルパスは `pathlib.Path` で操作**し、パストラバーサルを防ぐ
|
|
133
|
+
- **XML 処理には `defusedxml` を使用**。標準の `xml.etree.ElementTree` は XXE・展開爆弾に脆弱(FT180 で確認)
|
|
134
|
+
```bash
|
|
135
|
+
uv add defusedxml
|
|
136
|
+
```
|
|
137
|
+
`import xml.etree.ElementTree` の代わりに `import defusedxml.ElementTree` を使う。
|
|
133
138
|
|
|
134
139
|
### 依存関係の脆弱性スキャン
|
|
135
140
|
|
|
@@ -233,6 +238,37 @@ AI エージェント(Claude 等)がこのコードベースを正確に理
|
|
|
233
238
|
- `nene2.http.problem_details_response()` で RFC 9457 エラー応答
|
|
234
239
|
- `nene2.http.PaginationQueryParser` でページネーション
|
|
235
240
|
|
|
241
|
+
### APIRouter パターン(必須)
|
|
242
|
+
|
|
243
|
+
すべての FastAPI アプリで `APIRouter` + `create_app()` ファクトリパターンを使うこと。
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
# ✅ 正しい構造 — app.py
|
|
247
|
+
router = APIRouter()
|
|
248
|
+
|
|
249
|
+
@router.post("/items") # ← すべてのルート定義は router に紐付ける
|
|
250
|
+
def create_item(...): ...
|
|
251
|
+
|
|
252
|
+
@router.get("/items/{item_id}")
|
|
253
|
+
def get_item(...): ...
|
|
254
|
+
|
|
255
|
+
def create_app() -> FastAPI: # ← create_app() はファイル末尾に定義する
|
|
256
|
+
application = FastAPI(title="...")
|
|
257
|
+
application.include_router(router)
|
|
258
|
+
return application
|
|
259
|
+
|
|
260
|
+
app = create_app() # ← モジュールレベルの app は最終行
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**`create_app()` はファイルの末尾**(全 `@router.xxx()` デコレーター定義の後)に置くこと。
|
|
264
|
+
先に `app = create_app()` を呼ぶと `router` にルートが登録される前に `include_router()` が実行され、
|
|
265
|
+
エンドポイントが空になるバグが発生する(FT182 で発見)。
|
|
266
|
+
|
|
267
|
+
- `router = APIRouter()` → ファイル先頭の定数・モデル定義の後
|
|
268
|
+
- `@router.post(...)` デコレーター → ハンドラー関数の定義
|
|
269
|
+
- `create_app()` → ファイル末尾
|
|
270
|
+
- `app = create_app()` → ファイル最終行
|
|
271
|
+
|
|
236
272
|
### ミドルウェアスタック順序(重要)
|
|
237
273
|
|
|
238
274
|
`app.add_middleware()` は **LIFO**(後から追加したものが外側になる)。
|
|
@@ -378,7 +414,11 @@ Python 標準ライブラリ・サードパーティライブラリを nene2-pyt
|
|
|
378
414
|
テンプレート: docs/templates/field-trial-report.md
|
|
379
415
|
6. DX Review(6ペルソナ)を実施(後述)
|
|
380
416
|
7. FT番号が3の倍数なら セキュリティ診断 を実施(後述)
|
|
381
|
-
8. Follow-up Issues
|
|
417
|
+
8. Follow-up Issues をその場で修正してからクローズする
|
|
418
|
+
- 発見した問題(摩擦点・セキュリティ指摘)は FT PR に含めて修正する
|
|
419
|
+
- 修正 → テスト全通過 → PR に含める → GitHub Issue は PR 内でクローズ(Closes #NNN)
|
|
420
|
+
- CLAUDE.md 追記・docs 更新・サンドボックスのコード修正すべてを同じ PR に含める
|
|
421
|
+
- 「外部依存の修正待ち」など対応不可能な理由がある場合のみ Issue を残し、理由を PR 説明に記載する
|
|
382
422
|
9. まとめて main merge → パッチバージョン(v1.8.N)でリリース
|
|
383
423
|
```
|
|
384
424
|
|
|
@@ -418,7 +458,7 @@ Python 標準ライブラリ・サードパーティライブラリを nene2-pyt
|
|
|
418
458
|
|
|
419
459
|
**合否判定**:
|
|
420
460
|
- **合格**: 全カテゴリ問題なし
|
|
421
|
-
- **条件付き合格**: MEDIUM
|
|
461
|
+
- **条件付き合格**: MEDIUM 以下の指摘のみ → **同 FT の PR 内で修正してからマージ**
|
|
422
462
|
- **不合格**: HIGH/CRITICAL の指摘あり → main merge 前に必須修正
|
|
423
463
|
|
|
424
464
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.62
|
|
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,220 @@
|
|
|
1
|
+
# FT190: multiprocessing モジュール
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-21
|
|
4
|
+
**テーマ**: プロセスベース並行処理・共有状態・プロセスプール
|
|
5
|
+
**セキュリティ診断**: なし(190 % 3 = 1)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 概要
|
|
10
|
+
|
|
11
|
+
`multiprocessing` は threading と異なりプロセスを分離して実行するため、GIL(Global Interpreter Lock)の影響を受けず CPU バウンドタスクの並列化に有効。本 FT では `Pool.map` / `Pool.starmap` / `Pool.imap` / `Pool.apply_async`・共有メモリ(`Value`)・プロセス間キュー(`Queue`)・初期化関数付きプールなどの主要パターンを FastAPI エンドポイントから検証する。
|
|
12
|
+
|
|
13
|
+
FT188(threading)・FT189(subprocess)の直後として、プロセスベース並行処理の違いと型安全上の注意点を記録することも目的とする。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 実装したサンプルアプリ
|
|
18
|
+
|
|
19
|
+
**場所**: `/home/xi/docker/nene2-python-FT/ft190-multiprocessing/`
|
|
20
|
+
|
|
21
|
+
### 主要機能
|
|
22
|
+
|
|
23
|
+
| 関数/クラス | 概要 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `spawn_process(name)` | 単一プロセスを起動して PID・Alive 状態を返す |
|
|
26
|
+
| `pool_map(values, workers)` | Pool.map で並列二乗計算 |
|
|
27
|
+
| `pool_map_cube(values, workers)` | Pool.map で並列三乗計算 |
|
|
28
|
+
| `pool_starmap(pairs, workers)` | Pool.starmap で並列加算 |
|
|
29
|
+
| `apply_async_demo(values, delay)` | apply_async で非同期タスク |
|
|
30
|
+
| `shared_counter_demo(num_processes, increments_each)` | Value + Lock で共有カウンター |
|
|
31
|
+
| `queue_producer_consumer(items)` | Queue でプロデューサー/コンシューマー |
|
|
32
|
+
| `pool_imap_ordered(values, workers)` | Pool.imap(順序保証) |
|
|
33
|
+
| `pool_imap_unordered(values, workers)` | Pool.imap_unordered(順序不定) |
|
|
34
|
+
| `pool_map_chunksize(values, chunksize, workers)` | チャンクサイズ付き Pool.map |
|
|
35
|
+
| `pool_with_initializer(values, config)` | 初期化関数付き Pool |
|
|
36
|
+
| `build_task_func(operation)` | 操作名から関数を返す(match 文使用) |
|
|
37
|
+
|
|
38
|
+
### HTTP エンドポイント
|
|
39
|
+
|
|
40
|
+
| メソッド | パス | 概要 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| POST | `/multiprocessing/spawn` | プロセス起動・PID 取得 |
|
|
43
|
+
| GET | `/multiprocessing/cpu-count` | CPU コア数・start method |
|
|
44
|
+
| POST | `/multiprocessing/pool-map` | 並列二乗計算 |
|
|
45
|
+
| POST | `/multiprocessing/pool-map-cube` | 並列三乗計算 |
|
|
46
|
+
| POST | `/multiprocessing/pool-starmap` | 並列加算 |
|
|
47
|
+
| POST | `/multiprocessing/apply-async` | 非同期タスク |
|
|
48
|
+
| POST | `/multiprocessing/shared-counter` | 共有カウンター(Lock 付き) |
|
|
49
|
+
| POST | `/multiprocessing/queue` | プロデューサー/コンシューマー |
|
|
50
|
+
| POST | `/multiprocessing/imap-ordered` | 順序保証 imap |
|
|
51
|
+
| POST | `/multiprocessing/imap-unordered` | 順序不定 imap |
|
|
52
|
+
| POST | `/multiprocessing/chunksize` | チャンクサイズ指定 map |
|
|
53
|
+
| POST | `/multiprocessing/with-initializer` | 初期化関数付きプール |
|
|
54
|
+
| GET | `/multiprocessing/daemon-demo` | デーモンプロセスデモ |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## テスト結果
|
|
59
|
+
|
|
60
|
+
**56 passed**
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
56 passed in 1.18s
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 摩擦ポイント
|
|
69
|
+
|
|
70
|
+
### F-1: Pool.starmap にローカル関数を渡すと PicklingError(深刻度: 中)
|
|
71
|
+
|
|
72
|
+
**事象**: `pool_starmap()` 内でローカル関数 `add` を定義して `pool.starmap(add, pairs)` に渡したところ、`_pickle.PicklingError: Can't pickle local object` が発生した。
|
|
73
|
+
|
|
74
|
+
**原因**: multiprocessing はワーカープロセスにタスクを pickle で送信する。ローカル関数はモジュールトップレベルに存在しないため、ワーカーが unpickle できない。threading ではそのまま渡せるため、つい同じように書いてしまう。
|
|
75
|
+
|
|
76
|
+
**対応**: `_add(a, b)` としてモジュールレベルに定義。ワーカー用関数は必ずモジュールレベルに置くルールを確認(threading と multiprocessing の違い)。
|
|
77
|
+
|
|
78
|
+
### F-2: `multiprocessing.Value` に `Synchronized[c_int]` を使うと mypy エラー(深刻度: 低)
|
|
79
|
+
|
|
80
|
+
**事象**: `counter: Synchronized[c_int] = Value(c_int, 0)` と書くと、`counter.value += 1` に対して `Unsupported operand types for + ("c_int" and "int")` エラーが発生した。
|
|
81
|
+
|
|
82
|
+
**原因**: typeshed の `Synchronized[_CT]` は `.value` を `_CT` 型として定義しており、`_CT = c_int` のとき `c_int + int` が不正になる。しかし実際の runtime では `Value("i", 0).value` は Python の `int` を返す。型スタブが実態と乖離している。
|
|
83
|
+
|
|
84
|
+
**対応**: `Synchronized[int]` とアノテーションし `Value("i", 0)` (文字列フォーマットコード) を使用。mypy は文字列フォーマットコードからジェネリック型を解決できないため `Synchronized[int]` の明示アノテーションで型整合が取れる。
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 観察点
|
|
89
|
+
|
|
90
|
+
### 観察1: Pool ワーカー関数のスコープ制約
|
|
91
|
+
|
|
92
|
+
multiprocessing の Pool ワーカーは pickle でシリアライズしてワーカープロセスに送信する。pickle できるのはモジュールトップレベルで定義された関数のみ。lambda・クロージャ・ローカル関数は pickle できない。
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# ❌ PicklingError
|
|
96
|
+
def my_func(pairs):
|
|
97
|
+
def add(a, b): return a + b
|
|
98
|
+
with Pool() as pool:
|
|
99
|
+
return pool.starmap(add, pairs)
|
|
100
|
+
|
|
101
|
+
# ✅ 正しい
|
|
102
|
+
def _add(a: int, b: int) -> int: # モジュールレベル
|
|
103
|
+
return a + b
|
|
104
|
+
|
|
105
|
+
def my_func(pairs):
|
|
106
|
+
with Pool() as pool:
|
|
107
|
+
return pool.starmap(_add, pairs)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 観察2: GIL と multiprocessing
|
|
111
|
+
|
|
112
|
+
threading では GIL により Python バイトコードの並列実行が制限される(I/O バウンドは並行可)が、multiprocessing は別プロセスなので GIL を回避して CPU バウンドタスクを並列化できる。プロセス起動コスト(~50ms)があるため、軽量タスクには Pool よりも threading が適する。
|
|
113
|
+
|
|
114
|
+
### 観察3: Value の型アノテーション戦略
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# 実態と合うアノテーション
|
|
118
|
+
from multiprocessing.sharedctypes import Synchronized
|
|
119
|
+
counter: Synchronized[int] = Value("i", 0) # 文字列フォーマットコードを使う
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`Synchronized[int]` とすることで `counter.value` が `int` として扱われ、`counter.value += 1` が mypy --strict を通過する。`Value(c_int, 0)` の形式は typeshed との不整合を生む。
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## nene2-python フレームワークとの統合
|
|
127
|
+
|
|
128
|
+
- Pool ワーカー関数はモジュールレベルに置く制約があるため、プロセスプールを使う UseCase では関数をモジュールレベルの `_private` 関数として分離するパターンが必要
|
|
129
|
+
- FastAPI のリクエストハンドラー内で `Pool` を生成する場合、`with Pool() as pool:` のコンテキストマネージャーで確実にクリーンアップすること(`pool.terminate()` の漏れ防止)
|
|
130
|
+
- multiprocessing は `__main__` ガード(`if __name__ == "__main__":`)が必要な start method(spawn/forkserver)があるが、FastAPI アプリとして使う場合は `fork`(Linux デフォルト)なので不要。ただし Windows 移植時は注意
|
|
131
|
+
- `MAX_WORKERS: int = 8` 定数でワーカー数を上限制限し、DoS を防止
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Developer Experience (DX) Review
|
|
136
|
+
|
|
137
|
+
### ペルソナ1: 初心者(Python 歴1年・独学中・女性・バックエンド志望)
|
|
138
|
+
|
|
139
|
+
チュートリアルで threading を学んだ後、multiprocessing に入門する段階。
|
|
140
|
+
|
|
141
|
+
**ドキュメント理解**: `Pool.map` の使い方は直感的で理解しやすい。ワーカー関数がモジュールレベルでないと `PicklingError` になる制約は、threading と混同して気づきにくい(F-1)。エラーメッセージが英語で `Can't pickle local object` と出るので原因の特定は可能だが、初心者には意味が分かりにくい。
|
|
142
|
+
**事故リスク**: 中。PicklingError は実行時エラーで早期発見できるが、初心者が「なぜ動かないのか」を理解するまでに時間がかかる。
|
|
143
|
+
**規約の使いやすさ**: `with Pool(processes=n) as pool:` のパターンは覚えやすい。ワーカー関数をモジュールレベルに置く規則は一度理解すれば機械的に適用できる。
|
|
144
|
+
|
|
145
|
+
### ペルソナ2: ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)
|
|
146
|
+
|
|
147
|
+
threading コードをコピーして multiprocessing に変えようとする場面。
|
|
148
|
+
|
|
149
|
+
**コピペ可能性**: `Pool.map` のサンプルは分かりやすい。ただし、ラムダや内側クロージャを気軽に使うと PicklingError になる。threading では動いたコードをそのまま転用できないケースがある。
|
|
150
|
+
**拡張時の罠**: `Value("i", 0)` の型アノテーションを `Synchronized[c_int]` と書くと mypy エラーになる(F-2)。型を「直しよう」としてはまる。`Synchronized[int]` と書く正解はドキュメントに明示されていない。
|
|
151
|
+
**セキュリティ的な事故リスク**: 中。`workers` 上限がなければ `Pool(processes=99999)` でプロセス枯渇 DoS が可能。本実装では `MAX_WORKERS = 8` で上限制限している。
|
|
152
|
+
|
|
153
|
+
### ペルソナ3: フロントエンド寄り経験者(React/TS 歴4年・バックエンド転向中・ノンバイナリ)
|
|
154
|
+
|
|
155
|
+
並行処理の概念(Promise.all 的なもの)はわかるが、プロセスとスレッドの違いが曖昧な段階。
|
|
156
|
+
|
|
157
|
+
**エラーレスポンスの質**: 422 Unprocessable Entity が `workers` 超過・`values` 超過で正しく返る。`PicklingError` は HTTP 500 になるが、デモコードのワーカー関数はモジュールレベルに固定しているため HTTP 経由では発生しない。
|
|
158
|
+
**Python 固有概念の学習コスト**: `Pool` が Python オブジェクトプールではなくプロセスプールであること、`fork` vs `spawn` の start method の違いは非直感的。
|
|
159
|
+
**事故リスク**: 低。HTTP 入力のバリデーションが Pydantic で守られており、エンドポイント経由では PicklingError には到達できない。
|
|
160
|
+
|
|
161
|
+
### ペルソナ4: バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)
|
|
162
|
+
|
|
163
|
+
Celery・concurrent.futures との比較で評価する。
|
|
164
|
+
|
|
165
|
+
**他フレームワークとの差異**: `concurrent.futures.ProcessPoolExecutor` と `multiprocessing.Pool` は機能が重複する。FastAPI アプリで重い CPU 処理をオフロードするなら `ProcessPoolExecutor` の方が Python 公式の高レベル API として推奨されている。本 FT が `Pool` を選んだのは stdlib の低レベル API を直接学ぶため。
|
|
166
|
+
**nene2-python の薄さへの評価**: UseCase 層が HTTP・DB 非依存なので、Pool ワーカーに UseCase を渡す設計も可能(ただし pickle 可能なオブジェクトに限る)。
|
|
167
|
+
**本番投入可能性**: `MAX_WORKERS` の上限設定・`with Pool() as pool:` のコンテキスト管理が適切。本番では `ProcessPoolExecutor` との使い分けガイドが欲しい。
|
|
168
|
+
|
|
169
|
+
### ペルソナ5: シニアエンジニア(設計・コードレビュー担当・女性・10-12年)
|
|
170
|
+
|
|
171
|
+
チームで multiprocessing を使う場合のリスクをレビューする。
|
|
172
|
+
|
|
173
|
+
**コードレビューチェックポイント**:
|
|
174
|
+
- [ ] `Pool.map` のワーカー関数がモジュールレベルか(ローカル関数・lambda は PicklingError)
|
|
175
|
+
- [ ] `with Pool() as pool:` でコンテキストマネージャーを使い、確実に終了しているか
|
|
176
|
+
- [ ] `workers` に上限制限があるか(`min(workers, MAX_WORKERS)` パターン)
|
|
177
|
+
- [ ] `join(timeout=N)` でゾンビプロセス防止が書かれているか
|
|
178
|
+
- [ ] `Value` の型アノテーションが `Synchronized[int]` か(`Synchronized[c_int]` は mypy 不整合)
|
|
179
|
+
|
|
180
|
+
**チームでの安全な共有パターン**: ワーカー関数ファイルを `_workers.py` として分離する規則を設けると pickle 可能性が明確になる。
|
|
181
|
+
**ツール追加の必要性**: ruff には multiprocessing 固有のルールはない。`pool.map(lambda x: x, [])` のようなラムダ誤用は静的解析では検出できない(実行時エラー)。
|
|
182
|
+
|
|
183
|
+
### ペルソナ6: 設計者・ポリシー照合(nene2-python 設計ポリシー目線)
|
|
184
|
+
|
|
185
|
+
CLAUDE.md ポリシーとの整合性を確認する。
|
|
186
|
+
|
|
187
|
+
**ポリシー達成度**: 高
|
|
188
|
+
**「初心者でも安全な API」達成度**: 中(PicklingError は HTTP 経由では発生しないが、demos.py を直接使う場面では踏みやすい)
|
|
189
|
+
**設計上の負債・ドキュメント不足**: `multiprocessing.Value` の型アノテーション方法(`Synchronized[int]` vs `Synchronized[c_int]`)が typeshed の実態と乖離している点は How-to に記録する価値がある(→ Follow-up Issue)
|
|
190
|
+
**Follow-up Issues**: 下記参照
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Follow-up Issues
|
|
195
|
+
|
|
196
|
+
今回の FT で発見した問題を同 FT PR 内で即時対応済み(バックログを残さないルール)。
|
|
197
|
+
|
|
198
|
+
### 即時対応済み
|
|
199
|
+
|
|
200
|
+
| 対応内容 | 対応方法 |
|
|
201
|
+
|---|---|
|
|
202
|
+
| Pool.starmap にローカル関数を渡して PicklingError(F-1) | `_add()` をモジュールレベルへ移動 |
|
|
203
|
+
| `Synchronized[c_int]` mypy 不整合(F-2) | `Synchronized[int]` + `Value("i", 0)` に変更 |
|
|
204
|
+
|
|
205
|
+
### 文書化 Issue(同 PR で作成・クローズ)
|
|
206
|
+
|
|
207
|
+
| タイトル | 種別 |
|
|
208
|
+
|---|---|
|
|
209
|
+
| multiprocessing.Value のアノテーションには `Synchronized[int]` を使う | docs |
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## まとめ
|
|
214
|
+
|
|
215
|
+
multiprocessing の主要パターン(Pool.map/starmap/imap/apply_async・Value・Queue・初期化関数)を 13 エンドポイント・56 テストで検証した。FT190 固有の発見は 2 点:
|
|
216
|
+
|
|
217
|
+
1. **PicklingError**: Pool ワーカー関数のモジュールレベル配置制約(threading との差異)
|
|
218
|
+
2. **Value 型アノテーション**: `Synchronized[c_int]` は typeshed と乖離、`Synchronized[int]` + 文字列フォーマットコードで回避
|
|
219
|
+
|
|
220
|
+
いずれも実装中に即時修正済み。FT191 に向けた懸案はなし。
|
|
@@ -223,6 +223,7 @@
|
|
|
223
223
|
| [FT187](2026-05-field-trial-187.md) | collections モジュール — Counter・defaultdict・deque・ChainMap・NamedTuple・OrderedDict | | |
|
|
224
224
|
| [FT188](2026-05-field-trial-188.md) | threading モジュール — Thread・Lock・RLock・Semaphore・Event・ThreadPoolExecutor・Queue・Timer | 🔍 | |
|
|
225
225
|
| [FT189](2026-05-field-trial-189.md) | subprocess モジュール — 安全なプロセス実行・stdin/stdout 制御・ストリーミング | 🔒 | [#524](https://github.com/hideyukiMORI/nene2-python/issues/524) |
|
|
226
|
+
| [FT190](2026-05-field-trial-190.md) | multiprocessing モジュール — プロセスベース並行処理・共有状態・プロセスプール | | |
|
|
226
227
|
|
|
227
228
|
---
|
|
228
229
|
|
|
@@ -238,4 +239,4 @@ FT172, FT176, FT180, FT184, FT188
|
|
|
238
239
|
|
|
239
240
|
---
|
|
240
241
|
|
|
241
|
-
*最終更新: 2026-05-21 (
|
|
242
|
+
*最終更新: 2026-05-21 (FT190 / v1.8.62)*
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# How-to: decimal モジュールと Unicode 数字入力
|
|
2
|
+
|
|
3
|
+
## Python の Decimal は Unicode 全角数字を受け入れる
|
|
4
|
+
|
|
5
|
+
`decimal.Decimal()` は Unicode の全角数字(U+FF10〜U+FF19: 0123456789)を
|
|
6
|
+
そのまま数値として解釈します。HTTP API を通じてユーザーが全角数字を入力した場合、
|
|
7
|
+
**Pydantic の `str` フィールドを通過してしまう**ことがあります。
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from decimal import Decimal
|
|
11
|
+
|
|
12
|
+
Decimal("123") # → Decimal('123') ← 正常に変換される
|
|
13
|
+
Decimal("1.5") # → Decimal('1.5')
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 問題: 想定外の入力が通過する可能性
|
|
17
|
+
|
|
18
|
+
金融計算 API で `price: str = Field(...)` としている場合、
|
|
19
|
+
クライアントが `"1000"` を送ると `Decimal("1000")` → `Decimal('1000')` として処理されます。
|
|
20
|
+
これ自体はエラーではありませんが、**入力の正規化が必要な場合**(ログ記録・DB 保存等)は
|
|
21
|
+
Unicode 正規化を行ってから処理してください。
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import unicodedata
|
|
25
|
+
from decimal import Decimal
|
|
26
|
+
|
|
27
|
+
def parse_decimal_safe(value: str) -> Decimal:
|
|
28
|
+
"""Unicode 正規化(NFKC)して Decimal に変換する."""
|
|
29
|
+
normalized = unicodedata.normalize("NFKC", value.strip())
|
|
30
|
+
return Decimal(normalized)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`unicodedata.normalize("NFKC", ...)` は全角数字を半角に変換します。
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
unicodedata.normalize("NFKC", "123") # → "123"
|
|
37
|
+
unicodedata.normalize("NFKC", "1.5") # → "1.5"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Pydantic でのバリデーション
|
|
41
|
+
|
|
42
|
+
Pydantic の `model_validator` を使って入力値の正規化を強制することを推奨します。
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from pydantic import BaseModel, Field, model_validator
|
|
46
|
+
|
|
47
|
+
class PriceRequest(BaseModel):
|
|
48
|
+
price: str = Field(max_length=20, description="価格(半角数字)")
|
|
49
|
+
|
|
50
|
+
@model_validator(mode="before")
|
|
51
|
+
@classmethod
|
|
52
|
+
def normalize_unicode(cls, values: dict) -> dict:
|
|
53
|
+
import unicodedata
|
|
54
|
+
if "price" in values and isinstance(values["price"], str):
|
|
55
|
+
values["price"] = unicodedata.normalize("NFKC", values["price"])
|
|
56
|
+
return values
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 関連 Issue
|
|
60
|
+
|
|
61
|
+
- [FT176] #500: parse_decimal_safe() の Unicode 全角数字受け入れ挙動を文書化
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# How-to: メールアドレスのパースと parseaddr() の挙動
|
|
2
|
+
|
|
3
|
+
## parseaddr() は寛容なパーサー
|
|
4
|
+
|
|
5
|
+
`email.utils.parseaddr()` は RFC 2822 準拠のフォーマット(`"Name <addr@example.com>"` 形式)を解析しますが、
|
|
6
|
+
**不正なアドレスを渡してもエラーを送出せず、空文字列を返します**。
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from email.utils import parseaddr
|
|
10
|
+
|
|
11
|
+
# 正常ケース
|
|
12
|
+
parseaddr("Alice <alice@example.com>") # → ("Alice", "alice@example.com")
|
|
13
|
+
parseaddr("alice@example.com") # → ("", "alice@example.com")
|
|
14
|
+
|
|
15
|
+
# 不正なアドレス — エラーにならず ("", "") を返す
|
|
16
|
+
parseaddr("not-an-email") # → ("", "")
|
|
17
|
+
parseaddr("") # → ("", "")
|
|
18
|
+
parseaddr("bad @ format") # → ("", "")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## HTTP 境界での検証は別途行うこと
|
|
22
|
+
|
|
23
|
+
`parseaddr()` の戻り値が空かどうかで有効性を確認しても、
|
|
24
|
+
**セキュリティ上の検証としては不十分**です。ユーザーが入力したアドレスは
|
|
25
|
+
Pydantic の `EmailStr` や正規表現で検証した後に `parseaddr()` を使ってください。
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import re
|
|
29
|
+
from email.utils import parseaddr
|
|
30
|
+
|
|
31
|
+
_EMAIL_RE = re.compile(r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$")
|
|
32
|
+
|
|
33
|
+
def validate_and_parse(raw: str) -> tuple[str, str] | None:
|
|
34
|
+
name, addr = parseaddr(raw)
|
|
35
|
+
if not addr or not _EMAIL_RE.match(addr):
|
|
36
|
+
return None
|
|
37
|
+
return name, addr
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## ヘッダーインジェクション対策
|
|
41
|
+
|
|
42
|
+
`Subject` や `From` ヘッダーに CR/LF (`\r\n`) が含まれると
|
|
43
|
+
**メールヘッダーインジェクション**が発生します。`email.message.EmailMessage` を使えば
|
|
44
|
+
自動的にエスケープされますが、`smtplib.sendmail()` に生文字列を渡す場合は
|
|
45
|
+
事前に CR/LF を除去してください。
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import re
|
|
49
|
+
_INJECT_RE = re.compile(r"[\r\n]")
|
|
50
|
+
|
|
51
|
+
def sanitize_header(value: str) -> str:
|
|
52
|
+
return _INJECT_RE.sub("", value)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 関連 Issue
|
|
56
|
+
|
|
57
|
+
- [FT182] #511: parseaddr() の寛容な挙動を How-to ドキュメントに記載
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# TODO — current
|
|
2
2
|
|
|
3
3
|
最終更新: 2026-05-21
|
|
4
|
-
現状: **v1.8.
|
|
4
|
+
現状: **v1.8.62 安定版 / フィールドトライアルループ継続中(FT190 完了)**
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## 状態サマリー
|
|
9
9
|
|
|
10
|
-
v1.8.
|
|
11
|
-
フィールドトライアルループは
|
|
10
|
+
v1.8.62 完了済み。FT190(multiprocessing — プロセスベース並行処理・共有状態・プロセスプール)を含む FT190 件を実施済み。
|
|
11
|
+
フィールドトライアルループは FT191 以降も継続中。
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -20,21 +20,7 @@ v1.8.60 完了済み。FT189(subprocess — 安全なプロセス実行・stdi
|
|
|
20
20
|
|
|
21
21
|
## オープン Issue(優先度順)
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|---|---|---|
|
|
25
|
-
| [#524](https://github.com/hideyukiMORI/nene2-python/issues/524) | [FT189] subprocess args に Null バイトが含まれる場合の OSError を 400 で返す | 中 |
|
|
26
|
-
| [#520](https://github.com/hideyukiMORI/nene2-python/issues/520) | [FT186] /flatten エンドポイントの内側リスト要素数に上限を追加 | 中 |
|
|
27
|
-
| [#517](https://github.com/hideyukiMORI/nene2-python/issues/517) | [FT184] DNS リバインディング攻撃への対策検討(TTL0 + IP 切り替え) | 低 |
|
|
28
|
-
| [#516](https://github.com/hideyukiMORI/nene2-python/issues/516) | [FT184] fetch_safe のリダイレクト SSRF 対策(Location ヘッダー先の IP 検証) | 中 |
|
|
29
|
-
| [#514](https://github.com/hideyukiMORI/nene2-python/issues/514) | [FT183] SmtpConfig.password を SecretStr に変更 | 低 |
|
|
30
|
-
| [#513](https://github.com/hideyukiMORI/nene2-python/issues/513) | [FT183] /send・/check-server の SSRF 対策(Private IP ブロック) | 中 |
|
|
31
|
-
| [#511](https://github.com/hideyukiMORI/nene2-python/issues/511) | [FT182] parseaddr() の寛容な挙動を How-to ドキュメントに記載 | 低 |
|
|
32
|
-
| [#510](https://github.com/hideyukiMORI/nene2-python/issues/510) | [FT182] CLAUDE.md に create_app() はファイル末尾に定義するルールを追記 | 中 |
|
|
33
|
-
| [#507](https://github.com/hideyukiMORI/nene2-python/issues/507) | [FT180] build_xml() の子タグ名にも NCName バリデーションを追加 | 低 |
|
|
34
|
-
| [#506](https://github.com/hideyukiMORI/nene2-python/issues/506) | [FT180] defusedxml を XML 処理の必須依存として CLAUDE.md に追記 | 中 |
|
|
35
|
-
| [#501](https://github.com/hideyukiMORI/nene2-python/issues/501) | [FT177] FastAPI アプリファクトリで APIRouter パターンを標準化 | 中 |
|
|
36
|
-
| [#500](https://github.com/hideyukiMORI/nene2-python/issues/500) | [FT176] parse_decimal_safe() の Unicode 全角数字受け入れ挙動を文書化 | 低 |
|
|
37
|
-
| [#499](https://github.com/hideyukiMORI/nene2-python/issues/499) | [FT176] calculate_tax/discount にビジネスロジックバリデーション追加 | 中 |
|
|
23
|
+
なし(すべて解消済み)
|
|
38
24
|
|
|
39
25
|
---
|
|
40
26
|
|
|
@@ -42,6 +28,8 @@ v1.8.60 完了済み。FT189(subprocess — 安全なプロセス実行・stdi
|
|
|
42
28
|
|
|
43
29
|
| バージョン | 主な内容 |
|
|
44
30
|
|---|---|
|
|
31
|
+
| v1.8.62 | FT190: multiprocessing — プロセスベース並行処理・共有状態・プロセスプール |
|
|
32
|
+
| v1.8.61 | バックログ Issue 一括解消(CLAUDE.md ルール更新・FT サンドボックス修正・ドキュメント追記) |
|
|
45
33
|
| v1.8.60 | FT189: subprocess — 安全なプロセス実行・stdin/stdout 制御・ストリーミング(セキュリティ診断、Issue #524) |
|
|
46
34
|
| v1.8.59 | FT188: threading — Thread・Lock・RLock・Semaphore・Event・ThreadPoolExecutor・Queue・Timer(クラッカーペンテスト) |
|
|
47
35
|
| v1.8.58 | FT187: collections — Counter・defaultdict・deque・ChainMap・NamedTuple・OrderedDict |
|
|
@@ -65,12 +53,12 @@ v1.8.60 完了済み。FT189(subprocess — 安全なプロセス実行・stdi
|
|
|
65
53
|
|
|
66
54
|
## フィールドトライアル進捗
|
|
67
55
|
|
|
68
|
-
**実施済み**: FT1〜
|
|
56
|
+
**実施済み**: FT1〜FT190(全 190 件)
|
|
69
57
|
|
|
70
58
|
索引: [`docs/field-trials/INDEX.md`](../field-trials/INDEX.md)
|
|
71
59
|
|
|
72
60
|
**次のアクション**:
|
|
73
|
-
-
|
|
61
|
+
- FT191 以降を継続(FT191 は 191 % 3 = 2 → 診断なし、191 % 4 = 3 → ペンテストなし)
|
|
74
62
|
|
|
75
63
|
---
|
|
76
64
|
|
|
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.60 → nene2_python-1.8.62}/alembic/versions/001_create_notes_and_tags_tables.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|