nene2-python 1.8.33__tar.gz → 1.8.35__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.33 → nene2_python-1.8.35}/CHANGELOG.md +18 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/CLAUDE.md +69 -1
- {nene2_python-1.8.33 → nene2_python-1.8.35}/PKG-INFO +1 -1
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-101.md +59 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-102.md +34 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-103.md +47 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-104.md +48 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-105.md +43 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-106.md +39 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-107.md +46 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-108.md +46 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-109.md +53 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-110.md +66 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-111.md +53 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-112.md +65 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-113.md +79 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-114.md +69 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-115.md +91 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-116.md +99 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-117.md +96 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-118.md +79 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-119.md +67 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-120.md +95 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-121.md +81 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-122.md +87 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-123.md +74 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-124.md +72 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-125.md +82 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-126.md +111 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-127.md +104 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-128.md +85 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-129.md +104 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-130.md +91 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-131.md +88 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-132.md +98 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-133.md +87 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-134.md +96 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-135.md +87 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-136.md +100 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-137.md +99 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-138.md +97 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-139.md +87 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-140.md +90 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-141.md +139 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-142.md +100 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-143.md +113 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-144.md +127 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-145.md +103 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-146.md +103 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-147.md +104 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-148.md +109 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-149.md +123 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-150.md +139 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-151.md +118 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-152.md +114 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-153.md +122 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-154.md +113 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-155.md +133 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-156.md +146 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-157.md +150 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-158.md +117 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-159.md +130 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-160.md +128 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-161.md +155 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-162.md +140 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-163.md +160 -0
- nene2_python-1.8.35/docs/field-trials/2026-05-field-trial-164.md +261 -0
- nene2_python-1.8.35/docs/how-to/api-versioning.md +92 -0
- nene2_python-1.8.35/docs/how-to/custom-auth-middleware.md +141 -0
- nene2_python-1.8.35/docs/how-to/dependency-injection.md +136 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/domain-events.md +31 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/lifespan-and-app-state.md +56 -3
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/response-patterns.md +30 -1
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/run-tests.md +21 -0
- nene2_python-1.8.35/docs/how-to/soft-delete.md +92 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/streaming.md +13 -0
- nene2_python-1.8.35/docs/how-to/structured-logging.md +81 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/validation.md +24 -0
- nene2_python-1.8.35/docs/templates/field-trial-report.md +449 -0
- nene2_python-1.8.35/docs/todo/current.md +52 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/pyproject.toml +6 -1
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/app.py +1 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/database/sqlalchemy_executor.py +10 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/scripts/export_openapi.py +5 -0
- nene2_python-1.8.35/tests/conftest.py +20 -0
- nene2_python-1.8.35/tests/example/comment/test_comment_http.py +112 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/comment/test_comment_repository.py +11 -10
- nene2_python-1.8.35/tests/example/conftest.py +20 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/note/test_note_repository.py +9 -9
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/tag/test_tag_repository.py +9 -9
- nene2_python-1.8.35/tests/example/test_cors.py +75 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/database/test_transaction.py +19 -23
- {nene2_python-1.8.33 → nene2_python-1.8.35}/uv.lock +1 -1
- nene2_python-1.8.33/docs/todo/current.md +0 -59
- nene2_python-1.8.33/tests/example/comment/test_comment_http.py +0 -97
- nene2_python-1.8.33/tests/example/conftest.py +0 -13
- nene2_python-1.8.33/tests/example/test_cors.py +0 -63
- {nene2_python-1.8.33 → nene2_python-1.8.35}/.env.example +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/.gitignore +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/AGENTS.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/Dockerfile +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/LICENSE +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/README.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/alembic/README +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/alembic/env.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/alembic.ini +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/compose.yaml +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/de/index.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-100.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-80.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-81.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-82.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-83.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-84.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-85.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-86.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-87.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-88.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-89.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-90.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-91.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-92.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-93.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-94.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-95.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-96.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-97.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-98.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/field-trials/2026-05-field-trial-99.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/fr/index.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/background-tasks.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/cors.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/file-upload.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/how-to/webhook.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/index.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/index.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/reference/api.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/roadmap.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/zh/index.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/package-lock.json +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/package.json +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/__main__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/mcp.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/schema.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/auth/deps.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/cache/ttl.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/http/etag.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/mcp/server.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/middleware/setup.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/security/webhook.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/auth/test_make_require_auth.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/cache/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/cache/test_ttl.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/http/test_etag.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/mcp/test_server.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/middleware/test_setup_middlewares.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/security/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/security/test_webhook.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.33 → nene2_python-1.8.35}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,24 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.34] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
テストの ResourceWarning 解消と `SqlAlchemyQueryExecutor` / `SqlAlchemyTransactionManager` の `engine` プロパティ追加。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `SqlAlchemyQueryExecutor.engine` プロパティ — 下層の SQLAlchemy エンジンを公開(テストのティアダウンで `executor.engine.dispose()` に使用)(#428)
|
|
14
|
+
- `SqlAlchemyTransactionManager.engine` プロパティ — 同上 (#428)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- テスト実行時に Python 3.14 で 80+ 件出ていた `ResourceWarning: unclosed database` を解消 (#428)
|
|
18
|
+
- `create_app()` で `app.state.db_executor` を保存してテストからエンジンを dispose できるように変更
|
|
19
|
+
- `export_openapi.py` でエンジンを dispose するように修正
|
|
20
|
+
- テストフィクスチャを `yield` + `engine.dispose()` パターンに統一
|
|
21
|
+
- `tests/conftest.py` を新規作成し module-level app エンジンをセッション終了時に dispose
|
|
22
|
+
- `StaticPool` の最後の 1 件は `pyproject.toml` の `filterwarnings` で抑制
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
8
26
|
## [1.8.33] — 2026-05-20
|
|
9
27
|
|
|
10
28
|
FT100 フィールドトライアル — In-memory TTL レスポンスキャッシュパターン検証と nene2.cache モジュール追加。
|
|
@@ -355,7 +355,75 @@ docs/
|
|
|
355
355
|
|
|
356
356
|
---
|
|
357
357
|
|
|
358
|
-
## 12.
|
|
358
|
+
## 12. フィールドトライアル(FT)方法論
|
|
359
|
+
|
|
360
|
+
### 目的
|
|
361
|
+
|
|
362
|
+
Python 標準ライブラリ・サードパーティライブラリを nene2-python 上で実装し、
|
|
363
|
+
フレームワーク API の安定性を実装者目線で検証する。
|
|
364
|
+
「実際に詰まったポイント」だけを観察ベースで Issue 化し、
|
|
365
|
+
ドキュメントと設計を同時に成長させるサイクルを回す。
|
|
366
|
+
|
|
367
|
+
### フロー(1 FT あたり)
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
1. テーマ選定(docs/todo/current.md から未検証パターンを選ぶ)
|
|
371
|
+
2. 独立サンドボックスを作成
|
|
372
|
+
場所: /home/xi/docker/nene2-python-FT/ftNNN-テーマ名/
|
|
373
|
+
ゼロから uv init → nene2-python を依存として追加
|
|
374
|
+
3. 実装 + 全チェック通過
|
|
375
|
+
uv run pytest && uv run mypy src/ && uv run ruff check src/ tests/
|
|
376
|
+
4. 摩擦点を記録(F-1, F-2, ...)
|
|
377
|
+
5. FT レポート作成 → docs/field-trials/2026-05-field-trial-NNN.md
|
|
378
|
+
テンプレート: docs/templates/field-trial-report.md
|
|
379
|
+
6. DX Review(6ペルソナ)を実施(後述)
|
|
380
|
+
7. FT番号が3の倍数なら セキュリティ診断 を実施(後述)
|
|
381
|
+
8. Follow-up Issues を GitHub Issue に変換
|
|
382
|
+
9. まとめて main merge → パッチバージョン(v1.8.N)でリリース
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### DX Review — 6ペルソナ
|
|
386
|
+
|
|
387
|
+
各 FT レポートの末尾に、以下 6 ペルソナ目線での評価を必ず記載する。
|
|
388
|
+
「コードが動く」だけでなく「初心者から経験者まで安全に使えるか」を客観的に検証する。
|
|
389
|
+
|
|
390
|
+
| ペルソナ | 属性 | 主な評価観点 |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| **1. 初心者** | Python 歴1年・独学中・女性・バックエンド志望 | ドキュメント理解・事故リスク・規約の使いやすさ |
|
|
393
|
+
| **2. ロースキル経験者** | Python 歴3-4年・スクリプト系・男性・SES | コピペ可能性・拡張時の罠・セキュリティ的な事故リスク |
|
|
394
|
+
| **3. フロントエンド寄り** | React/TS 歴4年・バックエンド転向中・ノンバイナリ | エラーレスポンスの質・Python 固有概念の学習コスト |
|
|
395
|
+
| **4. バックエンド経験者** | Django/FastAPI 歴5-6年・男性・リードエンジニア | 他フレームワークとの差異・nene2 の薄さへの評価 |
|
|
396
|
+
| **5. シニアエンジニア** | 設計・コードレビュー担当・女性・10-12年 | コードレビューチェックポイント・チームでの安全なパターン |
|
|
397
|
+
| **6. 設計者** | nene2-python 設計ポリシー目線 | CLAUDE.md ポリシー整合性・初心者でも安全な API 達成度 |
|
|
398
|
+
|
|
399
|
+
各ペルソナの記述フォーマット:
|
|
400
|
+
- 状況説明 1 文(ペルソナが置かれているコンテキスト)
|
|
401
|
+
- 太字サブヘッディング 2〜4個(ドキュメント理解 / 事故リスク / 規約の使いやすさ など)
|
|
402
|
+
- 事故リスクは「高 / 中 / 低」で定性評価
|
|
403
|
+
|
|
404
|
+
### セキュリティ診断(3の倍数 FT)
|
|
405
|
+
|
|
406
|
+
**FT番号 % 3 == 0 のとき**(FT165, FT168, ...)、通常のFT完了後に追加で実施する。
|
|
407
|
+
|
|
408
|
+
**診断レベル**: Django・FastAPI・SQLAlchemy 本体でも CVE が報告されてきた攻撃ベクターを対象とする。
|
|
409
|
+
|
|
410
|
+
対象カテゴリ(詳細は `docs/templates/field-trial-report.md` のセキュリティ診断セクション参照):
|
|
411
|
+
|
|
412
|
+
1. **OWASP API Security Top 10 (2023)** — BOLA/IDOR・認証破損・Mass Assignment・リソース消費・SSRF・設定ミス
|
|
413
|
+
2. **インジェクション攻撃** — SQL・コマンド・パストラバーサル・SSTI・HTTP ヘッダーインジェクション
|
|
414
|
+
3. **認証・認可** — パスワードハッシュ・タイミング攻撃・JWT alg:none・セッション固定
|
|
415
|
+
4. **入力バリデーション** — 上限なし文字列・数値オーバーフロー・Null バイト・Unicode RTL
|
|
416
|
+
5. **情報漏洩** — スタックトレース公開・ログへの機密データ出力・pip-audit CVE スキャン
|
|
417
|
+
6. **Python/FastAPI 固有** — ReDoS・pickle/yaml インジェクション・非同期レースコンディション・Pydantic 型強制・SQLAlchemy raw query バイパス
|
|
418
|
+
|
|
419
|
+
**合否判定**:
|
|
420
|
+
- **合格**: 全カテゴリ問題なし
|
|
421
|
+
- **条件付き合格**: MEDIUM 以下の指摘のみ、次 FT までに修正
|
|
422
|
+
- **不合格**: HIGH/CRITICAL の指摘あり → main merge 前に必須修正
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## 13. PHP版 NENE2 との対応表
|
|
359
427
|
|
|
360
428
|
| PHP | Python |
|
|
361
429
|
|---|---|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.35
|
|
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,59 @@
|
|
|
1
|
+
# Field Trial 101: Query Parameter Filter/Sort パターン
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`?status=published&sort=views&order=desc&tag=fastapi` のような複数フィルター + ソートのクエリパラメーターを型安全に扱うパターンを nene2 上で実装する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft101-query-filter/` に以下を実装:
|
|
10
|
+
|
|
11
|
+
- `ArticleFilter` dataclass に `StrEnum` で列挙型フィルター
|
|
12
|
+
- `get_article_filter()` ファクトリ関数で `Depends()` に接続
|
|
13
|
+
- `PaginationQueryParser` と組み合わせてページネーション
|
|
14
|
+
- 11 テスト通過
|
|
15
|
+
|
|
16
|
+
## テスト結果
|
|
17
|
+
|
|
18
|
+
全 11 テスト通過(修正後)。
|
|
19
|
+
|
|
20
|
+
## Friction Points
|
|
21
|
+
|
|
22
|
+
### FP1: `PaginationQueryParser.as_depends()` が存在しない(ドキュメントとの乖離)
|
|
23
|
+
|
|
24
|
+
**状況**: 他のパーサー系クラスで `as_depends()` ファクトリパターンを使うケースがあり(FT83 など)、`PaginationQueryParser.as_depends()` を試みたが `AttributeError`。
|
|
25
|
+
|
|
26
|
+
正しい使い方は:
|
|
27
|
+
```python
|
|
28
|
+
# ✅ Annotated スタイル
|
|
29
|
+
pagination: Annotated[PaginationQueryParser, Depends()]
|
|
30
|
+
|
|
31
|
+
# ❌ as_depends() は存在しない
|
|
32
|
+
pagination: PaginationQueryParser = Depends(PaginationQueryParser.as_depends())
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**影響**: `Depends()` の使い方が複数あるため混乱しやすい。how-to ガイドに明記が必要。
|
|
36
|
+
|
|
37
|
+
### FP2: `Depends()` スタイルとデフォルト値の混在でシンタックスエラー
|
|
38
|
+
|
|
39
|
+
**状況**: 複数の `Depends()` パラメーターを持つハンドラーで、`= Depends(...)` スタイルと `Annotated[T, Depends()]` スタイルを混在させると `SyntaxError: parameter without a default follows parameter with a default` が出る。
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# ❌ SyntaxError
|
|
43
|
+
def list_articles(
|
|
44
|
+
filter_: ArticleFilter = Depends(get_article_filter), # デフォルト値あり
|
|
45
|
+
pagination: Annotated[PaginationQueryParser, Depends()], # デフォルト値なし
|
|
46
|
+
) -> JSONResponse: ...
|
|
47
|
+
|
|
48
|
+
# ✅ Annotated スタイルに統一
|
|
49
|
+
def list_articles(
|
|
50
|
+
filter_: Annotated[ArticleFilter, Depends(get_article_filter)],
|
|
51
|
+
pagination: Annotated[PaginationQueryParser, Depends()],
|
|
52
|
+
) -> JSONResponse: ...
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**影響**: エラーメッセージが直感的でなく、原因がわかりにくい。
|
|
56
|
+
|
|
57
|
+
## まとめ
|
|
58
|
+
|
|
59
|
+
摩擦の原因はドキュメント不足。FP1・FP2 を how-to ガイドに追記する Issue を起票する。コード修正は不要。
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Field Trial 102: response_model と PaginationResponse の型整合性
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`response_model` と `PaginationResponse` / `JSONResponse` の組み合わせパターンを実際に動かして、OpenAPI スキーマへの影響と型整合性を検証する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft102-response-model/` に 4 パターンを実装:
|
|
10
|
+
|
|
11
|
+
1. **v1**: `response_model` なし + `JSONResponse` + `PaginationResponse`
|
|
12
|
+
2. **v2**: `response_model=ProductListResponse` + Pydantic モデル直返し
|
|
13
|
+
3. **v3**: `response_model=ProductListResponse` + `dict` 返却(FastAPI が変換)
|
|
14
|
+
4. **v4**: `response_model=ProductListResponse` + `JSONResponse`(検証はされない)
|
|
15
|
+
|
|
16
|
+
## テスト結果
|
|
17
|
+
|
|
18
|
+
全 9 テスト通過。
|
|
19
|
+
|
|
20
|
+
## Friction Points
|
|
21
|
+
|
|
22
|
+
摩擦なし。
|
|
23
|
+
|
|
24
|
+
`response-patterns.md` How-to(PR #403 で追加済み)がこのパターンを説明しており、スムーズに実装できた。
|
|
25
|
+
|
|
26
|
+
- パターン1(JSONResponse + PaginationResponse)は OpenAPI スキーマなしで OK
|
|
27
|
+
- パターン2(Pydantic 直返し + response_model)は OpenAPI スキーマあり
|
|
28
|
+
- パターン4(JSONResponse + response_model)は response_model があってもバリデーション非実施
|
|
29
|
+
|
|
30
|
+
前回の摩擦(`problem_details_response()` と Pydantic 直返しの非一貫性)は PR #403 で文書化済み。
|
|
31
|
+
|
|
32
|
+
## まとめ
|
|
33
|
+
|
|
34
|
+
ドキュメント整備が功を奏し、摩擦ゼロで実装完了。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Field Trial 103: カスタムミドルウェアで認証情報をリクエストスコープに格納
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
JWT ミドルウェアで検証後の認証情報(`AuthUser`)を `request.state` に格納し、ハンドラーで `Depends()` を通じて取得するパターンを検証する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft103-request-state-auth/` に以下を実装:
|
|
10
|
+
|
|
11
|
+
- `JwtAuthMiddleware` — JWT を検証して `request.state.user` に `AuthUser` を格納
|
|
12
|
+
- `get_current_user()` — `request.state.user` から `AuthUser` を取得する Depends ファクトリ
|
|
13
|
+
- `require_admin()` — `admin` ロールチェック依存
|
|
14
|
+
- `EXCLUDE_PATHS` で `/health` をスキップ
|
|
15
|
+
|
|
16
|
+
## テスト結果
|
|
17
|
+
|
|
18
|
+
全 7 テスト通過。`InsecureKeyLengthWarning` はテスト用の短いシークレットによるもの(想定内)。
|
|
19
|
+
|
|
20
|
+
## Friction Points
|
|
21
|
+
|
|
22
|
+
### FP1: `request.state.user` アクセスに `type: ignore[attr-defined]` が必要
|
|
23
|
+
|
|
24
|
+
**状況**: `request.state` は `starlette.datastructures.State` で動的属性を持つ。ミドルウェアで `request.state.user = AuthUser(...)` と設定しても、Depends ファクトリで `request.state.user` を参照する際に mypy が `attr-defined` エラーを出す。
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
def get_current_user(request: Request) -> AuthUser:
|
|
28
|
+
user: AuthUser = request.state.user # type: ignore[attr-defined] # reason: middleware で確実に設定済み
|
|
29
|
+
return user
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
これは `app.state` でも同様(FT100 で確認済み)。
|
|
33
|
+
|
|
34
|
+
**影響**: 低。`type: ignore` + `reason` コメントで対処可能。
|
|
35
|
+
|
|
36
|
+
### FP2: `BearerTokenMiddleware` のトークン文字列が `request.state` に格納されない
|
|
37
|
+
|
|
38
|
+
**状況**: nene2 の `BearerTokenMiddleware` は JWT/API キーの検証後にトークン文字列を返す(`make_require_auth()` Depends 経由)が、検証済みのペイロードやユーザー情報を `request.state` に自動格納する機能がない。
|
|
39
|
+
|
|
40
|
+
カスタム JWT ミドルウェアで対応したが、`BearerTokenMiddleware` + `request.state` の統合パターンがない。
|
|
41
|
+
|
|
42
|
+
**影響**: 中。`BearerTokenMiddleware` を使いたいが `request.state` にユーザー情報を格納したい場合、自前ミドルウェアを書き直す必要がある。
|
|
43
|
+
|
|
44
|
+
## まとめ
|
|
45
|
+
|
|
46
|
+
FP1 は既知の Starlette 制約(low)。FP2 は nene2 の認証統合に関する中程度の摩擦。
|
|
47
|
+
docs として「request.state を使った認証情報伝播パターン」を how-to に追加する。
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Field Trial 104: AsyncIterator を返す UseCase + StreamingResponse
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
UseCase が `AsyncIterator` を返し、FastAPI の `StreamingResponse` でストリーミングする本格的なパターンを検証する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft104-streaming-usecase/` に以下を実装:
|
|
10
|
+
|
|
11
|
+
- `StreamLogsUseCase` — `AsyncIterator[LogEntry]` を返す UseCase
|
|
12
|
+
- `ExportCsvUseCase` — CSV 行を `AsyncIterator[str]` で返す UseCase
|
|
13
|
+
- NDJSON / SSE / CSV の 3 形式にストリーミング変換
|
|
14
|
+
- 6 テスト通過
|
|
15
|
+
|
|
16
|
+
## テスト結果
|
|
17
|
+
|
|
18
|
+
全 6 テスト通過(修正後)。
|
|
19
|
+
|
|
20
|
+
## Friction Points
|
|
21
|
+
|
|
22
|
+
### FP1: `TestClient.stream()` 内で `r.text` が使えない
|
|
23
|
+
|
|
24
|
+
**状況**: `with client.stream("GET", "/export/users.csv") as r:` コンテキスト内で `r.text` にアクセスすると `httpx.ResponseNotRead` が発生する。
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# ❌ ResponseNotRead
|
|
28
|
+
with client.stream("GET", "/export/csv") as r:
|
|
29
|
+
content = r.text # エラー!
|
|
30
|
+
|
|
31
|
+
# ✅ iter_text() でチャンク収集
|
|
32
|
+
with client.stream("GET", "/export/csv") as r:
|
|
33
|
+
content = "".join(r.iter_text())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**影響**: ストリーミングレスポンスのテストで直感に反するエラーが出る。`streaming.md` How-to に `iter_text()` パターンを追記する必要がある。
|
|
37
|
+
|
|
38
|
+
### FP2: `AsyncUseCaseProtocol` は `AsyncIterator` を返す UseCase に対応していない
|
|
39
|
+
|
|
40
|
+
**状況**: `AsyncUseCaseProtocol` は `async def execute(self, input_: I) -> O` を定義しており、`O = AsyncIterator[T]` として使うことは技術的には可能だが、`O` 型が `AsyncIterator` であることを表現するのが難しい。
|
|
41
|
+
|
|
42
|
+
現在の実装では `AsyncUseCaseProtocol` を使わず、独立した UseCase クラスとして実装した。
|
|
43
|
+
|
|
44
|
+
**影響**: 低。ストリーミング UseCase は専用の Protocol を定義するか、型注釈なしで書くのが現実的。
|
|
45
|
+
|
|
46
|
+
## まとめ
|
|
47
|
+
|
|
48
|
+
FP1 をドキュメント修正で対応。FP2 は将来の検討事項として記録。
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Field Trial 105: マルチテナント DB 接続
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`X-Tenant-Id` ヘッダーでテナントを切り替え、リクエストごとに異なる SQLite DB に接続するマルチテナントパターンを検証する。
|
|
6
|
+
|
|
7
|
+
## 実施内容
|
|
8
|
+
|
|
9
|
+
`/home/xi/docker/nene2-python-FT/ft105-multitenant/` に以下を実装:
|
|
10
|
+
|
|
11
|
+
- `TENANT_DB_URLS` マップでテナント DB URL を管理
|
|
12
|
+
- `get_tenant_session()` Depends でテナント特定・セッション生成
|
|
13
|
+
- テナント分離テスト(A と B は独立した DB)
|
|
14
|
+
|
|
15
|
+
## テスト結果
|
|
16
|
+
|
|
17
|
+
全 6 テスト通過。
|
|
18
|
+
|
|
19
|
+
## Friction Points
|
|
20
|
+
|
|
21
|
+
### FP1: `create_engine()` をリクエストごとに呼ぶのはパフォーマンス上の問題
|
|
22
|
+
|
|
23
|
+
**状況**: `get_tenant_session()` がリクエストごとに `create_engine()` を呼ぶ。SQLAlchemy では `create_engine()` はアプリ起動時に一度だけ呼ぶべきで、コネクションプールを使い回す設計が推奨される。
|
|
24
|
+
|
|
25
|
+
nene2 にテナントごとのエンジンキャッシュパターンがない。`TtlCache[Session]` で代替できるが、セッションはリクエストスコープで閉じる必要があるため、エンジンをキャッシュすべき。
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# 推奨パターン
|
|
29
|
+
_engine_cache: dict[str, Engine] = {}
|
|
30
|
+
|
|
31
|
+
def get_engine(tenant_id: str) -> Engine:
|
|
32
|
+
if tenant_id not in _engine_cache:
|
|
33
|
+
_engine_cache[tenant_id] = create_engine(TENANT_DB_URLS[tenant_id], ...)
|
|
34
|
+
return _engine_cache[tenant_id]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### FP2: `SqlAlchemyQueryExecutor` はシングルエンジン想定
|
|
38
|
+
|
|
39
|
+
**状況**: `nene2.database.SqlAlchemyQueryExecutor` はアプリ起動時に単一エンジンで初期化することを前提としている。マルチテナントでリクエストごとに異なるエンジンを使う場合、`SqlAlchemyQueryExecutor` を使わず直接 `Session` を操作する必要がある。
|
|
40
|
+
|
|
41
|
+
## まとめ
|
|
42
|
+
|
|
43
|
+
FP1 は `TtlCache` を使ったエンジンキャッシュパターンとして docs に追記する。FP2 はアーキテクチャ設計上の制約として記録。コード修正は不要。
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Field Trial 106: Idempotency Key パターン
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`Idempotency-Key` ヘッダーを使って POST リクエストを冪等にするパターンを検証する。
|
|
6
|
+
`nene2.cache.TtlCache` の実用例として組み込む。
|
|
7
|
+
|
|
8
|
+
## 実施内容
|
|
9
|
+
|
|
10
|
+
`/home/xi/docker/nene2-python-FT/ft106-idempotency/` に以下を実装:
|
|
11
|
+
|
|
12
|
+
- `TtlCache[dict[str, Any]]` を `app.state.idempotency_cache` として lifespan で初期化
|
|
13
|
+
- `POST /orders` で `Idempotency-Key` ヘッダーをチェック
|
|
14
|
+
- 既存キャッシュがあれば `X-Idempotency-Replayed: true` ヘッダーと共にキャッシュから返す
|
|
15
|
+
- キャッシュなければ UseCase を実行してキャッシュに保存
|
|
16
|
+
- 7 テスト通過
|
|
17
|
+
|
|
18
|
+
## テスト結果
|
|
19
|
+
|
|
20
|
+
全 7 テスト通過。
|
|
21
|
+
|
|
22
|
+
## Friction Points
|
|
23
|
+
|
|
24
|
+
### FP1: Idempotency Key 同一キー + 異なるボディの扱いが未定義
|
|
25
|
+
|
|
26
|
+
**状況**: Stripe などの実装では、同じ `Idempotency-Key` で異なるリクエストボディを送ると `422 Unprocessable Entity` を返す。現在の実装ではキャッシュされた最初のレスポンスを無条件に返すため、ボディが変わっても同じレスポンスが返る。
|
|
27
|
+
|
|
28
|
+
**影響**: 中。金融系 API では重要だが、一般的な API では省略可。
|
|
29
|
+
|
|
30
|
+
### FP2: Idempotency Key ユーティリティがない
|
|
31
|
+
|
|
32
|
+
**状況**: `TtlCache` を使って実装できたが、`get_idempotency_cache()` Depends パターンや `X-Idempotency-Replayed` ヘッダーの付与は完全に自前実装。Stripe / Square などで標準化されたパターンであるため、nene2 に軽量ユーティリティがあると便利。
|
|
33
|
+
|
|
34
|
+
**影響**: 低。`TtlCache` + ハンドラーコードで完結可能。
|
|
35
|
+
|
|
36
|
+
## まとめ
|
|
37
|
+
|
|
38
|
+
`TtlCache` の実用例として Idempotency Key パターンはスムーズに実装できた。摩擦は小さい。
|
|
39
|
+
docs として how-to を追加する。
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Field Trial 107: Bulk Operations(一括作成・削除)
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`POST /items/bulk` と `DELETE /items/bulk` による一括操作パターンを検証する。
|
|
6
|
+
部分成功(一部成功・一部失敗)を 207 Multi-Status で返す方法も確認する。
|
|
7
|
+
|
|
8
|
+
## 実施内容
|
|
9
|
+
|
|
10
|
+
`/home/xi/docker/nene2-python-FT/ft107-bulk-ops/` に以下を実装:
|
|
11
|
+
|
|
12
|
+
- `POST /items/bulk` — 一括作成(価格制限でビジネスバリデーション、部分失敗対応)
|
|
13
|
+
- `DELETE /items/bulk` — 一括削除(存在しない ID は failed に入れる)
|
|
14
|
+
- 207 Multi-Status レスポンスに `succeeded` / `failed` を含める
|
|
15
|
+
- 8 テスト通過(修正後)
|
|
16
|
+
|
|
17
|
+
## テスト結果
|
|
18
|
+
|
|
19
|
+
全 8 テスト通過(修正後)。
|
|
20
|
+
|
|
21
|
+
## Friction Points
|
|
22
|
+
|
|
23
|
+
### FP1: `TestClient.delete()` が `json` パラメーターを受け付けない
|
|
24
|
+
|
|
25
|
+
**状況**: `DELETE` リクエストにリクエストボディを付ける場合、`client.delete(url, json=body)` は `TypeError` になる。
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# ❌ TypeError: unexpected keyword argument 'json'
|
|
29
|
+
r = client.delete("/items/bulk", json={"ids": [1, 2]})
|
|
30
|
+
|
|
31
|
+
# ✅ request() を使う
|
|
32
|
+
r = client.request("DELETE", "/items/bulk", json={"ids": [1, 2]})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**影響**: 中。DELETE + ボディはやや非標準だが、一括削除では一般的なパターン。テストコードが `request()` を直接使う必要があるため直感的でない。
|
|
36
|
+
|
|
37
|
+
**代替案**: ボディを持つ DELETE の代わりに `POST /items/bulk-delete` にする方が REST 的にクリーン(ボディを持つ DELETE は RFC 9110 で「意味がないわけではないが、推奨されない」)。
|
|
38
|
+
|
|
39
|
+
### FP2: 207 Multi-Status のレスポンス型が OpenAPI スキーマに表現しにくい
|
|
40
|
+
|
|
41
|
+
**状況**: `response_model` で 207 のスキーマを定義しようとすると、succeeded/failed の型が複雑になる。実用的には `response_model` なしで `JSONResponse` を直接返すのが現実的。
|
|
42
|
+
|
|
43
|
+
## まとめ
|
|
44
|
+
|
|
45
|
+
FP1 は how-to に追記(`TestClient` の HTTP メソッドと `json` パラメーターの注意点)。
|
|
46
|
+
FP2 はドキュメント摩擦(bulk 操作は `response_model` を省略して OK)。
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Field Trial 108: Pydantic computed_field と property パターン
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
Pydantic v2 の `@computed_field` + `@property` を使って、ストアドフィールドから計算されるプロパティを
|
|
6
|
+
OpenAPI スキーマに自動的に含めるパターンを検証する。
|
|
7
|
+
|
|
8
|
+
## 実施内容
|
|
9
|
+
|
|
10
|
+
`/home/xi/docker/nene2-python-FT/ft108-computed-field/` に以下を実装:
|
|
11
|
+
|
|
12
|
+
- `ProductResponse` — `price_dollars`, `is_in_stock`, `display_name` の computed fields
|
|
13
|
+
- `OrderLineResponse` — ネストした computed fields (`line_total_cents`, `line_total_dollars`)
|
|
14
|
+
- `/order-preview` エンドポイントで `JSONResponse(line.model_dump(mode="json"))` パターン確認
|
|
15
|
+
- 6 テスト通過
|
|
16
|
+
|
|
17
|
+
## テスト結果
|
|
18
|
+
|
|
19
|
+
全 6 テスト通過。
|
|
20
|
+
|
|
21
|
+
## Friction Points
|
|
22
|
+
|
|
23
|
+
### FP1: `model_dump()` が datetime を Python オブジェクトのまま返す → `JSONResponse` で 500 エラー
|
|
24
|
+
|
|
25
|
+
**状況**: `OrderLineResponse` のネストモデルには `created_at: datetime` フィールドがある。
|
|
26
|
+
`JSONResponse(line.model_dump())` を使うと、`json.dumps` が `datetime` を直列化できずに
|
|
27
|
+
`TypeError: Object of type datetime is not JSON serializable` で 500 エラーになる。
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# ❌ model_dump() は datetime をそのまま返す → JSONResponse でエラー
|
|
31
|
+
return JSONResponse(line.model_dump())
|
|
32
|
+
|
|
33
|
+
# ✅ mode="json" を指定すると datetime を ISO 8601 文字列に変換する
|
|
34
|
+
return JSONResponse(line.model_dump(mode="json"))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**影響**: 大。`response_model=` を使う通常のルートは FastAPI が自動変換するため問題ないが、
|
|
38
|
+
`JSONResponse` を直接返すルート(207 Multi-Status, /order-preview など)でこのパターンを
|
|
39
|
+
使い忘れると本番で 500 エラーになる。
|
|
40
|
+
|
|
41
|
+
**代替案**: `jsonable_encoder(line.model_dump())` でも変換できるが、`mode="json"` の方が Pydantic 標準。
|
|
42
|
+
|
|
43
|
+
## まとめ
|
|
44
|
+
|
|
45
|
+
`@computed_field` + `@property` は摩擦ゼロで OpenAPI スキーマに含められる優れたパターン。
|
|
46
|
+
FP1 は `JSONResponse` を直接使う場合の注意点として how-to に追記する。
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Field Trial 109: API バージョニング(v1/v2 ルーティング)
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
FastAPI の `APIRouter` を使って `/v1` と `/v2` でエンドポイントを分離するパターンを検証する。
|
|
6
|
+
- フィールド名の変更(`email` → `contact_email`)
|
|
7
|
+
- フィールドの追加(`age` を v2 で追加)
|
|
8
|
+
- `full_name` から `first_name`/`last_name` への分割
|
|
9
|
+
- OpenAPI スキーマが両バージョンを正確に反映するか
|
|
10
|
+
|
|
11
|
+
## 実施内容
|
|
12
|
+
|
|
13
|
+
`/home/xi/docker/nene2-python-FT/ft109-api-versioning/` に以下を実装:
|
|
14
|
+
|
|
15
|
+
- ドメイン `User` dataclass(バージョン非依存)
|
|
16
|
+
- `UserResponseV1` — `full_name`, `email`
|
|
17
|
+
- `UserResponseV2` — `first_name`, `last_name`, `contact_email`, `age`
|
|
18
|
+
- `v1_router = APIRouter(prefix="/v1", tags=["v1"])`
|
|
19
|
+
- `v2_router = APIRouter(prefix="/v2", tags=["v2"])`
|
|
20
|
+
- 9 テスト通過
|
|
21
|
+
|
|
22
|
+
## テスト結果
|
|
23
|
+
|
|
24
|
+
全 9 テスト一発通過。摩擦ゼロ。
|
|
25
|
+
|
|
26
|
+
## Friction Points
|
|
27
|
+
|
|
28
|
+
なし。`APIRouter(prefix="/v1")` でルーターを分離し `app.include_router()` で登録するだけで
|
|
29
|
+
バージョン分離が完成する。OpenAPI スキーマも `UserResponseV1` / `UserResponseV2` を
|
|
30
|
+
個別に定義することで両バージョンのスキーマが正確に生成される。
|
|
31
|
+
|
|
32
|
+
## 観察
|
|
33
|
+
|
|
34
|
+
### O1: バージョン間の共有ロジックはドメイン層に置く
|
|
35
|
+
|
|
36
|
+
`find_user()` などのクエリ関数はバージョン非依存のドメイン層に置き、
|
|
37
|
+
各バージョンのハンドラーから呼ぶ設計が自然に機能した。
|
|
38
|
+
`from_domain()` クラスメソッドで変換することでドメインと HTTP を分離できた。
|
|
39
|
+
|
|
40
|
+
### O2: OpenAPI タグでバージョンを整理できる
|
|
41
|
+
|
|
42
|
+
`tags=["v1"]` / `tags=["v2"]` を `APIRouter` に指定すると、
|
|
43
|
+
Swagger UI でバージョンごとにグループ化されて見やすくなる。
|
|
44
|
+
|
|
45
|
+
### O3: `APIRouter` prefix は重複しない
|
|
46
|
+
|
|
47
|
+
`app.include_router(v1_router)` で登録すると `/v1/users` になる。
|
|
48
|
+
ルーター内のパスに `/v1` を書く必要はない(二重にならない)。
|
|
49
|
+
|
|
50
|
+
## まとめ
|
|
51
|
+
|
|
52
|
+
FT109 は摩擦ゼロ確認。APIバージョニングは FastAPI の `APIRouter` + `prefix` で
|
|
53
|
+
自然に実現できる。how-to ドキュメントに記録のみ。
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Field Trial 110: ソフトデリートパターン(論理削除)
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
`deleted_at: datetime | None` フィールドを使った論理削除パターンを検証する。
|
|
6
|
+
- `DELETE /resources/{id}` が物理削除ではなく `deleted_at` をセットする
|
|
7
|
+
- `GET /resources` / `GET /resources/{id}` が削除済みアイテムを除外する
|
|
8
|
+
- DELETE は冪等(既に削除済みでも 204)
|
|
9
|
+
- `deleted_at` を公開レスポンスに含めない(管理エンドポイントのみ)
|
|
10
|
+
|
|
11
|
+
## 実施内容
|
|
12
|
+
|
|
13
|
+
`/home/xi/docker/nene2-python-FT/ft110-soft-delete/` に以下を実装:
|
|
14
|
+
|
|
15
|
+
- `Article` dataclass — `deleted_at: datetime | None = None`、`is_deleted` プロパティ
|
|
16
|
+
- `dataclasses.replace()` で frozen dataclass を更新(`deleted_at` をセット)
|
|
17
|
+
- `ArticleResponse` に `deleted_at` フィールドを含めない設計
|
|
18
|
+
- 管理用 `/articles/{id}/deleted` エンドポイントで `deleted_at` を確認
|
|
19
|
+
- 9 テスト通過
|
|
20
|
+
|
|
21
|
+
## テスト結果
|
|
22
|
+
|
|
23
|
+
全 9 テスト一発通過。摩擦ゼロ。
|
|
24
|
+
|
|
25
|
+
## Friction Points
|
|
26
|
+
|
|
27
|
+
なし。
|
|
28
|
+
|
|
29
|
+
## 観察
|
|
30
|
+
|
|
31
|
+
### O1: frozen dataclass の更新は dataclasses.replace() で簡潔に書ける
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from dataclasses import replace
|
|
35
|
+
|
|
36
|
+
_articles[article_id] = replace(article, deleted_at=datetime.now(UTC))
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`frozen=True` な dataclass でも `replace()` で新しいインスタンスを作成できる。
|
|
40
|
+
イミュータブル設計を壊さずに更新できる。
|
|
41
|
+
|
|
42
|
+
### O2: is_deleted プロパティでビジネスロジックをドメインに閉じ込める
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
@property
|
|
46
|
+
def is_deleted(self) -> bool:
|
|
47
|
+
return self.deleted_at is not None
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
クエリフィルタ(`a.is_deleted`)でリポジトリ層が `deleted_at is not None` を意識しなくてよい。
|
|
51
|
+
|
|
52
|
+
### O3: DELETE は 204 + 冪等が REST ベストプラクティス
|
|
53
|
+
|
|
54
|
+
既に削除済みのリソースへの DELETE も 204 を返すことで冪等性を保つ。
|
|
55
|
+
存在しないリソースへの DELETE も同様。
|
|
56
|
+
|
|
57
|
+
### O4: deleted_at をレスポンスモデルから除外するのは Pydantic の通常設計で簡単
|
|
58
|
+
|
|
59
|
+
`ArticleResponse` に `deleted_at` フィールドを定義しなければ、自動的に除外される。
|
|
60
|
+
`exclude=` や `model_config` の設定は不要。
|
|
61
|
+
|
|
62
|
+
## まとめ
|
|
63
|
+
|
|
64
|
+
FT110 は摩擦ゼロ。ソフトデリートは nene2 + Python dataclass で自然に実装できる。
|
|
65
|
+
`dataclasses.replace()` / `is_deleted` プロパティ / Pydantic レスポンスフィルタリングの
|
|
66
|
+
各パターンを how-to に記録する。
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Field Trial 111: カーソルベースページネーション
|
|
2
|
+
|
|
3
|
+
## テーマ
|
|
4
|
+
|
|
5
|
+
ID ベースのカーソル(Base64 エンコード)で大規模データを安定的にページングするパターンを検証する。
|
|
6
|
+
既存の `PaginationQueryParser`(オフセット型)との比較として実装する。
|
|
7
|
+
|
|
8
|
+
## 実施内容
|
|
9
|
+
|
|
10
|
+
`/home/xi/docker/nene2-python-FT/ft111-cursor-pagination/` に以下を実装:
|
|
11
|
+
|
|
12
|
+
- `after` クエリパラメータ + `limit` でカーソルページネーション
|
|
13
|
+
- `next_cursor` / `has_next` を含む `CursorPage` レスポンスモデル
|
|
14
|
+
- Base64 URL-safe エンコードでカーソルを不透明にする
|
|
15
|
+
- 100件のサンプルデータで重複なし検証
|
|
16
|
+
- 7 テスト通過(修正後)
|
|
17
|
+
|
|
18
|
+
## テスト結果
|
|
19
|
+
|
|
20
|
+
全 7 テスト通過(1件修正後)。
|
|
21
|
+
|
|
22
|
+
## Friction Points
|
|
23
|
+
|
|
24
|
+
### FP1: 無効なカーソルで `binascii.Error` が捕捉されず 500 になる
|
|
25
|
+
|
|
26
|
+
**状況**: `?after=invalidcursor!` のような無効な Base64 文字列を渡すと、
|
|
27
|
+
`base64.urlsafe_b64decode()` が `binascii.Error` を raise する。
|
|
28
|
+
これを捕捉しないと `ErrorHandlerMiddleware` が 500 として返す。
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# ❌ binascii.Error が uncaught → 500
|
|
32
|
+
after_id = _decode_cursor(after)
|
|
33
|
+
|
|
34
|
+
# ✅ try-except でカーソルデコードを保護する
|
|
35
|
+
try:
|
|
36
|
+
after_id = _decode_cursor(after)
|
|
37
|
+
except Exception:
|
|
38
|
+
return JSONResponse({"detail": "Invalid cursor"}, status_code=400)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**影響**: 中。入力バリデーションとして当然捕捉すべきだが、
|
|
42
|
+
Pydantic の `Query()` では Base64 形式の検証はできない(文字列型のみ)。
|
|
43
|
+
カーソルデコードは必ず try-except で保護する必要がある。
|
|
44
|
+
|
|
45
|
+
**代替案**: `ValidationException` を raise して 422 にするパターンもあるが、
|
|
46
|
+
不正なカーソルは「バリデーションエラー」ではなく「不正リクエスト」なので 400 が適切。
|
|
47
|
+
|
|
48
|
+
## まとめ
|
|
49
|
+
|
|
50
|
+
FP1 を how-to に追記。カーソルのデコードは必ず try-except で保護し 400 を返す。
|
|
51
|
+
オフセットページネーションとの使い分け:
|
|
52
|
+
- オフセット型: 件数が少ない、管理画面など「ページ番号」で飛びたい場合
|
|
53
|
+
- カーソル型: 件数が多い、リアルタイムデータ、無限スクロールなど
|