├── .clinerules ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── ruff.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs └── design.md ├── pyproject.toml ├── src ├── __init__.py ├── main.py ├── markdown_converter.py ├── mcp_server.py └── notion_client.py ├── tests └── conftest.py └── uv.lock /.clinerules: -------------------------------------------------------------------------------- 1 | # Cline Rules 2 | 3 | ## ロール定義 4 | 5 | あなたは熟練のPythonプログラマとしてコードを書いてください。 6 | 7 | 8 | ## 期待する回答 9 | 10 | - 実装コードは省略せず、完全な形で提供 11 | - 日本語での詳細な説明 12 | 13 | 14 | ## 注意事項 15 | 16 | ### 設計書 17 | 18 | - 新規開発時は docs ディレクトリ以下に以下の内容を含む設計書 `design.md`を作成してください: 19 | - 要件定義書 20 | - 設計書(概略・機能・クラス構成) 21 | - 既存のソフトウェアを修正する場合: 22 | - 既存の設計書を参照してソフトウェアを開発してください 23 | - 修正内容に応じて設計書も更新してください 24 | - 設計書を作成したら、コードを作成する前にユーザーに設計書のチェックを依頼してください 25 | 26 | ### コーディング規約 27 | 28 | - PEP8に従ったコードを書いてください 29 | - ruffのフォーマッタでファイルの保存と同時に自動整形するので、フォーマットの修正は不要です 30 | - GoogleスタイルのDocstringを書いてください 31 | 32 | ### テストコード 33 | 34 | - テストコードを tests ディレクトリ以下に src ディレクトリと同じ構成で作成してください 35 | - テストコードを作成したら pytest を実行してエラー無いことを確認してください。エラーが出たら修正してください 36 | 37 | ### Git操作 38 | 39 | - gitの操作はgit statusでステータス確認しながら慎重に行ってください 40 | - git管理されているファイルは、git mv や git rm を使って移動削除してください 41 | 42 | ### Pull Request(PR) 43 | 44 | #### PR作成時 45 | - PRを要望されたら、gitコマンドで差分を確認したうえで、`gh pr` コマンドを使ってPRを作成してください 46 | - PRのdescriptionは .github/pull_request_template.md を読み取ってフォーマットを合わせてください 47 | 48 | #### PRレビュー時 49 | 以下の手順でファイルごとにコメントを付けてください: 50 | 51 | 1. チェックする観点は .github/pull_request_template.md を参照してください 52 | 2. PRの差分を確認: 53 | ```bash 54 | gh pr diff 55 | ``` 56 | 57 | 3. ファイルごとに、変更後のファイル全体とPRの差分を確認した上でレビューコメントを追加: 58 | ```bash 59 | gh api repos///pulls//comments \ 60 | -F body="レビューコメント" \ 61 | -F commit_id="$(gh pr view --json headRefOid --jq .headRefOid)" \ 62 | -F path="対象ファイルのパス" \ 63 | -F position= 64 | ``` 65 | 66 | パラメータの説明: 67 | - position: diffの行番号(新規ファイルの場合は1から開始) 68 | - commit_id: PRの最新のコミットIDを自動取得 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: バグ報告 3 | about: バグの報告用テンプレート 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | # バグ報告 10 | 11 | ## バグの概要 12 | バグの内容を簡潔に説明してください。 13 | 14 | ## 再現手順 15 | 1. 16 | 2. 17 | 3. 18 | 19 | ## 期待する動作 20 | 本来期待される動作を説明してください。 21 | 22 | ## 実際の動作 23 | 実際に起きている動作を説明してください。 24 | 25 | ## 環境情報 26 | - OS: [例: macOS 14.0] 27 | - Python: [例: 3.11.0] 28 | - Pyxel: [例: 1.9.0] 29 | 30 | ## スクリーンショット 31 | 可能であれば、問題の説明に役立つスクリーンショットを添付してください。 32 | 33 | ## 補足情報 34 | 問題に関する追加情報があれば記載してください。 35 | 36 | ## 再現用コード 37 | ```python 38 | # 問題を再現するための最小限のコード -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 機能要望 3 | about: 新機能の提案用テンプレート 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | # 機能要望 10 | 11 | ## 概要 12 | 提案する機能の概要を説明してください。 13 | 14 | ## 背景・目的 15 | この機能が必要な理由や背景を説明してください。 16 | 17 | ## 提案内容 18 | 具体的な機能の実装案があれば記載してください。 19 | 20 | ### 期待する動作 21 | 1. 22 | 2. 23 | 3. 24 | 25 | ### 技術的な考慮事項 26 | - 27 | - 28 | 29 | ## 代替案 30 | 他に検討した実装方法があれば記載してください。 31 | 32 | ## その他 33 | - 参考情報 34 | - 関連するIssue/PR 35 | - スクリーンショット(必要な場合) 36 | 37 | ## チェックリスト 38 | - [ ] 既存の機能と重複していない 39 | - [ ] 実装の技術的な実現可能性を確認している -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | 3 | ## 変更内容 4 | 変更の概要を簡潔に記載してください。 5 | 6 | ### 追加した機能 7 | - 8 | - 9 | 10 | ### 変更・修正した機能 11 | - 12 | - 13 | 14 | ### 削除した機能 15 | - 16 | - 17 | 18 | ## 変更理由 19 | この変更が必要な理由や背景を説明してください。 20 | 21 | ## 影響範囲 22 | この変更による影響範囲を記載してください。 23 | - [ ] 既存の機能への影響 24 | - [ ] パフォーマンスへの影響 25 | - [ ] セキュリティへの影響 26 | 27 | ## 動作確認 28 | - [ ] ユニットテストの追加・更新 29 | - [ ] 動作確認の実施 30 | - [ ] ドキュメントの更新 31 | 32 | ## スクリーンショット 33 | 必要に応じてスクリーンショットを添付してください。 34 | 35 | ## 補足情報 36 | レビュアーに伝えたい追加情報があれば記載してください。 37 | 38 | ## チェックリスト 39 | - [ ] コーディング規約に準拠している 40 | - [ ] 適切なコメントを追加している 41 | - [ ] 必要なテストを追加している 42 | - [ ] ドキュメントを更新している 43 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: Ruff 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | ruff: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.10" 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install ruff==0.9.1 24 | 25 | - name: Run Ruff Check 26 | run: ruff check --line-length=127 27 | 28 | - name: Run Ruff Format Check 29 | run: ruff format --check --diff --line-length=127 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | 173 | .DS_Store 174 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 karaage 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NotionMCP Light 2 | 3 | NotionMCP Lightは、Notion APIを使用してMarkdownファイルとNotionページを同期するModel Context Protocol (MCP)サーバーです。 4 | 5 | ## 概要 6 | 7 | このプロジェクトは、Notionの公式Model Context Protocol (MCP)サーバーが抱える非効率性(Markdownをブロック単位で読み書きし、LLMトークンを消費する点)を解決するために開発されました。トークンを使用せず、API経由で直接MarkdownファイルとNotionのページ/データベースを同期できる非公式のMCPサーバーを提供します。 8 | 9 | ## 機能 10 | 11 | - **Markdown → Notion** 12 | - H1をページタイトルとして認識 13 | - Markdownの内容をNotionページまたはデータベースのページとして作成 14 | - データベースIDを指定可能 15 | - Notion APIを直接使用(トークン未使用) 16 | 17 | - **Notion → Markdown** 18 | - 指定されたページまたはデータベースのページをMarkdown形式に変換 19 | - タイトルをH1として出力 20 | - ブロック構造をMarkdownに変換 21 | - ファイルに保存 22 | 23 | - **MCPサーバー対応** 24 | - Model Context Protocol(MCP)に準拠 25 | - CursorやClineなどのAIツールから呼び出し可能なエンドポイントを提供 26 | - JSON-RPC over stdioベースで動作 27 | 28 | ## インストール 29 | 30 | ### 依存関係のインストール 31 | 32 | ```bash 33 | # uvがインストールされていない場合は先にインストール 34 | # pip install uv 35 | 36 | # 依存関係のインストール 37 | uv sync 38 | ``` 39 | 40 | ### Notion API Tokenの設定 41 | 42 | 1. [Notion Developers](https://developers.notion.com/)でアカウントを作成し、APIトークンを取得します。 43 | 2. 環境変数に設定するか、`.env`ファイルを作成してトークンを設定します。 44 | 45 | ```bash 46 | # .envファイルの例 47 | NOTION_TOKEN=your_notion_api_token 48 | ``` 49 | 50 | ## 使い方 51 | 52 | ### MCPサーバーの起動 53 | 54 | #### uvを使用する場合(推奨) 55 | 56 | ```bash 57 | uv run python -m src.main 58 | ``` 59 | 60 | または、トークンを直接指定する場合: 61 | 62 | ```bash 63 | uv run python -m src.main --token your_notion_api_token 64 | ``` 65 | 66 | #### 通常のPythonを使用する場合 67 | 68 | ```bash 69 | python -m src.main 70 | ``` 71 | 72 | または、トークンを直接指定する場合: 73 | 74 | ```bash 75 | python -m src.main --token your_notion_api_token 76 | ``` 77 | 78 | ### Cline/Cursorでの設定 79 | 80 | Cline/CursorなどのAIツールでNotionMCP Lightを使用するには、`mcp_settings.json`ファイルに以下のような設定を追加します: 81 | 82 | ```json 83 | "notion-mcp-light": { 84 | "command": "uv", 85 | "args": [ 86 | "run", 87 | "--directory", 88 | "/path/to/notion-mcp-light", 89 | "python", 90 | "-m", 91 | "src.main" 92 | ], 93 | "env": { 94 | "NOTION_TOKEN": "your_notion_api_token" 95 | }, 96 | "disabled": false, 97 | "alwaysAllow": [] 98 | } 99 | ``` 100 | 101 | `/path/to/notion-mcp-light`は、NotionMCP Lightのインストールディレクトリに置き換えてください。 102 | 103 | ## MCPツールの使用方法 104 | 105 | NotionMCP Lightは以下のMCPツールを提供します: 106 | 107 | ### uploadMarkdown 108 | 109 | Markdownファイルをアップロードし、Notionページとして作成します。 110 | 111 | ```json 112 | { 113 | "jsonrpc": "2.0", 114 | "method": "uploadMarkdown", 115 | "params": { 116 | "filepath": "path/to/markdown.md", 117 | "database_id": "optional_database_id", 118 | "page_id": "optional_parent_page_id" 119 | }, 120 | "id": 1 121 | } 122 | ``` 123 | 124 | ### downloadMarkdown 125 | 126 | NotionページをダウンロードしてMarkdownファイルとして保存します。 127 | 128 | ```json 129 | { 130 | "jsonrpc": "2.0", 131 | "method": "downloadMarkdown", 132 | "params": { 133 | "page_id": "notion_page_id", 134 | "output_path": "path/to/output.md" 135 | }, 136 | "id": 2 137 | } 138 | ``` 139 | 140 | ## ライセンス 141 | 142 | このプロジェクトはMITライセンスの下で公開されています。詳細は[LICENSE](LICENSE)ファイルを参照してください。 143 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | 2 | # 要件・設計書 3 | 4 | ## 1. 要件定義 5 | 6 | ### 1.1 基本情報 7 | - ソフトウェア名称: NotionMCP Light 8 | - リポジトリ名: notion-mcp-light 9 | 10 | ### 1.2 プロジェクト概要 11 | 12 | 本プロジェクトは、Notionの公式Model Context Protocol (MCP)サーバーが抱える非効率性(Markdownをブロック単位で読み書きし、LLMトークンを消費する点)を解決するために、トークンを使用せず、API経由で直接MarkdownファイルとNotionのページ/データベースを同期できる非公式のMCPサーバーを構築することを目的とする。 13 | 14 | ### 1.3 機能要件 15 | 16 | #### 1.3.1 Markdown → Notion 17 | - H1をページタイトルとして認識 18 | - Markdownの内容をNotionページまたはデータベースのページとして作成 19 | - データベースIDを指定可能 20 | - Notion APIを直接使用(トークン未使用) 21 | 22 | #### 1.3.2 Notion → Markdown 23 | - 指定されたページまたはデータベースのページをMarkdown形式に変換 24 | - タイトルをH1として出力 25 | - ブロック構造をMarkdownに変換 26 | - ファイルに保存 27 | 28 | #### 1.3.3 MCPサーバー対応 29 | - Model Context Protocol(MCP)に準拠 30 | - CursorやClineなどのAIツールから呼び出し可能なエンドポイントを提供 31 | - JSON-RPC over stdioベースで動作(予定) 32 | 33 | ### 1.4 非機能要件 34 | 35 | - 迅速なレスポンス(LLMを使用しないため) 36 | - ストリーム処理不要(非同期でなくても良い) 37 | - シンプルな構成とメンテナンス性重視 38 | 39 | ### 1.5 制約条件 40 | 41 | - Python 3.10以上で動作 42 | - Notion API Tokenが必要 43 | - Markdown構造に大きな自由度は持たせない(H1 = タイトル) 44 | 45 | ### 1.6 開発環境 46 | 47 | - 言語: Python 48 | - フレームワーク: FastAPI(または標準JSON-RPC) 49 | - 外部ライブラリ: 50 | - `notion-sdk-py` 51 | - `python-sdk` (Model Context Protocol) 52 | - `markdown`, `mistune`(Markdown処理用) 53 | 54 | ### 1.7 成果物 55 | 56 | - Python製MCPサーバースクリプト 57 | - ドキュメント変換ユーティリティ 58 | - README / 利用手順 59 | - 設計書 60 | 61 | ## 2. システム設計 62 | 63 | ### 2.1 システム概要設計 64 | 65 | #### 2.1.1 システムアーキテクチャ 66 | ``` 67 | [MCPクライアント(Cline, Cursor)] <-> [MCPサーバー (Python)] <-> [Notion API] 68 | | 69 | [Markdownファイル I/O] 70 | ``` 71 | 72 | #### 2.1.2 主要コンポーネント 73 | - **MCPサーバー** 74 | - JSON-RPC over stdioをリッスン 75 | - Notionとの連携を管理 76 | - **Notionクライアント** 77 | - APIラッパー 78 | - ページの作成・取得 79 | - **Markdownコンバータ** 80 | - Markdown → Notionブロック変換 81 | - Notionブロック → Markdown変換 82 | 83 | ### 2.2 詳細設計 84 | 85 | #### 2.2.1 クラス設計 86 | 87 | ##### `NotionClient` 88 | ```python 89 | class NotionClient: 90 | def upload_markdown(filepath: str, database_id: Optional[str] = None, page_id: Optional[str] = None) -> str 91 | def download_page(page_id: str, output_path: str) -> None 92 | ``` 93 | 94 | ##### `MarkdownConverter` 95 | ```python 96 | class MarkdownConverter: 97 | def parse_markdown_to_blocks(md: str) -> List[dict] 98 | def convert_blocks_to_markdown(blocks: List[dict]) -> str 99 | ``` 100 | 101 | ##### `MCPServer` 102 | ```python 103 | class MCPServer: 104 | def handle_upload_markdown(request) -> Response 105 | def handle_download_markdown(request) -> Response 106 | ``` 107 | 108 | ### 2.3 インターフェース設計 109 | 110 | - JSON-RPCエンドポイント: 111 | - `uploadMarkdown`: Markdownファイル → Notion 112 | - `downloadMarkdown`: Notionページ → Markdownファイル 113 | 114 | ### 2.4 セキュリティ設計 115 | 116 | - Notion APIトークンは環境変数で管理(`.env`) 117 | - 外部からの直接アクセスは制限(ローカル環境前提) 118 | 119 | ### 2.5 テスト設計 120 | 121 | - 単体テスト 122 | - Markdown変換の正確性 123 | - Notion APIの応答確認 124 | - 統合テスト 125 | - 実ファイルを使用した変換テスト 126 | - MCPリクエストを模擬した動作確認 127 | 128 | ### 2.6 開発環境・依存関係 129 | 130 | - Python 3.10+ 131 | - `notion-client` 132 | - `python-sdk`(MCP) 133 | - `markdown`, `mistune`, `dotenv` 134 | 135 | ### 2.7 開発工程 136 | 137 | | フェーズ | 内容 | 期間 | 138 | |---------|------|------| 139 | | 要件定義 | 本仕様書作成 | 第1週 | 140 | | 設計 | アーキテクチャ・モジュール設計 | 第1週 | 141 | | 実装 | 各モジュールの開発 | 第2-3週 | 142 | | テスト | 単体・統合テスト | 第4週 | 143 | | リリース | ドキュメント整備・デプロイ対応 | 第5週 | 144 | 145 | ## 3. Notion MCPサーバー比較 146 | 147 | ### 3.1 公式Notion MCPサーバーのシーケンス図(Markdown→Notion書き込み) 148 | 149 | 150 | ```mermaid 151 | sequenceDiagram 152 | participant LLM as LLM 153 | participant MCP as 公式Notion MCPサーバー 154 | participant MD as Markdownファイル 155 | participant API as Notion API 156 | participant Notion as Notionページ 157 | 158 | %% Markdownからページ作成のケース 159 | LLM->>MD: Markdownファイル読み込み 160 | MD-->>LLM: Markdown内容 161 | Note over LLM: Markdownをブロックに分割 162 | 163 | LLM->>MCP: ブロック1のページ作成リクエスト 164 | Note over LLM,MCP: LLMトークン消費量大 165 | MCP->>API: ブロック1のページ作成API呼び出し 166 | API->>Notion: ブロック1を作成 167 | Notion-->>API: 作成完了 168 | API-->>MCP: レスポンス 169 | MCP-->>LLM: 結果返却 170 | Note over LLM,MCP: LLMトークン消費量大 171 | 172 | LLM->>MCP: ブロック2のページ作成リクエスト 173 | Note over LLM,MCP: LLMトークン消費量大 174 | MCP->>API: ブロック2のページ作成API呼び出し 175 | API->>Notion: ブロック2を作成 176 | Notion-->>API: 作成完了 177 | API-->>MCP: レスポンス 178 | MCP-->>LLM: 結果返却 179 | Note over LLM,MCP: LLMトークン消費量大 180 | 181 | Note over LLM,Notion: ブロック3...Nについても同様に繰り返し 182 | ``` 183 | 184 | 185 | ### 3.2 NotionMCP Lightのシーケンス図(Markdown→Notion書き込み) 186 | 187 | 188 | ```mermaid 189 | sequenceDiagram 190 | participant LLM as LLM 191 | participant MCP as NotionMCP Light 192 | participant MD as Markdownファイル 193 | participant API as Notion API 194 | participant Notion as Notionページ 195 | 196 | %% Markdownからページ作成のケース 197 | LLM->>MCP: uploadMarkdownリクエスト 198 | Note over LLM,MCP: LLMトークン消費量小 199 | MCP->>MD: Markdownファイル読み込み 200 | MD-->>MCP: Markdown内容 201 | MCP->>MCP: MarkdownConverterでブロック変換 202 | MCP->>API: NotionClientでページ作成API呼び出し 203 | API->>Notion: ページ作成 204 | Notion-->>API: 作成完了 205 | API-->>MCP: レスポンス 206 | MCP-->>LLM: 結果返却 207 | Note over LLM,MCP: LLMトークン消費量小 208 | ``` 209 | 210 | 211 | ### 3.3 主な違い 212 | 213 | 1. **LLMトークン消費**: 214 | - 公式MCPサーバー: ブロックごとに処理するため、LLMトークン消費量が大きい 215 | - NotionMCP Light: 一括処理のため、LLMトークン消費量が小さい 216 | 217 | 2. **処理方法**: 218 | - 公式MCPサーバー: LLMがMarkdownを読み込み、ブロック単位で何度もAPIを呼び出す 219 | - NotionMCP Light: MCPサーバーがMarkdownファイル全体を一括処理 220 | 221 | 3. **効率性**: 222 | - 公式MCPサーバー: ブロックごとの処理によりトークン消費が多く、API呼び出しも複数回 223 | - NotionMCP Light: ファイル操作による直接同期で効率的、API呼び出しは1回 224 | 225 | 4. **データフロー**: 226 | - 公式MCPサーバー: LLM→Markdownファイル→LLM→MCP→Notion API(ブロックごとに繰り返し) 227 | - NotionMCP Light: LLM→MCP→Markdownファイル→Notion API(一括処理) 228 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "notion-mcp-light" 7 | version = "0.1.0" 8 | description = "Notion MCP Light - Notion APIを使用してMarkdownファイルとNotionページを同期するMCPサーバー" 9 | authors = [ 10 | {name = "NotionMCP Light Team"} 11 | ] 12 | readme = "README.md" 13 | requires-python = ">=3.10" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.10", 17 | "Operating System :: OS Independent", 18 | "License :: OSI Approved :: MIT License", 19 | "Topic :: Text Processing :: Markup :: Markdown", 20 | "Topic :: Office/Business :: Office Suites", 21 | ] 22 | dependencies = [ 23 | "notion-client", 24 | "mcp[cli]", 25 | "markdown", 26 | "mistune", 27 | "python-dotenv", 28 | ] 29 | 30 | [project.optional-dependencies] 31 | dev = [ 32 | "pytest", 33 | ] 34 | 35 | [project.scripts] 36 | notion-mcp-light = "main:main" 37 | 38 | [tool.setuptools] 39 | package-dir = {"" = "src"} 40 | 41 | [tool.setuptools.packages.find] 42 | where = ["src"] -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | # このファイルは notion_mcp_light パッケージを認識するためのものです。 2 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | NotionMCP Light 4 | 5 | Notion APIを使用してMarkdownファイルとNotionページを同期するMCPサーバー 6 | """ 7 | 8 | import os 9 | import sys 10 | import argparse 11 | from dotenv import load_dotenv 12 | 13 | from .mcp_server import MCPServer 14 | 15 | 16 | def main(): 17 | """ 18 | メイン関数 19 | 20 | コマンドライン引数を解析し、MCPサーバーを起動します。 21 | """ 22 | # コマンドライン引数の解析 23 | parser = argparse.ArgumentParser( 24 | description="NotionMCP Light - Notion APIを使用してMarkdownファイルとNotionページを同期するMCPサーバー" 25 | ) 26 | parser.add_argument("--token", help="Notion API Token(指定されない場合は環境変数から取得)") 27 | args = parser.parse_args() 28 | 29 | # 環境変数の読み込み 30 | load_dotenv() 31 | 32 | # トークンの取得 33 | token = args.token or os.getenv("NOTION_TOKEN") 34 | 35 | if not token: 36 | print( 37 | "エラー: Notion API Tokenが指定されていません。--tokenオプションまたは環境変数NOTION_TOKENを設定してください。", 38 | file=sys.stderr, 39 | ) 40 | sys.exit(1) 41 | 42 | try: 43 | # MCPサーバーの起動 44 | server = MCPServer(token) 45 | server.start() 46 | 47 | except KeyboardInterrupt: 48 | print("サーバーを終了します。", file=sys.stderr) 49 | sys.exit(0) 50 | 51 | except Exception as e: 52 | print(f"エラーが発生しました: {str(e)}", file=sys.stderr) 53 | sys.exit(1) 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | -------------------------------------------------------------------------------- /src/markdown_converter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Markdownコンバータモジュール 3 | 4 | NotionのブロックとMarkdown形式の相互変換を行うクラスを提供します。 5 | """ 6 | 7 | from typing import List, Dict, Any 8 | import mistune 9 | 10 | 11 | class MarkdownConverter: 12 | """ 13 | MarkdownとNotionブロック間の変換を行うクラス 14 | 15 | MarkdownテキストをパースしてNotionブロック形式に変換する機能と、 16 | NotionブロックをパースしてMarkdown形式に変換する機能を提供します。 17 | """ 18 | 19 | def __init__(self): 20 | """ 21 | MarkdownConverterのコンストラクタ 22 | """ 23 | self.markdown_parser = mistune.create_markdown() 24 | 25 | def parse_markdown_to_blocks(self, md: str) -> List[Dict[str, Any]]: 26 | """ 27 | Markdownテキストをパースし、Notionブロック形式に変換します。 28 | 29 | Args: 30 | md: 変換するMarkdownテキスト 31 | 32 | Returns: 33 | Notionブロック形式のリスト 34 | """ 35 | # Markdownを行ごとに分割 36 | lines = md.strip().split("\n") 37 | blocks = [] 38 | 39 | # タイトル(H1)を抽出 40 | title = None 41 | content_start_idx = 0 42 | 43 | # H1をタイトルとして扱う 44 | for i, line in enumerate(lines): 45 | if line.startswith("# "): 46 | title = line[2:].strip() 47 | content_start_idx = i + 1 48 | break 49 | 50 | # タイトルがない場合は空文字を設定 51 | if title is None: 52 | title = "" 53 | 54 | # 残りの内容をブロックに変換 55 | i = content_start_idx 56 | while i < len(lines): 57 | line = lines[i] 58 | 59 | # 見出し(H2-H6) 60 | if line.startswith("## "): 61 | blocks.append( 62 | { 63 | "type": "heading_2", 64 | "heading_2": {"rich_text": [{"type": "text", "text": {"content": line[3:].strip()}}]}, 65 | } 66 | ) 67 | elif line.startswith("### "): 68 | blocks.append( 69 | { 70 | "type": "heading_3", 71 | "heading_3": {"rich_text": [{"type": "text", "text": {"content": line[4:].strip()}}]}, 72 | } 73 | ) 74 | # 箇条書き 75 | elif line.startswith("- "): 76 | blocks.append( 77 | { 78 | "type": "bulleted_list_item", 79 | "bulleted_list_item": {"rich_text": [{"type": "text", "text": {"content": line[2:].strip()}}]}, 80 | } 81 | ) 82 | # 番号付きリスト 83 | elif line.strip() and line[0].isdigit() and ". " in line: 84 | content = line.split(". ", 1)[1] 85 | blocks.append( 86 | { 87 | "type": "numbered_list_item", 88 | "numbered_list_item": {"rich_text": [{"type": "text", "text": {"content": content.strip()}}]}, 89 | } 90 | ) 91 | # コードブロック 92 | elif line.startswith("```"): 93 | code_lines = [] 94 | language = line[3:].strip() 95 | i += 1 96 | 97 | while i < len(lines) and not lines[i].startswith("```"): 98 | code_lines.append(lines[i]) 99 | i += 1 100 | 101 | blocks.append( 102 | { 103 | "type": "code", 104 | "code": { 105 | "rich_text": [{"type": "text", "text": {"content": "\n".join(code_lines)}}], 106 | "language": language if language else "plain text", 107 | }, 108 | } 109 | ) 110 | # 通常のテキスト(段落) 111 | elif line.strip(): 112 | blocks.append( 113 | {"type": "paragraph", "paragraph": {"rich_text": [{"type": "text", "text": {"content": line.strip()}}]}} 114 | ) 115 | 116 | i += 1 117 | 118 | return blocks, title 119 | 120 | def convert_blocks_to_markdown(self, blocks: List[Dict[str, Any]], title: str = None) -> str: 121 | """ 122 | Notionブロックをパースし、Markdown形式に変換します。 123 | 124 | Args: 125 | blocks: 変換するNotionブロックのリスト 126 | title: ページのタイトル(指定された場合はH1として出力) 127 | 128 | Returns: 129 | Markdown形式のテキスト 130 | """ 131 | md_lines = [] 132 | 133 | # タイトルがあればH1として追加 134 | if title: 135 | md_lines.append(f"# {title}") 136 | md_lines.append("") # 空行を追加 137 | 138 | for block in blocks: 139 | block_type = block.get("type") 140 | 141 | if block_type == "paragraph": 142 | text_content = self._extract_text_content(block.get("paragraph", {}).get("rich_text", [])) 143 | md_lines.append(text_content) 144 | md_lines.append("") # 空行を追加 145 | 146 | elif block_type == "heading_1": 147 | text_content = self._extract_text_content(block.get("heading_1", {}).get("rich_text", [])) 148 | md_lines.append(f"# {text_content}") 149 | md_lines.append("") 150 | 151 | elif block_type == "heading_2": 152 | text_content = self._extract_text_content(block.get("heading_2", {}).get("rich_text", [])) 153 | md_lines.append(f"## {text_content}") 154 | md_lines.append("") 155 | 156 | elif block_type == "heading_3": 157 | text_content = self._extract_text_content(block.get("heading_3", {}).get("rich_text", [])) 158 | md_lines.append(f"### {text_content}") 159 | md_lines.append("") 160 | 161 | elif block_type == "bulleted_list_item": 162 | text_content = self._extract_text_content(block.get("bulleted_list_item", {}).get("rich_text", [])) 163 | md_lines.append(f"- {text_content}") 164 | 165 | elif block_type == "numbered_list_item": 166 | text_content = self._extract_text_content(block.get("numbered_list_item", {}).get("rich_text", [])) 167 | md_lines.append(f"1. {text_content}") 168 | 169 | elif block_type == "code": 170 | code_block = block.get("code", {}) 171 | language = code_block.get("language", "") 172 | text_content = self._extract_text_content(code_block.get("rich_text", [])) 173 | 174 | md_lines.append(f"```{language}") 175 | md_lines.append(text_content) 176 | md_lines.append("```") 177 | md_lines.append("") 178 | 179 | elif block_type == "to_do": 180 | todo_item = block.get("to_do", {}) 181 | checked = todo_item.get("checked", False) 182 | text_content = self._extract_text_content(todo_item.get("rich_text", [])) 183 | 184 | checkbox = "[x]" if checked else "[ ]" 185 | md_lines.append(f"- {checkbox} {text_content}") 186 | 187 | elif block_type == "quote": 188 | text_content = self._extract_text_content(block.get("quote", {}).get("rich_text", [])) 189 | md_lines.append(f"> {text_content}") 190 | md_lines.append("") 191 | 192 | return "\n".join(md_lines) 193 | 194 | def _extract_text_content(self, rich_text_list): 195 | """ 196 | Notionのリッチテキスト配列からプレーンテキストを抽出します。 197 | 198 | Args: 199 | rich_text_list: Notionのリッチテキスト配列 200 | 201 | Returns: 202 | 抽出されたプレーンテキスト 203 | """ 204 | if not rich_text_list: 205 | return "" 206 | 207 | return "".join([rt.get("text", {}).get("content", "") for rt in rich_text_list if "text" in rt]) 208 | -------------------------------------------------------------------------------- /src/mcp_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCPサーバーモジュール 3 | 4 | Model Context Protocol (MCP)に準拠したサーバーを提供します。 5 | JSON-RPC over stdioを使用してクライアントからのリクエストを処理します。 6 | """ 7 | 8 | import sys 9 | import json 10 | import logging 11 | from typing import Dict, Any, List, Optional 12 | from pathlib import Path 13 | 14 | from src.notion_client import NotionClient 15 | 16 | 17 | class MCPServer: 18 | """ 19 | Model Context Protocol (MCP)に準拠したサーバークラス 20 | 21 | JSON-RPC over stdioを使用してクライアントからのリクエストを処理します。 22 | 23 | Attributes: 24 | notion_client: NotionClientのインスタンス 25 | logger: ロガー 26 | """ 27 | 28 | def __init__(self, token: Optional[str] = None): 29 | """ 30 | MCPServerのコンストラクタ 31 | 32 | Args: 33 | token: Notion API Token(指定されない場合は環境変数から取得) 34 | """ 35 | self.notion_client = NotionClient(token) 36 | 37 | # ロガーの設定 38 | self.logger = logging.getLogger("mcp_server") 39 | self.logger.setLevel(logging.INFO) 40 | 41 | # ファイルハンドラの設定 42 | log_dir = Path("logs") 43 | log_dir.mkdir(exist_ok=True) 44 | file_handler = logging.FileHandler(log_dir / "mcp_server.log") 45 | file_handler.setLevel(logging.INFO) 46 | 47 | # フォーマッタの設定 48 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 49 | file_handler.setFormatter(formatter) 50 | 51 | # ハンドラの追加 52 | self.logger.addHandler(file_handler) 53 | 54 | def start(self): 55 | """ 56 | サーバーを起動し、stdioからのリクエストをリッスンします。 57 | """ 58 | self.logger.info("MCPサーバーを起動しました") 59 | 60 | # サーバー情報を出力 61 | self._send_response( 62 | { 63 | "jsonrpc": "2.0", 64 | "method": "server/info", 65 | "params": { 66 | "name": "notion-mcp-light", 67 | "version": "0.1.0", 68 | "description": "Notion MCP Light Server", 69 | "tools": self._get_tools(), 70 | "resources": self._get_resources(), 71 | }, 72 | } 73 | ) 74 | 75 | # ツール情報を出力 76 | self._send_response( 77 | { 78 | "jsonrpc": "2.0", 79 | "method": "tools/list", 80 | "params": { 81 | "tools": self._get_tools(), 82 | }, 83 | } 84 | ) 85 | 86 | # リクエストをリッスン 87 | while True: 88 | try: 89 | # 標準入力からリクエストを読み込む 90 | request_line = sys.stdin.readline() 91 | if not request_line: 92 | break 93 | 94 | # リクエストをパース 95 | request = json.loads(request_line) 96 | self.logger.info(f"リクエストを受信しました: {request}") 97 | 98 | # リクエストを処理 99 | self._handle_request(request) 100 | 101 | except json.JSONDecodeError: 102 | self.logger.error("JSONのパースに失敗しました") 103 | self._send_error(-32700, "Parse error", None) 104 | 105 | except Exception as e: 106 | self.logger.error(f"エラーが発生しました: {str(e)}") 107 | self._send_error(-32603, f"Internal error: {str(e)}", None) 108 | 109 | def _handle_request(self, request: Dict[str, Any]): 110 | """ 111 | リクエストを処理します。 112 | 113 | Args: 114 | request: JSONリクエスト 115 | """ 116 | # リクエストのバリデーション 117 | if "jsonrpc" not in request or request["jsonrpc"] != "2.0": 118 | self._send_error(-32600, "Invalid Request", request.get("id")) 119 | return 120 | 121 | if "method" not in request: 122 | self._send_error(-32600, "Method not specified", request.get("id")) 123 | return 124 | 125 | # メソッドの取得 126 | method = request["method"] 127 | params = request.get("params", {}) 128 | request_id = request.get("id") 129 | 130 | # メソッドの処理 131 | if method == "initialize": 132 | self._handle_initialize(params, request_id) 133 | elif method == "tools/list": 134 | self._handle_tools_list(request_id) 135 | elif method == "tools/call": 136 | self._handle_tools_call(params, request_id) 137 | elif method == "uploadMarkdown": 138 | self._handle_upload_markdown(params, request_id) 139 | elif method == "downloadMarkdown": 140 | self._handle_download_markdown(params, request_id) 141 | else: 142 | self._send_error(-32601, f"Method not found: {method}", request_id) 143 | 144 | def _handle_initialize(self, params: Dict[str, Any], request_id: Any): 145 | """ 146 | initializeメソッドを処理します。 147 | 148 | Args: 149 | params: リクエストパラメータ 150 | request_id: リクエストID 151 | """ 152 | # クライアント情報を取得(オプション) 153 | client_name = params.get("client_name", "unknown") 154 | client_version = params.get("client_version", "unknown") 155 | 156 | self.logger.info(f"クライアント '{client_name} {client_version}' が接続しました") 157 | 158 | # サーバーの機能を返す 159 | response = { 160 | "protocolVersion": "2024-11-05", 161 | "serverInfo": {"name": "notion-mcp-light", "version": "0.1.0", "description": "Notion MCP Light Server"}, 162 | "capabilities": {"tools": {"listChanged": False}, "resources": {"listChanged": False, "subscribe": False}}, 163 | "instructions": "NotionMCP Lightを使用する際の注意点:\n1. filepathパラメータには絶対パスを使用してください。相対パスを使用すると正しく処理できない場合があります。\n2. page_idパラメータにはNotionページIDを使用してください。URLではなくページIDのみを指定してください。\n3. Notionページのページ形式のURLからページIDを抽出するには、URLの最後の部分(https://www.notion.so/xxx/yyy-zzz の zzz部分)を使用してください。\n4. データベースIDを指定する場合も同様に、URLではなくIDのみを使用してください。", 164 | } 165 | 166 | self._send_result(response, request_id) 167 | 168 | # ツール情報を送信 169 | self._send_response( 170 | { 171 | "jsonrpc": "2.0", 172 | "method": "tools/list", 173 | "params": { 174 | "tools": self._get_tools(), 175 | }, 176 | } 177 | ) 178 | 179 | def _handle_upload_markdown(self, params: Dict[str, Any], request_id: Any): 180 | """ 181 | uploadMarkdownメソッドを処理します。 182 | 183 | Args: 184 | params: リクエストパラメータ 185 | request_id: リクエストID 186 | """ 187 | # パラメータのバリデーション 188 | if "filepath" not in params: 189 | self._send_error(-32602, "Invalid params: filepath is required", request_id) 190 | return 191 | 192 | filepath = params["filepath"] 193 | database_id = params.get("database_id") 194 | 195 | try: 196 | # Markdownをアップロード 197 | page_id = self.notion_client.upload_markdown(filepath, database_id) 198 | 199 | # レスポンスを送信 200 | self._send_result({"page_id": page_id}, request_id) 201 | 202 | except FileNotFoundError: 203 | self._send_error(-32602, f"File not found: {filepath}", request_id) 204 | 205 | except Exception as e: 206 | self._send_error(-32603, f"Failed to upload markdown: {str(e)}", request_id) 207 | 208 | def _handle_download_markdown(self, params: Dict[str, Any], request_id: Any): 209 | """ 210 | downloadMarkdownメソッドを処理します。 211 | 212 | Args: 213 | params: リクエストパラメータ 214 | request_id: リクエストID 215 | """ 216 | # パラメータのバリデーション 217 | if "page_id" not in params: 218 | self._send_error(-32602, "Invalid params: page_id is required", request_id) 219 | return 220 | 221 | if "output_path" not in params: 222 | self._send_error(-32602, "Invalid params: output_path is required", request_id) 223 | return 224 | 225 | page_id = params["page_id"] 226 | output_path = params["output_path"] 227 | 228 | try: 229 | # ページをダウンロード 230 | self.notion_client.download_page(page_id, output_path) 231 | 232 | # レスポンスを送信 233 | self._send_result({"output_path": output_path}, request_id) 234 | 235 | except Exception as e: 236 | self._send_error(-32603, f"Failed to download markdown: {str(e)}", request_id) 237 | 238 | def _send_result(self, result: Any, request_id: Any): 239 | """ 240 | 成功レスポンスを送信します。 241 | 242 | Args: 243 | result: レスポンス結果 244 | request_id: リクエストID 245 | """ 246 | response = {"jsonrpc": "2.0", "result": result, "id": request_id} 247 | 248 | self._send_response(response) 249 | 250 | def _send_error(self, code: int, message: str, request_id: Any): 251 | """ 252 | エラーレスポンスを送信します。 253 | 254 | Args: 255 | code: エラーコード 256 | message: エラーメッセージ 257 | request_id: リクエストID 258 | """ 259 | response = {"jsonrpc": "2.0", "error": {"code": code, "message": message}, "id": request_id} 260 | 261 | self._send_response(response) 262 | 263 | def _send_response(self, response: Dict[str, Any]): 264 | """ 265 | レスポンスを標準出力に送信します。 266 | 267 | Args: 268 | response: レスポンス 269 | """ 270 | response_json = json.dumps(response) 271 | print(response_json, flush=True) 272 | self.logger.info(f"レスポンスを送信しました: {response_json}") 273 | 274 | def _get_tools(self) -> List[Dict[str, Any]]: 275 | """ 276 | サーバーが提供するツールの一覧を取得します。 277 | 278 | Returns: 279 | ツールの一覧 280 | """ 281 | return [ 282 | { 283 | "name": "uploadMarkdown", 284 | "description": "Markdownファイルをアップロードし、Notionページとして作成します", 285 | "inputSchema": { 286 | "type": "object", 287 | "properties": { 288 | "filepath": { 289 | "type": "string", 290 | "description": "アップロードするMarkdownファイルのパス。絶対パスを指定してください。", 291 | }, 292 | "database_id": { 293 | "type": "string", 294 | "description": "アップロード先のデータベースID。URLでなくIDです。", 295 | }, 296 | "page_id": { 297 | "type": "string", 298 | "description": "親ページID(database_idが指定されていない場合に使用)。URLでなくIDです。", 299 | }, 300 | }, 301 | "required": ["filepath"], 302 | }, 303 | }, 304 | { 305 | "name": "downloadMarkdown", 306 | "description": "NotionページをダウンロードしてMarkdownファイルとして保存します", 307 | "inputSchema": { 308 | "type": "object", 309 | "properties": { 310 | "page_id": {"type": "string", "description": "ダウンロードするNotionページのID。URLでなくIDです。"}, 311 | "output_path": {"type": "string", "description": "出力先のファイルパス。絶対ファイルパス。"}, 312 | }, 313 | "required": ["page_id", "output_path"], 314 | }, 315 | }, 316 | ] 317 | 318 | def _handle_tools_call(self, params: Dict[str, Any], request_id: Any): 319 | """ 320 | tools/callメソッドを処理します。 321 | 322 | Args: 323 | params: リクエストパラメータ 324 | request_id: リクエストID 325 | """ 326 | # パラメータのバリデーション 327 | if "name" not in params: 328 | self._send_error(-32602, "Invalid params: name is required", request_id) 329 | return 330 | 331 | if "arguments" not in params: 332 | self._send_error(-32602, "Invalid params: arguments is required", request_id) 333 | return 334 | 335 | tool_name = params["name"] 336 | arguments = params["arguments"] 337 | 338 | # ツールの処理 339 | if tool_name == "uploadMarkdown": 340 | try: 341 | page_id = self.notion_client.upload_markdown( 342 | arguments["filepath"], arguments.get("database_id"), arguments.get("page_id") 343 | ) 344 | self._send_result( 345 | {"content": [{"type": "text", "text": f"Markdownファイルをアップロードしました。ページID: {page_id}"}]}, 346 | request_id, 347 | ) 348 | except FileNotFoundError: 349 | self._send_result( 350 | { 351 | "content": [{"type": "text", "text": f"ファイルが見つかりません: {arguments.get('filepath')}"}], 352 | "isError": True, 353 | }, 354 | request_id, 355 | ) 356 | except Exception as e: 357 | self._send_result( 358 | { 359 | "content": [{"type": "text", "text": f"Markdownファイルのアップロードに失敗しました: {str(e)}"}], 360 | "isError": True, 361 | }, 362 | request_id, 363 | ) 364 | elif tool_name == "downloadMarkdown": 365 | try: 366 | self.notion_client.download_page(arguments["page_id"], arguments["output_path"]) 367 | self._send_result( 368 | { 369 | "content": [ 370 | {"type": "text", "text": f"Notionページをダウンロードしました。出力先: {arguments['output_path']}"} 371 | ] 372 | }, 373 | request_id, 374 | ) 375 | except Exception as e: 376 | self._send_result( 377 | { 378 | "content": [{"type": "text", "text": f"Notionページのダウンロードに失敗しました: {str(e)}"}], 379 | "isError": True, 380 | }, 381 | request_id, 382 | ) 383 | else: 384 | self._send_result( 385 | {"content": [{"type": "text", "text": f"ツールが見つかりません: {tool_name}"}], "isError": True}, request_id 386 | ) 387 | 388 | def _handle_tools_list(self, request_id: Any): 389 | """ 390 | tools/listメソッドを処理します。 391 | 392 | Args: 393 | request_id: リクエストID 394 | """ 395 | tools = self._get_tools() 396 | self._send_result({"tools": tools}, request_id) 397 | 398 | def _get_resources(self) -> List[Dict[str, Any]]: 399 | """ 400 | サーバーが提供するリソースの一覧を取得します。 401 | 402 | Returns: 403 | リソースの一覧 404 | """ 405 | return [] 406 | -------------------------------------------------------------------------------- /src/notion_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Notionクライアントモジュール 3 | 4 | NotionのAPIを利用してMarkdownファイルのアップロードとNotionページのダウンロードを行うクラスを提供します。 5 | """ 6 | 7 | import os 8 | from typing import List, Dict, Any, Optional 9 | from pathlib import Path 10 | from notion_client import Client 11 | from dotenv import load_dotenv 12 | 13 | from src.markdown_converter import MarkdownConverter 14 | 15 | 16 | class NotionClient: 17 | """ 18 | NotionのAPIを利用してMarkdownファイルのアップロードとNotionページのダウンロードを行うクラス 19 | 20 | Attributes: 21 | client: Notion APIクライアント 22 | converter: MarkdownConverterのインスタンス 23 | """ 24 | 25 | def __init__(self, token: Optional[str] = None): 26 | """ 27 | NotionClientのコンストラクタ 28 | 29 | Args: 30 | token: Notion API Token(指定されない場合は環境変数から取得) 31 | """ 32 | # 環境変数からトークンを取得 33 | load_dotenv() 34 | self.token = token or os.getenv("NOTION_TOKEN") 35 | 36 | if not self.token: 37 | raise ValueError("Notion API Tokenが指定されていません。環境変数NOTION_TOKENを設定してください。") 38 | 39 | self.client = Client(auth=self.token) 40 | self.converter = MarkdownConverter() 41 | 42 | def upload_markdown(self, filepath: str, database_id: Optional[str] = None, page_id: Optional[str] = None) -> str: 43 | """ 44 | Markdownファイルを読み込み、Notionページとしてアップロードします。 45 | 46 | Args: 47 | filepath: アップロードするMarkdownファイルのパス 48 | database_id: アップロード先のデータベースID 49 | page_id: 親ページID(database_idが指定されていない場合に使用) 50 | 51 | Returns: 52 | 作成されたNotionページのID 53 | """ 54 | # ファイルを読み込む 55 | file_path = Path(filepath) 56 | if not file_path.exists(): 57 | raise FileNotFoundError(f"ファイルが見つかりません: {filepath}") 58 | 59 | with open(file_path, "r", encoding="utf-8") as f: 60 | markdown_content = f.read() 61 | 62 | # Markdownをパースしてブロックに変換 63 | blocks, title = self.converter.parse_markdown_to_blocks(markdown_content) 64 | 65 | # ページを作成 66 | if database_id: 67 | # データベース内にページを作成 68 | page = self.client.pages.create( 69 | parent={"database_id": database_id}, 70 | properties={"title": {"title": [{"text": {"content": title}}]}}, 71 | children=blocks, 72 | ) 73 | elif page_id: 74 | # 親ページの下に新規ページを作成 75 | page = self.client.pages.create( 76 | parent={"page_id": page_id}, 77 | properties={"title": {"title": [{"text": {"content": title}}]}}, 78 | children=blocks, 79 | ) 80 | else: 81 | # 親ページIDが指定されていない場合は、エラーを発生させる 82 | raise ValueError( 83 | "database_idまたはpage_idを指定してください。ワークスペースに直接ページを作成することはできません。" 84 | ) 85 | 86 | return page["id"] 87 | 88 | def download_page(self, page_id: str, output_path: str) -> None: 89 | """ 90 | NotionページをダウンロードしてMarkdownファイルとして保存します。 91 | 92 | Args: 93 | page_id: ダウンロードするNotionページのID 94 | output_path: 出力先のファイルパス 95 | """ 96 | # ページ情報を取得 97 | page = self.client.pages.retrieve(page_id) 98 | 99 | # ページタイトルを取得 100 | title = "" 101 | title_property = page["properties"].get("title", {}) 102 | if title_property and "title" in title_property: 103 | title_items = title_property["title"] 104 | if title_items: 105 | title = title_items[0].get("plain_text", "") 106 | 107 | # ブロックを取得 108 | blocks = [] 109 | has_more = True 110 | cursor = None 111 | 112 | while has_more: 113 | if cursor: 114 | response = self.client.blocks.children.list(block_id=page_id, start_cursor=cursor) 115 | else: 116 | response = self.client.blocks.children.list(block_id=page_id) 117 | 118 | blocks.extend(response["results"]) 119 | has_more = response["has_more"] 120 | cursor = response.get("next_cursor") 121 | 122 | # ブロックをMarkdownに変換 123 | markdown_content = self.converter.convert_blocks_to_markdown(blocks, title) 124 | 125 | # ファイルに保存 126 | output_file = Path(output_path) 127 | output_file.parent.mkdir(parents=True, exist_ok=True) 128 | 129 | with open(output_file, "w", encoding="utf-8") as f: 130 | f.write(markdown_content) 131 | 132 | def get_database_pages(self, database_id: str) -> List[Dict[str, Any]]: 133 | """ 134 | データベース内のすべてのページを取得します。 135 | 136 | Args: 137 | database_id: 取得するデータベースのID 138 | 139 | Returns: 140 | データベース内のページのリスト 141 | """ 142 | pages = [] 143 | has_more = True 144 | cursor = None 145 | 146 | while has_more: 147 | if cursor: 148 | response = self.client.databases.query(database_id=database_id, start_cursor=cursor) 149 | else: 150 | response = self.client.databases.query(database_id=database_id) 151 | 152 | pages.extend(response["results"]) 153 | has_more = response["has_more"] 154 | cursor = response.get("next_cursor") 155 | 156 | return pages 157 | 158 | def download_database(self, database_id: str, output_dir: str) -> None: 159 | """ 160 | データベース内のすべてのページをダウンロードしてMarkdownファイルとして保存します。 161 | 162 | Args: 163 | database_id: ダウンロードするデータベースのID 164 | output_dir: 出力先のディレクトリパス 165 | """ 166 | # データベース内のページを取得 167 | pages = self.get_database_pages(database_id) 168 | 169 | # 出力ディレクトリを作成 170 | output_path = Path(output_dir) 171 | output_path.mkdir(parents=True, exist_ok=True) 172 | 173 | # 各ページをダウンロード 174 | for page in pages: 175 | page_id = page["id"] 176 | 177 | # ページタイトルを取得 178 | title = "" 179 | title_property = page["properties"].get("title", {}) 180 | if title_property and "title" in title_property: 181 | title_items = title_property["title"] 182 | if title_items: 183 | title = title_items[0].get("plain_text", "") 184 | 185 | # ファイル名を生成(タイトルがない場合はページIDを使用) 186 | filename = f"{title or page_id}.md" 187 | file_path = output_path / filename 188 | 189 | # ページをダウンロード 190 | self.download_page(page_id, str(file_path)) 191 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.insert(0, os.getcwd()) 5 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.10" 4 | 5 | [[package]] 6 | name = "annotated-types" 7 | version = "0.7.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.9.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 20 | { name = "idna" }, 21 | { name = "sniffio" }, 22 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 23 | ] 24 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } 25 | wheels = [ 26 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, 27 | ] 28 | 29 | [[package]] 30 | name = "certifi" 31 | version = "2025.1.31" 32 | source = { registry = "https://pypi.org/simple" } 33 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 34 | wheels = [ 35 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 36 | ] 37 | 38 | [[package]] 39 | name = "click" 40 | version = "8.1.8" 41 | source = { registry = "https://pypi.org/simple" } 42 | dependencies = [ 43 | { name = "colorama", marker = "sys_platform == 'win32'" }, 44 | ] 45 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 46 | wheels = [ 47 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 48 | ] 49 | 50 | [[package]] 51 | name = "colorama" 52 | version = "0.4.6" 53 | source = { registry = "https://pypi.org/simple" } 54 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 55 | wheels = [ 56 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 57 | ] 58 | 59 | [[package]] 60 | name = "exceptiongroup" 61 | version = "1.2.2" 62 | source = { registry = "https://pypi.org/simple" } 63 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 64 | wheels = [ 65 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 66 | ] 67 | 68 | [[package]] 69 | name = "h11" 70 | version = "0.14.0" 71 | source = { registry = "https://pypi.org/simple" } 72 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 73 | wheels = [ 74 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 75 | ] 76 | 77 | [[package]] 78 | name = "httpcore" 79 | version = "1.0.8" 80 | source = { registry = "https://pypi.org/simple" } 81 | dependencies = [ 82 | { name = "certifi" }, 83 | { name = "h11" }, 84 | ] 85 | sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385 } 86 | wheels = [ 87 | { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 }, 88 | ] 89 | 90 | [[package]] 91 | name = "httpx" 92 | version = "0.28.1" 93 | source = { registry = "https://pypi.org/simple" } 94 | dependencies = [ 95 | { name = "anyio" }, 96 | { name = "certifi" }, 97 | { name = "httpcore" }, 98 | { name = "idna" }, 99 | ] 100 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 101 | wheels = [ 102 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 103 | ] 104 | 105 | [[package]] 106 | name = "httpx-sse" 107 | version = "0.4.0" 108 | source = { registry = "https://pypi.org/simple" } 109 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 110 | wheels = [ 111 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 112 | ] 113 | 114 | [[package]] 115 | name = "idna" 116 | version = "3.10" 117 | source = { registry = "https://pypi.org/simple" } 118 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 119 | wheels = [ 120 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 121 | ] 122 | 123 | [[package]] 124 | name = "iniconfig" 125 | version = "2.1.0" 126 | source = { registry = "https://pypi.org/simple" } 127 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } 128 | wheels = [ 129 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, 130 | ] 131 | 132 | [[package]] 133 | name = "markdown" 134 | version = "3.8" 135 | source = { registry = "https://pypi.org/simple" } 136 | sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906 } 137 | wheels = [ 138 | { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210 }, 139 | ] 140 | 141 | [[package]] 142 | name = "markdown-it-py" 143 | version = "3.0.0" 144 | source = { registry = "https://pypi.org/simple" } 145 | dependencies = [ 146 | { name = "mdurl" }, 147 | ] 148 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 149 | wheels = [ 150 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 151 | ] 152 | 153 | [[package]] 154 | name = "mcp" 155 | version = "1.6.0" 156 | source = { registry = "https://pypi.org/simple" } 157 | dependencies = [ 158 | { name = "anyio" }, 159 | { name = "httpx" }, 160 | { name = "httpx-sse" }, 161 | { name = "pydantic" }, 162 | { name = "pydantic-settings" }, 163 | { name = "sse-starlette" }, 164 | { name = "starlette" }, 165 | { name = "uvicorn" }, 166 | ] 167 | sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 } 168 | wheels = [ 169 | { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 }, 170 | ] 171 | 172 | [package.optional-dependencies] 173 | cli = [ 174 | { name = "python-dotenv" }, 175 | { name = "typer" }, 176 | ] 177 | 178 | [[package]] 179 | name = "mdurl" 180 | version = "0.1.2" 181 | source = { registry = "https://pypi.org/simple" } 182 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 183 | wheels = [ 184 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 185 | ] 186 | 187 | [[package]] 188 | name = "mistune" 189 | version = "3.1.3" 190 | source = { registry = "https://pypi.org/simple" } 191 | dependencies = [ 192 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 193 | ] 194 | sdist = { url = "https://files.pythonhosted.org/packages/c4/79/bda47f7dd7c3c55770478d6d02c9960c430b0cf1773b72366ff89126ea31/mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0", size = 94347 } 195 | wheels = [ 196 | { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410 }, 197 | ] 198 | 199 | [[package]] 200 | name = "notion-client" 201 | version = "2.3.0" 202 | source = { registry = "https://pypi.org/simple" } 203 | dependencies = [ 204 | { name = "httpx" }, 205 | ] 206 | sdist = { url = "https://files.pythonhosted.org/packages/b2/67/c1284de4877496a669ef3a5be36726491dace66261a78a78f73555bffe84/notion-client-2.3.0.tar.gz", hash = "sha256:c4b4ae04ce182eb89611d41544dac710049683a4d7309c4b22fde52f81cbcb39", size = 18790 } 207 | wheels = [ 208 | { url = "https://files.pythonhosted.org/packages/61/ea/03f2fc5d3f5a42397c0ca5a210d5ed605959bc60d7f13d6e5bfa84d31488/notion_client-2.3.0-py2.py3-none-any.whl", hash = "sha256:6696bb057b7872477077d6a3bb4299c4a7924450e7d168174e79cbf8e01d9576", size = 13928 }, 209 | ] 210 | 211 | [[package]] 212 | name = "notion-mcp-light" 213 | version = "0.1.0" 214 | source = { editable = "." } 215 | dependencies = [ 216 | { name = "markdown" }, 217 | { name = "mcp", extra = ["cli"] }, 218 | { name = "mistune" }, 219 | { name = "notion-client" }, 220 | { name = "python-dotenv" }, 221 | ] 222 | 223 | [package.optional-dependencies] 224 | dev = [ 225 | { name = "pytest" }, 226 | ] 227 | 228 | [package.metadata] 229 | requires-dist = [ 230 | { name = "markdown" }, 231 | { name = "mcp", extras = ["cli"] }, 232 | { name = "mistune" }, 233 | { name = "notion-client" }, 234 | { name = "pytest", marker = "extra == 'dev'" }, 235 | { name = "python-dotenv" }, 236 | ] 237 | provides-extras = ["dev"] 238 | 239 | [[package]] 240 | name = "packaging" 241 | version = "24.2" 242 | source = { registry = "https://pypi.org/simple" } 243 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 244 | wheels = [ 245 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 246 | ] 247 | 248 | [[package]] 249 | name = "pluggy" 250 | version = "1.5.0" 251 | source = { registry = "https://pypi.org/simple" } 252 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 253 | wheels = [ 254 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 255 | ] 256 | 257 | [[package]] 258 | name = "pydantic" 259 | version = "2.11.3" 260 | source = { registry = "https://pypi.org/simple" } 261 | dependencies = [ 262 | { name = "annotated-types" }, 263 | { name = "pydantic-core" }, 264 | { name = "typing-extensions" }, 265 | { name = "typing-inspection" }, 266 | ] 267 | sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } 268 | wheels = [ 269 | { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, 270 | ] 271 | 272 | [[package]] 273 | name = "pydantic-core" 274 | version = "2.33.1" 275 | source = { registry = "https://pypi.org/simple" } 276 | dependencies = [ 277 | { name = "typing-extensions" }, 278 | ] 279 | sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } 280 | wheels = [ 281 | { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, 282 | { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, 283 | { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, 284 | { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, 285 | { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, 286 | { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, 287 | { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, 288 | { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, 289 | { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, 290 | { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, 291 | { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, 292 | { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, 293 | { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, 294 | { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, 295 | { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, 296 | { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, 297 | { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, 298 | { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, 299 | { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, 300 | { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, 301 | { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, 302 | { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, 303 | { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, 304 | { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, 305 | { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, 306 | { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, 307 | { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, 308 | { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, 309 | { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, 310 | { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, 311 | { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, 312 | { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, 313 | { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, 314 | { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, 315 | { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, 316 | { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, 317 | { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, 318 | { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, 319 | { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, 320 | { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, 321 | { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, 322 | { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, 323 | { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, 324 | { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, 325 | { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, 326 | { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, 327 | { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, 328 | { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, 329 | { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, 330 | { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, 331 | { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, 332 | { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, 333 | { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, 334 | { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, 335 | { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, 336 | { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, 337 | { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, 338 | { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, 339 | { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, 340 | { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, 341 | { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, 342 | { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, 343 | { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, 344 | { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, 345 | { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, 346 | { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, 347 | { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, 348 | { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, 349 | { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, 350 | { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, 351 | { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, 352 | { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, 353 | { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, 354 | { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, 355 | { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, 356 | { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, 357 | ] 358 | 359 | [[package]] 360 | name = "pydantic-settings" 361 | version = "2.8.1" 362 | source = { registry = "https://pypi.org/simple" } 363 | dependencies = [ 364 | { name = "pydantic" }, 365 | { name = "python-dotenv" }, 366 | ] 367 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } 368 | wheels = [ 369 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, 370 | ] 371 | 372 | [[package]] 373 | name = "pygments" 374 | version = "2.19.1" 375 | source = { registry = "https://pypi.org/simple" } 376 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 377 | wheels = [ 378 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 379 | ] 380 | 381 | [[package]] 382 | name = "pytest" 383 | version = "8.3.5" 384 | source = { registry = "https://pypi.org/simple" } 385 | dependencies = [ 386 | { name = "colorama", marker = "sys_platform == 'win32'" }, 387 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 388 | { name = "iniconfig" }, 389 | { name = "packaging" }, 390 | { name = "pluggy" }, 391 | { name = "tomli", marker = "python_full_version < '3.11'" }, 392 | ] 393 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } 394 | wheels = [ 395 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, 396 | ] 397 | 398 | [[package]] 399 | name = "python-dotenv" 400 | version = "1.1.0" 401 | source = { registry = "https://pypi.org/simple" } 402 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } 403 | wheels = [ 404 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, 405 | ] 406 | 407 | [[package]] 408 | name = "rich" 409 | version = "14.0.0" 410 | source = { registry = "https://pypi.org/simple" } 411 | dependencies = [ 412 | { name = "markdown-it-py" }, 413 | { name = "pygments" }, 414 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 415 | ] 416 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } 417 | wheels = [ 418 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, 419 | ] 420 | 421 | [[package]] 422 | name = "shellingham" 423 | version = "1.5.4" 424 | source = { registry = "https://pypi.org/simple" } 425 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 426 | wheels = [ 427 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 428 | ] 429 | 430 | [[package]] 431 | name = "sniffio" 432 | version = "1.3.1" 433 | source = { registry = "https://pypi.org/simple" } 434 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 435 | wheels = [ 436 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 437 | ] 438 | 439 | [[package]] 440 | name = "sse-starlette" 441 | version = "2.2.1" 442 | source = { registry = "https://pypi.org/simple" } 443 | dependencies = [ 444 | { name = "anyio" }, 445 | { name = "starlette" }, 446 | ] 447 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 448 | wheels = [ 449 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 450 | ] 451 | 452 | [[package]] 453 | name = "starlette" 454 | version = "0.46.2" 455 | source = { registry = "https://pypi.org/simple" } 456 | dependencies = [ 457 | { name = "anyio" }, 458 | ] 459 | sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } 460 | wheels = [ 461 | { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, 462 | ] 463 | 464 | [[package]] 465 | name = "tomli" 466 | version = "2.2.1" 467 | source = { registry = "https://pypi.org/simple" } 468 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 469 | wheels = [ 470 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, 471 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, 472 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, 473 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, 474 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, 475 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, 476 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, 477 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, 478 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 479 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 480 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, 481 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, 482 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, 483 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, 484 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, 485 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, 486 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, 487 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, 488 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 489 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 490 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, 491 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, 492 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, 493 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, 494 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, 495 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, 496 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, 497 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, 498 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 499 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 500 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 501 | ] 502 | 503 | [[package]] 504 | name = "typer" 505 | version = "0.15.2" 506 | source = { registry = "https://pypi.org/simple" } 507 | dependencies = [ 508 | { name = "click" }, 509 | { name = "rich" }, 510 | { name = "shellingham" }, 511 | { name = "typing-extensions" }, 512 | ] 513 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } 514 | wheels = [ 515 | { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, 516 | ] 517 | 518 | [[package]] 519 | name = "typing-extensions" 520 | version = "4.13.2" 521 | source = { registry = "https://pypi.org/simple" } 522 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } 523 | wheels = [ 524 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, 525 | ] 526 | 527 | [[package]] 528 | name = "typing-inspection" 529 | version = "0.4.0" 530 | source = { registry = "https://pypi.org/simple" } 531 | dependencies = [ 532 | { name = "typing-extensions" }, 533 | ] 534 | sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } 535 | wheels = [ 536 | { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, 537 | ] 538 | 539 | [[package]] 540 | name = "uvicorn" 541 | version = "0.34.1" 542 | source = { registry = "https://pypi.org/simple" } 543 | dependencies = [ 544 | { name = "click" }, 545 | { name = "h11" }, 546 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 547 | ] 548 | sdist = { url = "https://files.pythonhosted.org/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc", size = 76755 } 549 | wheels = [ 550 | { url = "https://files.pythonhosted.org/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065", size = 62404 }, 551 | ] 552 | --------------------------------------------------------------------------------