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