From 0634175a0065240216349c8529ef2a742a5c945b Mon Sep 17 00:00:00 2001 From: shun-harutaro Date: Tue, 15 Oct 2024 13:35:18 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E9=9F=B3=E5=A3=B0=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=81=AE=E6=89=B1=E3=81=84=E3=82=92=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/alembic/env.py | 2 +- api/v2/routers/raspi.py | 28 +++++++++------------------- api/v2/routers/user.py | 3 ++- api/v2/services/gpt.py | 4 ++++ 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/api/alembic/env.py b/api/alembic/env.py index 281fff1..7515545 100644 --- a/api/alembic/env.py +++ b/api/alembic/env.py @@ -3,9 +3,9 @@ from sqlalchemy import engine_from_config, pool from alembic import context +from config import get_db_object from db import Base from v2.models import Couple, Message, Raspi, User # noqa: F401 -from v2.utils.config import get_db_object DB_OBJ = get_db_object() diff --git a/api/v2/routers/raspi.py b/api/v2/routers/raspi.py index 60bb5cb..8cd900c 100644 --- a/api/v2/routers/raspi.py +++ b/api/v2/routers/raspi.py @@ -56,18 +56,15 @@ async def all( speaker: int = 1, db: AsyncSession = Depends(get_db), ) -> FileResponse: - file_location = os.path.join(UPLOAD_DIR, file.filename) try: get_user_task = asyncio.create_task(get_user_by_raspi_id(db, raspi_id)) - file_read_task = asyncio.create_task(file.read()) - - content: bytes = await file_read_task - with open(file_location, "wb") as f: - f.write(content) + content: bytes = await file.read() + with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as temp_file: + temp_file.write(content) + temp_file_path = temp_file.name # whisper - transcription: str = await speech2text(file_location) - os.remove(file_location) + transcription: str = await speech2text(temp_file_path) logger.info(f"transcription: {transcription.text}") push_transcription(raspi_id, transcription.text) @@ -80,17 +77,10 @@ async def all( # voicevox audio: bytes = await get_voicevox_audio(generated_text, speaker) - temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav") - with open(temp_file.name, "wb") as f: - f.write(audio) - except RequestError as e: - raise HTTPException( - status_code=500, detail=f"RequestError fetching data: {str(e)}" - ) - except HTTPStatusError as e: - raise HTTPException( - status_code=e.response.status_code, detail=f"Error fetching data: {str(e)}" - ) + with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as temp_file: + temp_file.write(audio) + except (RequestError, HTTPStatusError) as e: + raise HTTPException(status_code=500, detail=f"Error: {str(e)}") return FileResponse(temp_file.name, media_type="audio/wav", filename="audio.wav") diff --git a/api/v2/routers/user.py b/api/v2/routers/user.py index a68605f..118301a 100644 --- a/api/v2/routers/user.py +++ b/api/v2/routers/user.py @@ -8,7 +8,7 @@ import v2.schemas.user as user_schema from config import get_azure_sas_token, get_azure_storage_account from db import get_db -from v2.services.gpt import create_new_thread_id +from v2.services.gpt import create_new_thread_id, delete_thread_id from v2.utils.logging import get_logger router = APIRouter() @@ -65,4 +65,5 @@ async def delete_user(id: int, db: AsyncSession = Depends(get_db)): user = await user_crud.get_user(db, user_id=id) if user is None: raise HTTPException(status_code=404, detail="User not found") + await delete_thread_id(user.thread_id) return await user_crud.delete_user(db, user) diff --git a/api/v2/services/gpt.py b/api/v2/services/gpt.py index e38b5c8..7ff9ee3 100644 --- a/api/v2/services/gpt.py +++ b/api/v2/services/gpt.py @@ -13,6 +13,10 @@ async def create_new_thread_id() -> str: return thread.id +async def delete_thread_id(thread_id: str): + await client.beta.threads.delete(thread_id) + + async def generate_text(thread_id: int, prompt: str) -> str: await client.beta.threads.messages.create( thread_id=thread_id, From 0c374581e44994588d21b39f5ba284609af8ef34 Mon Sep 17 00:00:00 2001 From: shun-harutaro Date: Tue, 15 Oct 2024 16:45:23 +0900 Subject: [PATCH 2/6] =?UTF-8?q?raspis=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E6=9B=B8=E3=81=84=E3=81=A6=E3=81=BF=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/tests/conftest.py | 2 +- api/tests/cruds/conftest.py | 38 -------------------------- api/tests/raspi/conftest.py | 37 +++++++++++++++++++++++++ api/tests/{ => raspi}/test_v1_raspi.py | 0 api/tests/raspi/test_v2_raspi.py | 34 +++++++++++++++++++++++ 5 files changed, 72 insertions(+), 39 deletions(-) delete mode 100644 api/tests/cruds/conftest.py create mode 100644 api/tests/raspi/conftest.py rename api/tests/{ => raspi}/test_v1_raspi.py (100%) create mode 100644 api/tests/raspi/test_v2_raspi.py diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 47c5664..4e9a30d 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -12,7 +12,7 @@ ASYNC_DB_URL = "sqlite+aiosqlite:///:memory:" -@pytest.fixture +@pytest.fixture(scope="module") async def async_client() -> AsyncGenerator[AsyncClient, None]: # Async用のengineとsessionを作成 async_engine = create_async_engine(ASYNC_DB_URL, echo=True) diff --git a/api/tests/cruds/conftest.py b/api/tests/cruds/conftest.py deleted file mode 100644 index 9b56a85..0000000 --- a/api/tests/cruds/conftest.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest -import starlette.status - - -@pytest.fixture(scope="session") -async def raspi_fixture(async_client): - raspi_1 = await async_client.post("/v2/raspis", json={"name": "raspi_1"}) - assert raspi_1.status_code == starlette.status.HTTP_200_OK - raspi_2 = await async_client.post("/v2/raspis", json={"name": "raspi_2"}) - assert raspi_2.status_code == starlette.status.HTTP_200_OK - return [raspi_1.json(), raspi_2.json()] - - -@pytest.fixture(scope="session") -async def user_fixture(async_client, raspi_fixture): - user_1 = await async_client.post( - "/v2/users", json={"raspi_id": raspi_fixture[0]["id"], "name": "user_1"} - ) - assert user_1.status_code == starlette.status.HTTP_200_OK - user_2 = await async_client.post( - "/v2/users", json={"raspi_id": raspi_fixture[1]["id"], "name": "user_2"} - ) - assert user_2.status_code == starlette.status.HTTP_200_OK - return [user_1.json(), user_2.json()] - - -@pytest.fixture(scope="session") -async def couple_fixture(async_client, user_fixture): - couple_1 = await async_client.post( - "/v2/couples", - json={ - "user1_id": user_fixture[0]["id"], - "user2_id": user_fixture[1]["id"], - "name": "couple_1", - }, - ) - assert couple_1.status_code == starlette.status.HTTP_200_OK - return couple_1.json() diff --git a/api/tests/raspi/conftest.py b/api/tests/raspi/conftest.py new file mode 100644 index 0000000..2695556 --- /dev/null +++ b/api/tests/raspi/conftest.py @@ -0,0 +1,37 @@ +import pytest +import starlette.status + + +@pytest.fixture(scope="module") +async def raspi_fixture(async_client): + raspi_1 = await async_client.post("/v2/raspis/", json={"name": "raspi_1"}) + assert raspi_1.status_code == starlette.status.HTTP_200_OK + raspi_2 = await async_client.post("/v2/raspis/", json={"name": "raspi_2"}) + assert raspi_2.status_code == starlette.status.HTTP_200_OK + + user_1 = await async_client.post( + "/v2/users/", json={"raspi_id": raspi_1.json()["id"], "name": "user_1"} + ) + assert user_1.status_code == starlette.status.HTTP_200_OK + user_2 = await async_client.post( + "/v2/users/", json={"raspi_id": raspi_2.json()["id"], "name": "user_2"} + ) + assert user_2.status_code == starlette.status.HTTP_200_OK + + couple_1 = await async_client.post( + "/v2/couples/", + json={ + "user1_id": user_1.json()["id"], + "user2_id": user_2.json()["id"], + "name": "couple_1", + }, + ) + assert couple_1.status_code == starlette.status.HTTP_200_OK + + return { + "raspi_1": raspi_1.json(), + "raspi_2": raspi_2.json(), + "user_1": user_1.json(), + "user_2": user_2.json(), + "couple_1": couple_1.json(), + } diff --git a/api/tests/test_v1_raspi.py b/api/tests/raspi/test_v1_raspi.py similarity index 100% rename from api/tests/test_v1_raspi.py rename to api/tests/raspi/test_v1_raspi.py diff --git a/api/tests/raspi/test_v2_raspi.py b/api/tests/raspi/test_v2_raspi.py new file mode 100644 index 0000000..1d9b901 --- /dev/null +++ b/api/tests/raspi/test_v2_raspi.py @@ -0,0 +1,34 @@ +import pytest +import starlette.status + + +@pytest.mark.asyncio +async def test_transcribe_and_respond(async_client, raspi_fixture): + raspi_1 = raspi_fixture["raspi_1"] + audio_file_path = "tests/audio1.wav" + with open(audio_file_path, "rb") as audio_file: + files = {"file": ("audio1.wav", audio_file, "multipart/form-data")} + + response = await async_client.post( + f"/v2/raspis/{raspi_1["id"]}", + files=files, + ) + + assert response.status_code == starlette.status.HTTP_200_OK + assert response.headers["content-type"] == "audio/wav" + assert response.content is not None + + +# @pytest.mark.asyncio +# async def test_send_message(async_client, raspi_fixture): +# raspi_1 = raspi_fixture["raspi_1"] +# audio_file_path = "tests/audio1.wav" +# with open(audio_file_path, "rb") as audio_file: +# files = {"file": ("audio1.wav", audio_file, "multipart/form-data")} +# +# response = await async_client.post( +# f"/v2/raspis/{raspi_1["id"]}/messages", +# files=files, +# ) +# +# assert response.status_code == starlette.status.HTTP_200_OK From 80ca27a4c1cb673e3840f486b8d6a234fe6f54a8 Mon Sep 17 00:00:00 2001 From: NOZAKI Shuntaro <60352276+shun-harutaro@users.noreply.github.com> Date: Wed, 16 Oct 2024 06:34:12 +0900 Subject: [PATCH 3/6] =?UTF-8?q?deploy.yml=E3=81=93=E3=81=86=E3=81=97?= =?UTF-8?q?=E3=82=93=20(#219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4e466ea..704b519 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,6 +23,25 @@ jobs: username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} + - name: Create .env file + run: | + cat < ${{ github.workspace }}/api/.env + IS_DEV_MODE=0 + VOICEVOX_URL=${{ vars.VOICEVOX_URL }} + OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} + VOICEVOX_API_KEY=${{ secrets.VOICEVOX_API_KEY }} + OPENAI_ASSISTANT_ID=${{ vars.OPENAI_ASSISTANT_ID }} + OPENAI_THREAD_ID=${{ vars.OPENAI_THREAD_ID }} + AZURE_STORAGE_ACCOUNT=${{ vars.AZURE_STORAGE_ACCOUNT }} + AZURE_SAS_TOKEN=${{ secrets.AZURE_SAS_TOKEN }} + PUBSUB_CONNECTION_STRING=${{ secrets.PUBSUB_CONNECTION_STRING }} + DB_NAME=${{ vars.DB_NAME }} + DB_HOST=${{ vars.DB_HOST }} + DB_USERNAME=${{ vars.DB_USERNAME }} + DB_PASSWORD=${{ secrets.DB_PASSWORD }} + DB_CERT_PATH=${{ env.PROJECT_ROOT }}/api/db-cert.pem + EOL + - name: Add SSL certificates for DB run: | echo "${{ secrets.DB_SSL_CERT }}" > ${{ github.workspace }}/api/db-cert.pem From 9f60b890a4ec78d29682aa827c80ee4f0c315bcb Mon Sep 17 00:00:00 2001 From: NOZAKI Shuntaro <60352276+shun-harutaro@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:01:46 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Revert=20"user=E3=81=AE=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0?= =?UTF-8?q?=20(#217)"=20(#220)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit bc410e8d07665f55e5e622f953bd637afd0d92cc. --- api/tests/test_v2_user.py | 29 ++++++++++++++--------------- api/v2/cruds/user.py | 24 +++++++----------------- api/v2/routers/user.py | 8 ++------ api/v2/schemas/user.py | 2 -- 4 files changed, 23 insertions(+), 40 deletions(-) diff --git a/api/tests/test_v2_user.py b/api/tests/test_v2_user.py index 3b402cf..24238fb 100644 --- a/api/tests/test_v2_user.py +++ b/api/tests/test_v2_user.py @@ -1,5 +1,4 @@ import pytest -from sqlalchemy.exc import IntegrityError import starlette.status @@ -35,20 +34,20 @@ async def test_crud_user_no_name(async_client, create_raspi): res_put = res_3.json() assert res_put["name"] == "hoge" - # ラズパイを削除した時、raspi_idがnullになるか? - res_4 = await async_client.delete(f"/v2/raspis/{raspi_id}") - assert res_4.status_code == starlette.status.HTTP_200_OK - res_5 = await async_client.get("/v2/users/") - assert res_5.status_code == starlette.status.HTTP_200_OK - res_get_2 = res_5.json() - assert res_get_2[0]["raspi_id"] is None - - # 登録されていないラズパイを紐付けようとした場合404を返す - with pytest.raises(IntegrityError): - res_5 = await async_client.put( - f"/v2/users/{user_id}", json={"name": "hoge", "raspi_id": 2} - ) - assert res_5.status_code == starlette.status.HTTP_404_NOT_FOUND + # TODO:ラズパイを削除した時、raspi_idがnullになるか? + # res_4 = await async_client.delete(f"/v2/raspis/{raspi_id}") + # assert res_4.status_code == starlette.status.HTTP_200_OK + # res_5 = await async_client.get("/v2/users/") + # assert res_5.status_code == starlette.status.HTTP_200_OK + # res_get_2 = res_5.json() + # assert res_get_2[0]["raspi_id"] == None + + # TODO:登録されていないラズパイを紐付けようとした場合404を返す + # res_5 = await async_client.put( + # f"/v2/users/{user_id}", + # json={"name": "hoge", "raspi_id": 2} + # ) + # assert res_5.status_code == starlette.status.HTTP_404_NOT_FOUND # 削除:ユーザを消去 res_6 = await async_client.delete(f"/v2/users/{user_id}") diff --git a/api/v2/cruds/user.py b/api/v2/cruds/user.py index eb5812c..a86d4f3 100644 --- a/api/v2/cruds/user.py +++ b/api/v2/cruds/user.py @@ -1,33 +1,23 @@ from sqlalchemy import select from sqlalchemy.engine import Result -from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession import v2.models.user as user_model import v2.schemas.user as user_schema -class ForeignKeyError(Exception): - pass - - async def create_user( db: AsyncSession, user_create: user_schema.UserCreate, thread_id: str, ) -> user_model.User: - try: - # 引数にスキーマuser_create: user_schema.UserCreateを受け取りDBモデルのuser_model.Userに変換する - user = user_model.User(**user_create.model_dump()) - user.thread_id = thread_id - db.add(user) - await db.commit() - await db.refresh(user) - return user - except IntegrityError as e: - if "foreign key" in str(e.orig): - raise ForeignKeyError("Related resource not found") - raise + # 引数にスキーマuser_create: user_schema.UserCreateを受け取りDBモデルのuser_model.Userに変換する + user = user_model.User(**user_create.model_dump()) + user.thread_id = thread_id + db.add(user) + await db.commit() + await db.refresh(user) + return user async def get_users(db: AsyncSession): diff --git a/api/v2/routers/user.py b/api/v2/routers/user.py index 3bf035c..118301a 100644 --- a/api/v2/routers/user.py +++ b/api/v2/routers/user.py @@ -38,12 +38,8 @@ async def list_users(db: AsyncSession = Depends(get_db)): response_model=user_schema.UserResponse, ) async def create_user(user: user_schema.UserCreate, db: AsyncSession = Depends(get_db)): - try: - thread_id = await create_new_thread_id() - user_result = await user_crud.create_user(db, user, thread_id) - return user_result - except user_crud.ForeignKeyError as e: - raise HTTPException(status_code=404, detail=str(e)) + thread_id = await create_new_thread_id() + return await user_crud.create_user(db, user, thread_id) @router.put( diff --git a/api/v2/schemas/user.py b/api/v2/schemas/user.py index 73aaaba..48a04de 100644 --- a/api/v2/schemas/user.py +++ b/api/v2/schemas/user.py @@ -34,8 +34,6 @@ class UserResponse(UserBase): None, description="couple_id is applied by couple_schema", ) - # override - raspi_id: Optional[int] thread_id: str = Field(..., pattern=r"^thread_") created_at: datetime = Field( default_factory=lambda: datetime.now(), From aad1fe8968d112358d934ebcfba7676148263c55 Mon Sep 17 00:00:00 2001 From: NOZAKI Shuntaro <60352276+shun-harutaro@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:09:37 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Revert=20"deploy.yml=E3=81=93=E3=81=86?= =?UTF-8?q?=E3=81=97=E3=82=93=20(#219)"=20(#221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 80ca27a4c1cb673e3840f486b8d6a234fe6f54a8. --- .github/workflows/deploy.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 704b519..4e466ea 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,25 +23,6 @@ jobs: username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - - name: Create .env file - run: | - cat < ${{ github.workspace }}/api/.env - IS_DEV_MODE=0 - VOICEVOX_URL=${{ vars.VOICEVOX_URL }} - OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} - VOICEVOX_API_KEY=${{ secrets.VOICEVOX_API_KEY }} - OPENAI_ASSISTANT_ID=${{ vars.OPENAI_ASSISTANT_ID }} - OPENAI_THREAD_ID=${{ vars.OPENAI_THREAD_ID }} - AZURE_STORAGE_ACCOUNT=${{ vars.AZURE_STORAGE_ACCOUNT }} - AZURE_SAS_TOKEN=${{ secrets.AZURE_SAS_TOKEN }} - PUBSUB_CONNECTION_STRING=${{ secrets.PUBSUB_CONNECTION_STRING }} - DB_NAME=${{ vars.DB_NAME }} - DB_HOST=${{ vars.DB_HOST }} - DB_USERNAME=${{ vars.DB_USERNAME }} - DB_PASSWORD=${{ secrets.DB_PASSWORD }} - DB_CERT_PATH=${{ env.PROJECT_ROOT }}/api/db-cert.pem - EOL - - name: Add SSL certificates for DB run: | echo "${{ secrets.DB_SSL_CERT }}" > ${{ github.workspace }}/api/db-cert.pem From 1f0d3ff7b6bd5837e16817d9a827db20bc156feb Mon Sep 17 00:00:00 2001 From: shun-harutaro Date: Wed, 16 Oct 2024 08:13:34 +0900 Subject: [PATCH 6/6] =?UTF-8?q?Revert=20"Revert=20"user=E3=81=AE=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0=20(#217)"=20(#220)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 9f60b890a4ec78d29682aa827c80ee4f0c315bcb. --- api/tests/test_v2_user.py | 29 +++++++++++++++-------------- api/v2/cruds/user.py | 24 +++++++++++++++++------- api/v2/routers/user.py | 8 ++++++-- api/v2/schemas/user.py | 2 ++ 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/api/tests/test_v2_user.py b/api/tests/test_v2_user.py index 24238fb..3b402cf 100644 --- a/api/tests/test_v2_user.py +++ b/api/tests/test_v2_user.py @@ -1,4 +1,5 @@ import pytest +from sqlalchemy.exc import IntegrityError import starlette.status @@ -34,20 +35,20 @@ async def test_crud_user_no_name(async_client, create_raspi): res_put = res_3.json() assert res_put["name"] == "hoge" - # TODO:ラズパイを削除した時、raspi_idがnullになるか? - # res_4 = await async_client.delete(f"/v2/raspis/{raspi_id}") - # assert res_4.status_code == starlette.status.HTTP_200_OK - # res_5 = await async_client.get("/v2/users/") - # assert res_5.status_code == starlette.status.HTTP_200_OK - # res_get_2 = res_5.json() - # assert res_get_2[0]["raspi_id"] == None - - # TODO:登録されていないラズパイを紐付けようとした場合404を返す - # res_5 = await async_client.put( - # f"/v2/users/{user_id}", - # json={"name": "hoge", "raspi_id": 2} - # ) - # assert res_5.status_code == starlette.status.HTTP_404_NOT_FOUND + # ラズパイを削除した時、raspi_idがnullになるか? + res_4 = await async_client.delete(f"/v2/raspis/{raspi_id}") + assert res_4.status_code == starlette.status.HTTP_200_OK + res_5 = await async_client.get("/v2/users/") + assert res_5.status_code == starlette.status.HTTP_200_OK + res_get_2 = res_5.json() + assert res_get_2[0]["raspi_id"] is None + + # 登録されていないラズパイを紐付けようとした場合404を返す + with pytest.raises(IntegrityError): + res_5 = await async_client.put( + f"/v2/users/{user_id}", json={"name": "hoge", "raspi_id": 2} + ) + assert res_5.status_code == starlette.status.HTTP_404_NOT_FOUND # 削除:ユーザを消去 res_6 = await async_client.delete(f"/v2/users/{user_id}") diff --git a/api/v2/cruds/user.py b/api/v2/cruds/user.py index a86d4f3..eb5812c 100644 --- a/api/v2/cruds/user.py +++ b/api/v2/cruds/user.py @@ -1,23 +1,33 @@ from sqlalchemy import select from sqlalchemy.engine import Result +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession import v2.models.user as user_model import v2.schemas.user as user_schema +class ForeignKeyError(Exception): + pass + + async def create_user( db: AsyncSession, user_create: user_schema.UserCreate, thread_id: str, ) -> user_model.User: - # 引数にスキーマuser_create: user_schema.UserCreateを受け取りDBモデルのuser_model.Userに変換する - user = user_model.User(**user_create.model_dump()) - user.thread_id = thread_id - db.add(user) - await db.commit() - await db.refresh(user) - return user + try: + # 引数にスキーマuser_create: user_schema.UserCreateを受け取りDBモデルのuser_model.Userに変換する + user = user_model.User(**user_create.model_dump()) + user.thread_id = thread_id + db.add(user) + await db.commit() + await db.refresh(user) + return user + except IntegrityError as e: + if "foreign key" in str(e.orig): + raise ForeignKeyError("Related resource not found") + raise async def get_users(db: AsyncSession): diff --git a/api/v2/routers/user.py b/api/v2/routers/user.py index 118301a..3bf035c 100644 --- a/api/v2/routers/user.py +++ b/api/v2/routers/user.py @@ -38,8 +38,12 @@ async def list_users(db: AsyncSession = Depends(get_db)): response_model=user_schema.UserResponse, ) async def create_user(user: user_schema.UserCreate, db: AsyncSession = Depends(get_db)): - thread_id = await create_new_thread_id() - return await user_crud.create_user(db, user, thread_id) + try: + thread_id = await create_new_thread_id() + user_result = await user_crud.create_user(db, user, thread_id) + return user_result + except user_crud.ForeignKeyError as e: + raise HTTPException(status_code=404, detail=str(e)) @router.put( diff --git a/api/v2/schemas/user.py b/api/v2/schemas/user.py index 48a04de..73aaaba 100644 --- a/api/v2/schemas/user.py +++ b/api/v2/schemas/user.py @@ -34,6 +34,8 @@ class UserResponse(UserBase): None, description="couple_id is applied by couple_schema", ) + # override + raspi_id: Optional[int] thread_id: str = Field(..., pattern=r"^thread_") created_at: datetime = Field( default_factory=lambda: datetime.now(),