Repository: kizuna-org/akari-poc
Pull request #91: feat(io): implement _SaveModule for data saving with optional timestamp and add stream_call method
Author: login: shiron-dev
Created: 2025-05-04T13:13:52Z | Merged: 2025-05-04T13:34:25Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **新機能**
  - データ保存用モジュールを追加し、指定したデータフィールドをファイルに保存できるようになりました。タイムスタンプ付きファイル名や、WAV形式での音声データ保存もサポートされています。

- **その他**
  - 一時ファイルやディレクトリ（/tmp/配下）がGit管理対象外になりました。

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.gitignore b/.gitignore
index 43089ec..83b905e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -179,3 +179,4 @@ pyrightconfig.json
 
 *.mp3
 *.wav
+/tmp/*
diff --git a/modules/io/__init__.py b/modules/io/__init__.py
new file mode 100644
index 0000000..7ab31d7
--- /dev/null
+++ b/modules/io/__init__.py
@@ -0,0 +1,4 @@
+from .save import _SaveModule as SaveModule
+from .save import _SaveModuleParams as SaveModuleParams
+
+__all__ = ["SaveModule", "SaveModuleParams"]
diff --git a/modules/io/save.py b/modules/io/save.py
new file mode 100644
index 0000000..9406dcf
--- /dev/null
+++ b/modules/io/save.py
@@ -0,0 +1,68 @@
+import dataclasses
+import time
+import wave
+from datetime import datetime
+
+from akari import (
+    AkariData,
+    AkariDataSet,
+    AkariDataSetType,
+    AkariLogger,
+    AkariModule,
+    AkariModuleParams,
+    AkariModuleType,
+    AkariRouter,
+)
+
+
+@dataclasses.dataclass
+class _SaveModuleParams:
+    file_path: str
+    save_from_data: str
+    with_timestamp: bool = False
+
+
+class _SaveModule(AkariModule):
+    def __init__(
+        self,
+        router: AkariRouter,
+        logger: AkariLogger,
+    ) -> None:
+        super().__init__(router, logger)
+
+    def call(self, data: AkariData, params: _SaveModuleParams, callback: AkariModuleType | None = None) -> AkariDataSet:
+        try:
+            save_data = data.last().__dict__[params.save_from_data]
+        except KeyError:
+            raise ValueError(f"Data does not contain the key '{params.save_from_data}'.")
+        if not save_data:
+            raise ValueError(f"Data does not contain the key '{params.save_from_data}' or it is empty.")
+
+        path = params.file_path
+        if params.with_timestamp:
+            paths = path.split(".")
+            timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
+            if len(paths) > 1:
+                path = f"{'.'.join(paths[:-1])}_{timestamp}.{paths[-1]}"
+            else:
+                path = f"{path}_{timestamp}"
+
+        if path.endswith(".wav") and params.save_from_data == "audio":
+            audio_data = data.last().__dict__[params.save_from_data]
+            with wave.open(path, "wb") as wav_file:
+                wav_file.setnchannels(audio_data.channels if hasattr(audio_data, "channels") else 1)
+                wav_file.setsampwidth(audio_data.sample_width if hasattr(audio_data, "sample_width") else 2)
+                wav_file.setframerate(audio_data.rate if hasattr(audio_data, "rate") else 16000)
+                wav_file.writeframes(audio_data.main)
+            self._logger.debug("Audio data saved as WAV to %s", path)
+        else:
+            with open(path, "wb") as file:
+                file.write(data.last().__dict__[params.save_from_data].main)
+            self._logger.debug("Data saved to %s", path)
+
+        return data.last()
+
+    def stream_call(
+        self, data: AkariData, params: _SaveModuleParams, callback: AkariModuleType | None = None
+    ) -> AkariDataSet:
+        return self.call(data, params, callback)


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a new module for saving data to a file with an optional timestamp feature, along with a streaming API that delegates to the save functionality.
- Implemented _SaveModule to handle data saving.
- Added optional timestamp formatting to the output file name.
- Exposed the module and its parameters via the module's __init__.py for external use.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

| File                    | Description                                            |
| ----------------------- | ------------------------------------------------------ |
| modules/io/save.py      | Added _SaveModule with call() and stream_call() methods for saving data with an optional timestamp. |
| modules/io/__init__.py  | Exported _SaveModule and its parameters as SaveModule and SaveModuleParams. |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T13:14:32Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T13:16:06Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>modules/io/save.py (3)</summary><blockquote>

`2-2`: **未使用のインポートを削除してください**

`time` モジュールがインポートされていますが、コード内で使用されていません。未使用のインポートは削除することでコードの明確さを向上させてください。

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

2-2: `time` imported but unused

Remove unused import: `time`

(F401)

</details>

</details>

---

`59-61`: _🛠️ Refactor suggestion_

**コードの重複を避けてください**

ここでも同様に、`save_data` 変数が既に定義されているにもかかわらず、`data.last().__dict__[params.save_from_data]` を再度取得しています。コードの効率性と可読性を向上させるために、既存の変数を再利用してください。

```diff
with open(path, "wb") as file:
-    file.write(data.last().__dict__[params.save_from_data].main)
+    file.write(save_data.main)
```

---

`50-57`: _🛠️ Refactor suggestion_

**コードの重複を避けてください**

`save_data` 変数が既に定義されているにもかかわらず、51行目で `data.last().__dict__[params.save_from_data]` を再度取得しています。コードの効率性と可読性を向上させるために、既存の変数を再利用してください。

```diff
if path.endswith(".wav") and params.save_from_data == "audio":
-    audio_data = data.last().__dict__[params.save_from_data]
+    audio_data = save_data
    with wave.open(path, "wb") as wav_file:
        wav_file.setnchannels(audio_data.channels if hasattr(audio_data, "channels") else 1)
        wav_file.setsampwidth(audio_data.sample_width if hasattr(audio_data, "sample_width") else 2)
        wav_file.setframerate(audio_data.rate if hasattr(audio_data, "rate") else 16000)
        wav_file.writeframes(audio_data.main)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>modules/io/save.py (2)</summary><blockquote>

`9-9`: **未使用のインポートを削除してください**

`AkariDataSetType` および `AkariModuleParams` がインポートされていますが、コード内で使用されていません。未使用のインポートは削除してください。

```diff
from akari import (
    AkariData,
    AkariDataSet,
-    AkariDataSetType,
    AkariLogger,
    AkariModule,
-    AkariModuleParams,
    AkariModuleType,
    AkariRouter,
)
```



Also applies to: 12-12

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

9-9: `akari.AkariDataSetType` imported but unused

Remove unused import

(F401)

</details>

</details>

---

`65-68`: **stream_callメソッドにドキュメントコメントを追加してください**

`stream_call` メソッドが単純に `call` メソッドに委譲している理由を明確にするためのドキュメントコメントを追加することをお勧めします。なぜストリーミング動作と非ストリーミング動作が同じであるのかを説明するコメントがあると理解しやすくなります。

```diff
def stream_call(
    self, data: AkariData, params: _SaveModuleParams, callback: AkariModuleType | None = None
) -> AkariDataSet:
+    """
+    ストリーミングコンテキストでのデータ保存。
+    このモジュールでは、ストリーミングと非ストリーミングの動作は同じです。
+    データはバッチ処理され、一度に保存されます。
+    """
    return self.call(data, params, callback)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0378e8ae67fc186a03f4079e3ece1e4d995aa4a3 and 4516e01f21c2679e5dbf5608a0bba31c5f5e22ea.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `.gitignore` (1 hunks)
* `modules/io/__init__.py` (1 hunks)
* `modules/io/save.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (1)</summary>

<details>
<summary>`**/*.py`: Pythonプロジェクトにおける熟練の専門家・開発者としてPull Requestをレビューします。
レビューコメントをする際は、まず結論を簡潔に述べ、その後に指摘の根拠と改善案を順序立てて論理的に記述します。

レビューでは、以下の観点に合致していない点を重点的に指摘します。

- 型ヒント（type hints）を適切に使用している...</summary>

> `**/*.py`: Pythonプロジェクトにおける熟練の専門家・開発者としてPull Requestをレビューします。
> レビューコメントをする際は、まず結論を簡潔に述べ、その後に指摘の根拠と改善案を順序立てて論理的に記述します。
> 
> レビューでは、以下の観点に合致していない点を重点的に指摘します。
> 
> - 型ヒント（type hints）を適切に使用しているか（PEP 484に準拠）。
> - 非同期処理においては、async/awaitを正しく使用し、asyncioやaiohttp等の非同期ライブラリが適切に使われているか。
> - 型推論と明示的な型指定のバランスが適切か。
> - コレクション（リスト・辞書・集合など）に型指定がされており、例えばList[str], Dict[str, Any]などが活用されているか。
> - 抽象化・設計が適切で、関数・クラス・モジュールが過度に肥大化していないか（単一責任原則などを守っているか）。
> - 例外処理が漏れなく実装され、try/except節の使い方やカスタム例外の設計が適切か。
> - 外部ライブラリの使用において型定義（stubファイルやtypes-パッケージ）を活用しているか。
> - テストコードの書き方において、テストが冪等であり、setup_methodやteardown_methodによるリセット処理が適切か。
> - 予期しない型のエラーや例外を回避できる堅牢な実装となっているか。
> - パフォーマンスに著しい問題がないか（計算量、不要なオブジェクト生成、メモリリークの懸念など）。
> - 冪等性が求められる関数や処理でそれが保証されているか。
> - ライブラリのインポートが明示的かつ必要最小限で、循環参照の問題がないか。
> - コードの可読性・保守性が高く、コメントやdocstringが適切に記述されているか。
> - コード修正におけるデグレードのリスクがないか、機能や性能に影響を与えていないかを確認する。

- `modules/io/__init__.py`
- `modules/io/save.py`

</details>

</details><details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>modules/io/__init__.py (1)</summary><blockquote>

<details>
<summary>modules/io/save.py (2)</summary>

* `_SaveModule` (25-68)
* `_SaveModuleParams` (19-22)

</details>

</blockquote></details>
<details>
<summary>modules/io/save.py (1)</summary><blockquote>

<details>
<summary>tests/test_serial_module.py (1)</summary>

* `router` (21-22)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>modules/io/save.py</summary>

2-2: `time` imported but unused

Remove unused import: `time`

(F401)

---

9-9: `akari.AkariDataSetType` imported but unused

Remove unused import

(F401)

---

12-12: `akari.AkariModuleParams` imported but unused

Remove unused import

(F401)

---

37-37: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: python-test
* GitHub Check: python-lint
* GitHub Check: python-type-check
* GitHub Check: python-format
* GitHub Check: yaml-lint

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>.gitignore (1)</summary>

`182-182`: **適切な変更です**

`/tmp/*` パターンをgitignoreに追加することで、一時ファイルやテストデータがリポジトリに含まれないようにしています。これは新しい保存モジュールの追加に合わせた適切な対応です。

</details>
<details>
<summary>modules/io/__init__.py (1)</summary>

`1-4`: **適切なパブリックAPI定義です**

内部実装を `_SaveModule` と `_SaveModuleParams` として定義し、それらをパブリックAPIとして `SaveModule` と `SaveModuleParams` の名前でエクスポートする設計は適切です。`__all__` を使用して明示的にエクスポートする要素を定義しているのも良い実践です。

</details>
<details>
<summary>modules/io/save.py (2)</summary>

`18-22`: **適切なデータクラス定義です**

`_SaveModuleParams` のデータクラス定義は適切です。型ヒントが明確に定義されており、デフォルト値も適切に設定されています。

---

`25-31`: **初期化メソッドは適切に実装されています**

`_SaveModule` クラスの初期化メソッドは適切に実装されています。必要なパラメータが型ヒント付きで定義され、親クラスのコンストラクタも正しく呼び出されています。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T13:31:35Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T13:34:18Z

Review comments:
diff_hunk: @@ -0,0 +1,57 @@
+import dataclasses
+from datetime import datetime
+import time
path: modules/io/save.py
user: login: Copilot
body: The 'time' module is imported but not used. Please remove this unused import to keep the code clean.
```suggestion

```
created_at: 2025-05-04T13:14:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,57 @@
+import dataclasses
+from datetime import datetime
+import time
+from akari import (
+    AkariData,
+    AkariDataSet,
+    AkariDataSetType,
+    AkariLogger,
+    AkariModule,
+    AkariModuleParams,
+    AkariModuleType,
+    AkariRouter,
+)
+
+
+@dataclasses.dataclass
+class _SaveModuleParams:
+    file_path: str
+    save_from_data: str
+    with_timestamp: bool = False
+
+
+class _SaveModule(AkariModule):
+    def __init__(
+        self,
+        router: AkariRouter,
+        logger: AkariLogger,
+    ) -> None:
+        super().__init__(router, logger)
+
+    def call(self, data: AkariData, params: _SaveModuleParams, callback: AkariModuleType | None = None) -> AkariDataSet:
+        try:
+            data.last().__dict__[params.save_from_data]
+        except KeyError:
+            raise ValueError(f"Data does not contain the key '{params.save_from_data}'.")
+        if not data.last().__dict__[params.save_from_data]:
path: modules/io/save.py
user: login: Copilot
body: The same dictionary access is performed multiple times. Consider storing data.last().__dict__[params.save_from_data] in a local variable for improved readability and efficiency.
```suggestion
            save_data = data.last().__dict__[params.save_from_data]
        except KeyError:
            raise ValueError(f"Data does not contain the key '{params.save_from_data}'.")
        if not save_data:
```
created_at: 2025-05-04T13:14:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+import dataclasses
+import time
+import wave
+from datetime import datetime
+
+from akari import (
+    AkariData,
+    AkariDataSet,
+    AkariDataSetType,
+    AkariLogger,
+    AkariModule,
+    AkariModuleParams,
+    AkariModuleType,
+    AkariRouter,
+)
+
+
+@dataclasses.dataclass
+class _SaveModuleParams:
+    file_path: str
+    save_from_data: str
+    with_timestamp: bool = False
+
+
+class _SaveModule(AkariModule):
+    def __init__(
+        self,
+        router: AkariRouter,
+        logger: AkariLogger,
+    ) -> None:
+        super().__init__(router, logger)
+
+    def call(self, data: AkariData, params: _SaveModuleParams, callback: AkariModuleType | None = None) -> AkariDataSet:
+        try:
+            save_data = data.last().__dict__[params.save_from_data]
+        except KeyError:
+            raise ValueError(f"Data does not contain the key '{params.save_from_data}'.")
path: modules/io/save.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**例外連鎖を使用してください**

例外ハンドラ内で新しい例外を発生させる場合は、`raise ... from err` または `raise ... from None` を使用して例外の連鎖を維持することをお勧めします。これにより、デバッグ時に元の例外情報が保持されます。

```diff
try:
    save_data = data.last().__dict__[params.save_from_data]
except KeyError as err:
-    raise ValueError(f"Data does not contain the key '{params.save_from_data}'.")
+    raise ValueError(f"Data does not contain the key '{params.save_from_data}'.") from err
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        try:
            save_data = data.last().__dict__[params.save_from_data]
        except KeyError as err:
            raise ValueError(f"Data does not contain the key '{params.save_from_data}'.") from err
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

37-37: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-04T13:31:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+import dataclasses
+import time
+import wave
+from datetime import datetime
+
+from akari import (
+    AkariData,
+    AkariDataSet,
+    AkariDataSetType,
+    AkariLogger,
+    AkariModule,
+    AkariModuleParams,
+    AkariModuleType,
+    AkariRouter,
+)
+
+
+@dataclasses.dataclass
+class _SaveModuleParams:
+    file_path: str
+    save_from_data: str
+    with_timestamp: bool = False
+
+
+class _SaveModule(AkariModule):
+    def __init__(
+        self,
+        router: AkariRouter,
+        logger: AkariLogger,
+    ) -> None:
+        super().__init__(router, logger)
+
+    def call(self, data: AkariData, params: _SaveModuleParams, callback: AkariModuleType | None = None) -> AkariDataSet:
+        try:
+            save_data = data.last().__dict__[params.save_from_data]
+        except KeyError:
+            raise ValueError(f"Data does not contain the key '{params.save_from_data}'.")
+        if not save_data:
+            raise ValueError(f"Data does not contain the key '{params.save_from_data}' or it is empty.")
+
+        path = params.file_path
+        if params.with_timestamp:
+            paths = path.split(".")
+            timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
+            if len(paths) > 1:
+                path = f"{'.'.join(paths[:-1])}_{timestamp}.{paths[-1]}"
+            else:
+                path = f"{path}_{timestamp}"
+
+        if path.endswith(".wav") and params.save_from_data == "audio":
+            audio_data = data.last().__dict__[params.save_from_data]
+            with wave.open(path, "wb") as wav_file:
+                wav_file.setnchannels(audio_data.channels if hasattr(audio_data, "channels") else 1)
+                wav_file.setsampwidth(audio_data.sample_width if hasattr(audio_data, "sample_width") else 2)
+                wav_file.setframerate(audio_data.rate if hasattr(audio_data, "rate") else 16000)
+                wav_file.writeframes(audio_data.main)
+            self._logger.debug("Audio data saved as WAV to %s", path)
+        else:
+            with open(path, "wb") as file:
+                file.write(data.last().__dict__[params.save_from_data].main)
+            self._logger.debug("Data saved to %s", path)
+
+        return data.last()
path: modules/io/save.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**ファイル操作に関する例外処理を追加してください**

ファイル操作（書き込み）に関する例外処理が実装されていません。ディレクトリが存在しない場合や、アクセス権限の問題、ディスク容量不足などの状況に対処するための例外処理を追加することをお勧めします。

```diff
try:
    if path.endswith(".wav") and params.save_from_data == "audio":
        audio_data = save_data
        with wave.open(path, "wb") as wav_file:
            wav_file.setnchannels(audio_data.channels if hasattr(audio_data, "channels") else 1)
            wav_file.setsampwidth(audio_data.sample_width if hasattr(audio_data, "sample_width") else 2)
            wav_file.setframerate(audio_data.rate if hasattr(audio_data, "rate") else 16000)
            wav_file.writeframes(audio_data.main)
        self._logger.debug("Audio data saved as WAV to %s", path)
    else:
        with open(path, "wb") as file:
            file.write(save_data.main)
        self._logger.debug("Data saved to %s", path)
+ except (IOError, OSError) as err:
+     self._logger.error("Failed to save data to %s: %s", path, str(err))
+     raise RuntimeError(f"Failed to save data to {path}") from err
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    def call(self, data: AkariData, params: _SaveModuleParams, callback: AkariModuleType | None = None) -> AkariDataSet:
        try:
            save_data = data.last().__dict__[params.save_from_data]
        except KeyError:
            raise ValueError(f"Data does not contain the key '{params.save_from_data}'.")
        if not save_data:
            raise ValueError(f"Data does not contain the key '{params.save_from_data}' or it is empty.")

        path = params.file_path
        if params.with_timestamp:
            paths = path.split(".")
            timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
            if len(paths) > 1:
                path = f"{'.'.join(paths[:-1])}_{timestamp}.{paths[-1]}"
            else:
                path = f"{path}_{timestamp}"

        try:
            if path.endswith(".wav") and params.save_from_data == "audio":
                audio_data = save_data
                with wave.open(path, "wb") as wav_file:
                    wav_file.setnchannels(
                        audio_data.channels if hasattr(audio_data, "channels") else 1
                    )
                    wav_file.setsampwidth(
                        audio_data.sample_width if hasattr(audio_data, "sample_width") else 2
                    )
                    wav_file.setframerate(
                        audio_data.rate if hasattr(audio_data, "rate") else 16000
                    )
                    wav_file.writeframes(audio_data.main)
                self._logger.debug("Audio data saved as WAV to %s", path)
            else:
                with open(path, "wb") as file:
                    file.write(save_data.main)
                self._logger.debug("Data saved to %s", path)
        except (IOError, OSError) as err:
            self._logger.error("Failed to save data to %s: %s", path, str(err))
            raise RuntimeError(f"Failed to save data to {path}") from err

        return data.last()
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

37-37: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-04T13:31:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-04T13:13:59Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

この変更では、`modules/io` パッケージに新たな保存モジュールが追加されました。`modules/io/save.py` で `_SaveModule` クラスおよびそのパラメータクラス `_SaveModuleParams` が実装され、指定されたデータフィールドをファイルに保存する機能を提供します。`modules/io/__init__.py` ではこれらをパブリックAPIとして `SaveModule`, `SaveModuleParams` の名前でエクスポートしています。また、`.gitignore` に `/tmp/` ディレクトリ配下を無視するパターンが追加されました。

## Changes

| ファイル/グループ                | 変更内容の概要                                                                                                    |
|----------------------------------|------------------------------------------------------------------------------------------------------------------|
| .gitignore                       | ルート直下の `/tmp/` ディレクトリ配下を無視するパターンを追加。                                                   |
| modules/io/__init__.py           | `_SaveModule`, `_SaveModuleParams` をインポートし、それぞれ `SaveModule`, `SaveModuleParams` としてエクスポート。`__all__` を定義。 |
| modules/io/save.py               | データをファイルに保存する `_SaveModule` クラスとパラメータ用の `_SaveModuleParams` クラスを新規追加。            |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant SaveModule
    participant AkariData
    participant FileSystem

    User->>SaveModule: call(data, params)
    SaveModule->>AkariData: 指定キーのデータ取得
    alt データが存在しない場合
        SaveModule-->>User: エラー発生
    else データが存在する場合
        alt with_timestamp = True
            SaveModule->>SaveModule: ファイル名にタイムスタンプ追加
        end
        alt 拡張子が .wav かつデータが音声
            SaveModule->>FileSystem: wave モジュールで音声データ保存
        else
            SaveModule->>FileSystem: バイナリデータとして保存
        end
        SaveModule-->>User: 保存完了（データセット返却）
    end
```


<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=kizuna-org/akari-poc&utm_content=91):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: shiron-dev
created_at: 2025-05-04T13:34:18Z
body: @coderabbitai
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-04T13:35:06Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
> [!TIP]
> For best results, initiate chat on the files or code changes.

<details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Check out the PR branch to ensure we're working with the correct code
git fetch origin pull/91/head:pr-91
git checkout pr-91

# Let's first see what files have changed in this PR for a better understanding
git show --name-status

# Look at the contents of the added/modified files
echo -e "\n\n=== .gitignore ==="
cat .gitignore

echo -e "\n\n=== modules/io/__init__.py ==="
cat modules/io/__init__.py

echo -e "\n\n=== modules/io/save.py ==="
cat modules/io/save.py
```

Length of output: 7660

---



</details>

@shiron-dev Hi there! I noticed you mentioned me but didn't include any specific question or request. I see you've implemented a new `SaveModule` in the `modules/io` package that allows saving data to files with optional timestamp support and special handling for WAV audio files.

How can I help you with this PR? I'd be happy to:
- Review the code implementation in more detail
- Generate docstrings for your new module
- Suggest test cases
- Provide feedback on the design
- Answer any questions you might have

Let me know what you'd like me to do!

(ʘ ᴥ ʘ)


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Check out the PR branch to ensure we're working with the correct code
git fetch origin pull/91/head:pr-91
git checkout pr-91

# Let's first see what files have changed in this PR for a better understanding
git show --name-status

# Look at the contents of the added/modified files
echo -e "\n\n=== .gitignore ==="
cat .gitignore

echo -e "\n\n=== modules/io/__init__.py ==="
cat modules/io/__init__.py

echo -e "\n\n=== modules/io/save.py ==="
cat modules/io/save.py
```

Length of output: 7660

---



</details>

@shiron-dev Hi there! I noticed you mentioned me but didn't include any specific question or request. I see you've implemented a new `SaveModule` in the `modules/io` package that allows saving data to files with optional timestamp support and special handling for WAV audio files.

How can I help you with this PR? I'd be happy to:
- Review the code implementation in more detail
- Generate docstrings for your new module
- Suggest test cases
- Provide feedback on the design
- Answer any questions you might have

Let me know what you'd like me to do!

(ʘ ᴥ ʘ)</answer></rawResChunk> -->
author_association: CONTRIBUTOR