├── .gitignore ├── LICENSE ├── README.md ├── README_ZH.md ├── RELEASE_NOTES.md ├── package-lock.json ├── package.json ├── pyproject.toml ├── src └── hologres_mcp_server │ ├── __init__.py │ ├── __pycache__ │ ├── __init__.cpython-313.pyc │ ├── server.cpython-313.pyc │ ├── settings.cpython-313.pyc │ └── utils.cpython-313.pyc │ ├── server.py │ ├── settings.py │ └── utils.py ├── tests ├── .test_mcp_client_env_example ├── TEST_CASE_README.md ├── requirements.txt ├── test_mcp_client.py └── test_mcp_client_comprehensive.py └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | 12 | # Local configuration files 13 | .DS_Store 14 | 15 | # test case environment variables 16 | .test_mcp_client_env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [中文](README_ZH.md) 2 | 3 | # Hologres MCP Server 4 | 5 | Hologres MCP Server serves as a universal interface between AI Agents and Hologres databases. It enables seamless communication between AI Agents and Hologres, helping AI Agents retrieve Hologres database metadata and execute SQL operations. 6 | 7 | ## Configuration 8 | 9 | ### Mode 1: Using Local File 10 | 11 | #### Download 12 | 13 | Download from Github 14 | 15 | ```bash 16 | git clone https://github.com/aliyun/alibabacloud-hologres-mcp-server.git 17 | ``` 18 | 19 | #### MCP Integration 20 | 21 | Add the following configuration to the MCP client configuration file: 22 | 23 | ```json 24 | { 25 | "mcpServers": { 26 | "hologres-mcp-server": { 27 | "command": "uv", 28 | "args": [ 29 | "--directory", 30 | "/path/to/alibabacloud-hologres-mcp-server", 31 | "run", 32 | "hologres-mcp-server" 33 | ], 34 | "env": { 35 | "HOLOGRES_HOST": "host", 36 | "HOLOGRES_PORT": "port", 37 | "HOLOGRES_USER": "access_id", 38 | "HOLOGRES_PASSWORD": "access_key", 39 | "HOLOGRES_DATABASE": "database" 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | ### Mode 2: Using PIP Mode 47 | 48 | #### Installation 49 | 50 | Install MCP Server using the following package: 51 | 52 | ```bash 53 | pip install hologres-mcp-server 54 | ``` 55 | 56 | #### MCP Integration 57 | 58 | Add the following configuration to the MCP client configuration file: 59 | 60 | Use uv mode 61 | 62 | ```json 63 | { 64 | "mcpServers": { 65 | "hologres-mcp-server": { 66 | "command": "uv", 67 | "args": [ 68 | "run", 69 | "--with", 70 | "hologres-mcp-server", 71 | "hologres-mcp-server" 72 | ], 73 | "env": { 74 | "HOLOGRES_HOST": "host", 75 | "HOLOGRES_PORT": "port", 76 | "HOLOGRES_USER": "access_id", 77 | "HOLOGRES_PASSWORD": "access_key", 78 | "HOLOGRES_DATABASE": "database" 79 | } 80 | } 81 | } 82 | } 83 | ``` 84 | Use uvx mode 85 | 86 | ```json 87 | { 88 | "mcpServers": { 89 | "hologres-mcp-server": { 90 | "command": "uvx", 91 | "args": [ 92 | "hologres-mcp-server" 93 | ], 94 | "env": { 95 | "HOLOGRES_HOST": "host", 96 | "HOLOGRES_PORT": "port", 97 | "HOLOGRES_USER": "access_id", 98 | "HOLOGRES_PASSWORD": "access_key", 99 | "HOLOGRES_DATABASE": "database" 100 | } 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | ## Components 107 | 108 | ### Tools 109 | 110 | * `execute_hg_select_sql`: Execute a SELECT SQL query in Hologres database 111 | * `execute_hg_select_sql_with_serverless`: Execute a SELECT SQL query in Hologres database with serverless computing 112 | * `execute_hg_dml_sql`: Execute a DML (INSERT, UPDATE, DELETE) SQL query in Hologres database 113 | * `execute_hg_ddl_sql`: Execute a DDL (CREATE, ALTER, DROP, COMMENT ON) SQL query in Hologres database 114 | * `gather_hg_table_statistics`: Collect table statistics in Hologres database 115 | * `get_hg_query_plan`: Get query plan in Hologres database 116 | * `get_hg_execution_plan`: Get execution plan in Hologres database 117 | * `call_hg_procedure`: Invoke a procedure in Hologres database 118 | * `create_hg_maxcompute_foreign_table`: Create MaxCompute foreign tables in Hologres database. 119 | 120 | Since some Agents do not support resources and resource templates, the following tools are provided to obtain the metadata of schemas, tables, views, and external tables. 121 | * `list_hg_schemas`: Lists all schemas in the current Hologres database, excluding system schemas. 122 | * `list_hg_tables_in_a_schema`: Lists all tables in a specific schema, including their types (table, view, external table, partitioned table). 123 | * `show_hg_table_ddl`: Show the DDL script of a table, view, or external table in the Hologres database. 124 | 125 | ### Resources 126 | 127 | #### Built-in Resources 128 | 129 | * `hologres:///schemas`: Get all schemas in Hologres database 130 | 131 | #### Resource Templates 132 | 133 | * `hologres:///{schema}/tables`: List all tables in a schema in Hologres database 134 | * `hologres:///{schema}/{table}/partitions`: List all partitions of a partitioned table in Hologres database 135 | * `hologres:///{schema}/{table}/ddl`: Get table DDL in Hologres database 136 | * `hologres:///{schema}/{table}/statistic`: Show collected table statistics in Hologres database 137 | * `system:///{+system_path}`: 138 | System paths include: 139 | 140 | * `hg_instance_version` - Shows the hologres instance version. 141 | * `guc_value/` - Shows the guc (Grand Unified Configuration) value. 142 | * `missing_stats_tables` - Shows the tables that are missing statistics. 143 | * `stat_activity` - Shows the information of current running queries. 144 | * `query_log/latest/` - Get recent query log history with specified number of rows. 145 | * `query_log/user//` - Get query log history for a specific user with row limits. 146 | * `query_log/application//` - Get query log history for a specific application with row limits. 147 | * `query_log/failed//` - Get failed query log history with interval and specified number of rows. 148 | 149 | ### Prompts 150 | 151 | None at this time 152 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | 中文 2 | 3 | # Hologres MCP 服务器 4 | 5 | Hologres MCP 服务器作为 AI 代理与 Hologres 数据库之间的通用接口。它实现了 AI 代理与 Hologres 之间的无缝通信,帮助 AI 代理获取 Hologres 数据库元数据并执行 SQL 操作。 6 | 7 | ## 配置 8 | 9 | ### 模式 1:使用本地文件 10 | 11 | #### 下载 12 | 13 | 从 Github 下载 14 | 15 | ```shell 16 | git clone https://github.com/aliyun/alibabacloud-hologres-mcp-server.git 17 | ``` 18 | 19 | #### MCP 集成 20 | 在 MCP 客户端配置文件中添加以下配置: 21 | 22 | ```json 23 | { 24 | "mcpServers": { 25 | "hologres-mcp-server": { 26 | "command": "uv", 27 | "args": [ 28 | "--directory", 29 | "/path/to/alibabacloud-hologres-mcp-server", 30 | "run", 31 | "hologres-mcp-server" 32 | ], 33 | "env": { 34 | "HOLOGRES_HOST": "host", 35 | "HOLOGRES_PORT": "port", 36 | "HOLOGRES_USER": "access_id", 37 | "HOLOGRES_PASSWORD": "access_key", 38 | "HOLOGRES_DATABASE": "database" 39 | } 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | ### 模式 2:使用 PIP 模式 安装 46 | 使用以下命令安装 MCP 服务器: 47 | 48 | ```bash 49 | pip install hologres-mcp-server 50 | ``` 51 | 52 | #### MCP 集成 53 | 在 MCP 客户端配置文件中添加以下配置: 54 | 55 | 使用 UV 模式 56 | 57 | ```json 58 | { 59 | "mcpServers": { 60 | "hologres-mcp-server": { 61 | "command": "uv", 62 | "args": [ 63 | "run", 64 | "--with", 65 | "hologres-mcp-server", 66 | "hologres-mcp-server" 67 | ], 68 | "env": { 69 | "HOLOGRES_HOST": "host", 70 | "HOLOGRES_PORT": "port", 71 | "HOLOGRES_USER": "access_id", 72 | "HOLOGRES_PASSWORD": "access_key", 73 | "HOLOGRES_DATABASE": "database" 74 | } 75 | } 76 | } 77 | } 78 | ``` 79 | 使用 uvx 模式 80 | ```json 81 | { 82 | "mcpServers": { 83 | "hologres-mcp-server": { 84 | "command": "uvx", 85 | "args": [ 86 | "hologres-mcp-server" 87 | ], 88 | "env": { 89 | "HOLOGRES_HOST": "host", 90 | "HOLOGRES_PORT": "port", 91 | "HOLOGRES_USER": "access_id", 92 | "HOLOGRES_PASSWORD": "access_key", 93 | "HOLOGRES_DATABASE": "database" 94 | } 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | ## 组件 101 | ### 工具 102 | - `execute_hg_select_sql` :在 Hologres 数据库中执行 SELECT SQL 查询 103 | - `execute_hg_select_sql_with_serverless` :在 Hologres 数据库中使用无服务器计算执行 SELECT SQL 查询 104 | - `execute_hg_dml_sql` :在 Hologres 数据库中执行 DML(INSERT、UPDATE、DELETE)SQL 查询 105 | - `execute_hg_ddl_sql` :在 Hologres 数据库中执行 DDL(CREATE、ALTER、DROP、COMMENT ON)SQL 查询 106 | - `gather_hg_table_statistics` :收集 Hologres 数据库中的表统计信息 107 | - `get_hg_query_plan` :获取 Hologres 数据库中的查询计划 108 | - `get_hg_execution_plan` :获取 Hologres 数据库中的执行计划 109 | - `call_hg_procedure` :调用 Hologres 数据库中的存储过程 110 | - `create_hg_maxcompute_foreign_table` :在 Hologres 数据库中创建 MaxCompute 外部表 111 | 112 | 由于某些代理不支持资源和资源模板,提供了以下工具来获取模式、表、视图和外部表的元数据: 113 | 114 | - `list_hg_schemas` :列出当前 Hologres 数据库中的所有模式,不包括系统模式 115 | - `list_hg_tables_in_a_schema` :列出特定模式中的所有表,包括它们的类型(表、视图、外部表、分区表) 116 | - `show_hg_table_ddl` :显示 Hologres 数据库中表、视图或外部表的 DDL 脚本 117 | 118 | ### 资源 内置资源 119 | - `hologres:///schemas` :获取 Hologres 数据库中的所有模式 资源模板 120 | - `hologres:///{schema}/tables` :列出 Hologres 数据库中某个模式下的所有表 121 | - `hologres:///{schema}/{table}/partitions` :列出 Hologres 数据库中分区表的所有分区 122 | - `hologres:///{schema}/{table}/ddl` :获取 Hologres 数据库中的表 DDL 123 | - `hologres:///{schema}/{table}/statistic` :显示 Hologres 数据库中收集的表统计信息 124 | - `system:///{+system_path}` : 125 | 系统路径包括: 126 | 127 | - `hg_instance_version` - 显示 hologres 实例版本 128 | - `guc_value/` - 显示 guc(统一配置)值 129 | - `missing_stats_tables` - 显示缺少统计信息的表 130 | - `stat_activity` - 显示当前运行查询的信息 131 | - `query_log/latest/` - 获取指定行数的最近查询日志历史 132 | - `query_log/user//` - 获取特定用户的查询日志历史,带行数限制 133 | - `query_log/application//` - 获取特定应用程序的查询日志历史,带行数限制 134 | - `query_log/failed//` - 获取失败的查询日志历史,带时间间隔和指定行数 -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | ## Version 0.1.9 3 | ### Bugfix 4 | Fix the configuration issue when the STS token is not defined. 5 | 6 | ## Version 0.1.8 7 | ### Enhancement 8 | Add tools 9 | - `execute_hg_select_sql_with_serverless`: Execute a SELECT SQL query in Hologres database with serverless computing 10 | - `create_hg_maxcompute_foreign_table`: Create MaxCompute foreign tables in Hologres database. 11 | 12 | Since some Agents do not support resources and resource templates, the following tools are provided to obtain the metadata of schemas, tables, views, and external tables. 13 | - `list_hg_schemas`: Lists all schemas in the current Hologres database, excluding system schemas. 14 | - `list_hg_tables_in_a_schema`: Lists all tables in a specific schema, including their types (table, view, external table, partitioned table). 15 | - `show_hg_table_ddl`: Show the DDL script of a table, view, or external table in the Hologres database. 16 | 17 | In order for the AI Agent to better recognize the Tools, please rename the following Tools as follows. 18 | - Rename `execute_select_sql` to `execute_hg_select_sql` 19 | - Rename `execute_dml_sql` to `execute_hg_dml_sql` 20 | - Rename `execute_ddl_sql` to `execute_hg_ddl_sql` 21 | - Rename `gather_table_statistics` to `gather_hg_table_statistics` 22 | - Rename `get_query_plan` to `get_hg_query_plan` 23 | - Rename `get_execution_plan` to `get_hg_execution_plan` 24 | - Rename `call_procedure` to `call_hg_procedure` 25 | 26 | ## Version 0.1.7 27 | ### Bugfix 28 | Fix some bugs when using in Python 3.11. 29 | 30 | ## Version 0.1.6 31 | ### Enhancement 32 | update psycopg2 to psycopg3. 33 | select, dml, ddl use different tools to execute. 34 | 35 | ## Version 0.1.5 36 | ### Enhancement 37 | Now compatible with Python 3.10 and newer (previously required 3.13+). 38 | 39 | ## Version 0.1.4 40 | ### Enhancement 41 | The URI of the resource template has been refactored to enable the large language model (LLM) to use it more concisely. 42 | 43 | ## Version 0.1.2 (Initial Release) 44 | ### Description 45 | Hologres MCP Server serves as a universal interface between AI Agents and Hologres databases. It enables rapid implementation of seamless communication between AI Agents and Hologres, helping AI Agents retrieve Hologres database metadata and execute SQL for various operations. 46 | 47 | ### Key Features 48 | - **SQL Execution** 49 | - Execute SQL in Hologres, including DDL, DML, and Queries 50 | - Execute ANALYZE commands to collect statistics 51 | - **Database Metadata** 52 | - Display all schemas 53 | - Display all tables under a schema 54 | - Show table DDL 55 | - View table statistics 56 | - **System Information** 57 | - Query execution logs 58 | - Query missing statistics 59 | 60 | ### Dependencies 61 | - Python 3.10 or higher 62 | - Required packages 63 | - mcp >= 1.4.0 64 | - psycopg >= 3.1.0 65 | 66 | ### Configuration 67 | MCP Server requires the following environment variables to connect to Hologres instance: 68 | - `HOLOGRES_HOST` 69 | - `HOLOGRES_PORT` 70 | - `HOLOGRES_USER` 71 | - `HOLOGRES_PASSWORD` 72 | - `HOLOGRES_DATABASE` 73 | 74 | ### Installation 75 | Install MCP Server using the following package: 76 | ```bash 77 | pip install hologres-mcp-server 78 | ``` 79 | 80 | ### MCP Integration 81 | Add the following configuration to the MCP client configuration file: 82 | ```json 83 | "mcpServers": { 84 | "hologres-mcp-server": { 85 | "command": "uv", 86 | "args": [ 87 | "run", 88 | "--with", 89 | "hologres-mcp-server", 90 | "hologres-mcp-server" 91 | ], 92 | "env": { 93 | "HOLOGRES_HOST": "host", 94 | "HOLOGRES_PORT": "port", 95 | "HOLOGRES_USER": "access_id", 96 | "HOLOGRES_PASSWORD": "access_key", 97 | "HOLOGRES_DATABASE": "database" 98 | } 99 | } 100 | } 101 | ``` 102 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hologres_mcp_server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "hologres_mcp_server", 9 | "version": "1.0.0", 10 | "license": "ISC" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hologres_mcp_server", 3 | "version": "1.0.0", 4 | "description": "A MCP Server for Hologres", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "hologres-mcp-server" 3 | version = "0.1.9" 4 | description = "A MCP Server for Hologres" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "httpx>=0.28.1", 9 | "mcp>=1.4.1", 10 | "python-dotenv>=1.0.1", 11 | "psycopg>=3.1.0", 12 | "psycopg-binary>=3.1.0", 13 | "pglast>=7.5" 14 | ] 15 | [[project.authors]] 16 | name = "TimothyDing" 17 | email = "ding_ye_timo@163.com" 18 | 19 | [build-system] 20 | requires = [ "hatchling",] 21 | build-backend = "hatchling.build" 22 | 23 | [project.scripts] 24 | hologres-mcp-server = "hologres_mcp_server:main" 25 | -------------------------------------------------------------------------------- /src/hologres_mcp_server/__init__.py: -------------------------------------------------------------------------------- 1 | from . import server 2 | import asyncio 3 | 4 | def main(): 5 | """Main entry point for the package.""" 6 | asyncio.run(server.main()) 7 | 8 | # Optionally expose other important items at package level 9 | __all__ = ['main', 'server'] -------------------------------------------------------------------------------- /src/hologres_mcp_server/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-hologres-mcp-server/547ea3cd773c3c75975c4394342026dd8bc8d70b/src/hologres_mcp_server/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /src/hologres_mcp_server/__pycache__/server.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-hologres-mcp-server/547ea3cd773c3c75975c4394342026dd8bc8d70b/src/hologres_mcp_server/__pycache__/server.cpython-313.pyc -------------------------------------------------------------------------------- /src/hologres_mcp_server/__pycache__/settings.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-hologres-mcp-server/547ea3cd773c3c75975c4394342026dd8bc8d70b/src/hologres_mcp_server/__pycache__/settings.cpython-313.pyc -------------------------------------------------------------------------------- /src/hologres_mcp_server/__pycache__/utils.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/alibabacloud-hologres-mcp-server/547ea3cd773c3c75975c4394342026dd8bc8d70b/src/hologres_mcp_server/__pycache__/utils.cpython-313.pyc -------------------------------------------------------------------------------- /src/hologres_mcp_server/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | import psycopg 5 | import re 6 | from psycopg import OperationalError as Error 7 | from mcp.server import Server 8 | from mcp.types import Resource, Tool, TextContent, ResourceTemplate 9 | from pydantic import AnyUrl 10 | from hologres_mcp_server.utils import try_infer_view_comments, handle_read_resource, handle_call_tool 11 | 12 | """ 13 | # 修改日志配置,只使用文件处理器 14 | logging.basicConfig( 15 | level=logging.INFO, 16 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 17 | handlers=[ 18 | logging.FileHandler('hologres_mcp_log.out') # 只保留文件处理器 19 | ] 20 | ) 21 | logger = logging.getLogger("hologres-mcp-server") 22 | """ 23 | 24 | # Initialize server 25 | app = Server("hologres-mcp-server") 26 | 27 | # 定义 Resources 28 | @app.list_resources() 29 | async def list_resources() -> list[Resource]: 30 | """List basic Hologres resources.""" 31 | return [ 32 | Resource( 33 | uri="hologres:///schemas", 34 | name="All Schemas in Hologres database", 35 | description="Hologres is a PostgreSQL-compatible OLAP product. List all schemas in Hologres database", 36 | mimeType="text/plain" 37 | ) 38 | ] 39 | 40 | HOLO_SYSTEM_DESC = ''' 41 | System information in Hologres database, following are some common system_paths: 42 | 43 | 'hg_instance_version' Shows the hologres instance version. 44 | 'guc_value/' Shows the guc(Grand Unified Configuration) value. 45 | 'missing_stats_tables' Shows the tables that are missing statistics. 46 | 'stat_activity' Shows the information of current running queries. 47 | 'query_log/latest/' Get recent query log history with specified number of rows. 48 | 'query_log/user//' Get query log history for a specific user with row limits. 49 | 'query_log/application//' Get query log history for a specific application with row limits. 50 | 'query_log/failed//' - Get failed query log history with interval and specified number of rows. 51 | ''' 52 | 53 | @app.list_resource_templates() 54 | async def list_resource_templates() -> list[ResourceTemplate]: 55 | """Define resource URI templates for dynamic resources.""" 56 | return [ 57 | ResourceTemplate( 58 | uriTemplate="hologres:///{schema}/tables", 59 | name="List all tables in a specific schema in Hologres database", 60 | description="List all tables in a specific schema in Hologres database", 61 | mimeType="text/plain" 62 | ), 63 | ResourceTemplate( 64 | uriTemplate="hologres:///{schema}/{table}/ddl", 65 | name="Table DDL in Hologres database", 66 | description="Get the DDL script of a table in a specific schema in Hologres database", 67 | mimeType="text/plain" 68 | ), 69 | ResourceTemplate( 70 | uriTemplate="hologres:///{schema}/{table}/statistic", 71 | name="Table Statistics in Hologres database", 72 | description="Get statistics information of a table in Hologres database", 73 | mimeType="text/plain" 74 | ), 75 | ResourceTemplate( 76 | uriTemplate="hologres:///{schema}/{table}/partitions", 77 | name="Table Partitions in Hologres database", 78 | description="List all partitions of a partitioned table in Hologres database", 79 | mimeType="text/plain" 80 | ), 81 | ResourceTemplate( 82 | uriTemplate="system:///{+system_path}", 83 | name="System internal Information in Hologres database", 84 | description=HOLO_SYSTEM_DESC, 85 | mimeType="text/plain" 86 | ) 87 | ] 88 | 89 | @app.read_resource() 90 | async def read_resource(uri: AnyUrl): 91 | """Read resource content based on URI.""" 92 | uri_str = str(uri) 93 | 94 | if not (uri_str.startswith("hologres:///") or uri_str.startswith("system:///")): 95 | raise ValueError(f"Invalid URI scheme: {uri_str}") 96 | 97 | # Handle hologres:/// URIs 98 | if uri_str.startswith("hologres:///"): 99 | path_parts = uri_str[12:].split('/') 100 | 101 | if path_parts[0] == "schemas": 102 | # List all schemas 103 | query = """ 104 | SELECT table_schema 105 | FROM information_schema.tables 106 | WHERE table_schema NOT IN ('pg_catalog', 'information_schema','hologres','hologres_statistic','hologres_streaming_mv') 107 | GROUP BY table_schema 108 | ORDER BY table_schema; 109 | """ 110 | schemas = handle_read_resource("list_schemas", query) 111 | return "\n".join([schema[0] for schema in schemas]) 112 | 113 | elif len(path_parts) == 2 and path_parts[1] == "tables": 114 | # List tables in specific schema 115 | schema = path_parts[0] 116 | query = f""" 117 | SELECT 118 | tab.table_name, 119 | CASE WHEN tab.table_type = 'VIEW' THEN ' (view)' 120 | WHEN tab.table_type = 'FOREIGN' THEN ' (foreign table)' 121 | WHEN p.partrelid IS NOT NULL THEN ' (partitioned table)' 122 | ELSE '' 123 | END AS table_type_info 124 | FROM 125 | information_schema.tables AS tab 126 | LEFT JOIN pg_class AS cls ON tab.table_name = cls.relname 127 | LEFT JOIN pg_namespace AS ns ON tab.table_schema = ns.nspname 128 | LEFT JOIN pg_inherits AS inh ON cls.oid = inh.inhrelid 129 | LEFT JOIN pg_partitioned_table AS p ON cls.oid = p.partrelid 130 | WHERE 131 | tab.table_schema NOT IN ('pg_catalog', 'information_schema', 'hologres', 'hologres_statistic', 'hologres_streaming_mv') 132 | AND tab.table_schema = '{schema}' 133 | AND (inh.inhrelid IS NULL OR NOT EXISTS ( 134 | SELECT 1 135 | FROM pg_inherits 136 | WHERE inh.inhrelid = pg_inherits.inhrelid 137 | )) 138 | ORDER BY 139 | tab.table_name; 140 | """ 141 | tables = handle_read_resource("list_tables_in_schema", query) 142 | # 修复 SyntaxError 问题:f-string中不能包含反斜杠 143 | return "\n".join(['"' + table[0].replace('"', '""') + '"' + table[1] for table in tables]) 144 | 145 | elif len(path_parts) == 3 and path_parts[2] == "partitions": 146 | # Get partitions 147 | schema = path_parts[0] 148 | table = path_parts[1] 149 | query = f""" 150 | with inh as ( 151 | SELECT i.inhrelid, i.inhparent 152 | FROM pg_catalog.pg_class c 153 | LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace 154 | LEFT JOIN pg_catalog.pg_inherits i on c.oid=i.inhparent 155 | where n.nspname='{schema}' and c.relname='{table}' 156 | ) 157 | select 158 | c.relname as table_name 159 | from inh 160 | join pg_catalog.pg_class c on inh.inhrelid = c.oid 161 | join pg_catalog.pg_namespace n on c.relnamespace = n.oid 162 | join pg_partitioned_table p on p.partrelid = inh.inhparent order by table_name; 163 | """ 164 | tables = handle_read_resource("get_table_partitions", query) 165 | return "\n".join([table[0] for table in tables]) 166 | 167 | elif len(path_parts) == 3 and path_parts[2] == "ddl": 168 | # Get table DDL 169 | schema = path_parts[0] 170 | table = path_parts[1] 171 | query = f"SELECT hg_dump_script('\"{schema}\".\"{table}\"')" 172 | ddl = handle_read_resource("list_ddl", query)[0] 173 | 174 | if ddl and ddl[0]: 175 | if "Type: VIEW" in ddl[0]: 176 | # 修复 SyntaxError 问题:使用字符串连接而不是在f-string中使用反斜杠 177 | view_content = ddl[0].replace('\n\nEND;', '') 178 | comments = try_infer_view_comments(schema, table) 179 | return view_content + comments + "\n\nEND;" 180 | else: 181 | return ddl[0] 182 | else: 183 | return f"No DDL found for {schema}.{table}" 184 | 185 | elif len(path_parts) == 3 and path_parts[2] == "statistic": 186 | # Get table statistics 187 | schema = path_parts[0] 188 | table = path_parts[1] 189 | query = f""" 190 | SELECT 191 | schema_name, 192 | table_name, 193 | schema_version, 194 | statistic_version, 195 | total_rows, 196 | analyze_timestamp 197 | FROM hologres_statistic.hg_table_statistic 198 | WHERE schema_name = '{schema}' 199 | AND table_name = '{table}' 200 | ORDER BY analyze_timestamp DESC; 201 | """ 202 | rows = handle_read_resource("get_table_statistics", query) 203 | if not rows: 204 | return f"No statistics found for {schema}.{table}" 205 | 206 | headers = ["Schema", "Table", "Schema Version", "Stats Version", "Total Rows", "Analyze Time"] 207 | result = ["\t".join(headers)] 208 | for row in rows: 209 | result.append("\t".join(map(str, row))) 210 | return "\n".join(result) 211 | 212 | 213 | # Handle system:/// URIs 214 | elif uri_str.startswith("system:///"): 215 | path_parts = uri_str[10:].split('/') 216 | 217 | if path_parts[0] == "hg_instance_version": 218 | # Execute the SQL to get the version of the Hologres instance 219 | query = "SELECT HG_VERSION();" 220 | version = handle_read_resource("get_instance_version", query)[0][0] 221 | # Extract the version number from the full version string 222 | version_number = version.split(' ')[1] 223 | return version_number 224 | 225 | elif path_parts[0] == "missing_stats_tables": 226 | # Shows the tables that are missing statistics. 227 | query = """ 228 | SELECT 229 | * 230 | FROM hologres_statistic.hg_stats_missing 231 | WHERE schemaname NOT IN ('pg_catalog', 'information_schema','hologres','hologres_statistic','hologres_streaming_mv') 232 | ORDER BY schemaname, tablename; 233 | """ 234 | rows, headers = handle_read_resource("get_holo_instance_version", query, with_headers=True) 235 | 236 | if not rows: 237 | return "No tables found with missing statistics" 238 | result = ["\t".join(headers)] 239 | for row in rows: 240 | formatted_row = [str(val) if val is not None else "NULL" for val in row] 241 | result.append("\t".join(formatted_row)) 242 | return "\n".join(result) 243 | 244 | elif path_parts[0] == "stat_activity": 245 | # Shows the information of current running queries. 246 | query = """ 247 | SELECT 248 | * 249 | FROM 250 | hg_stat_activity 251 | ORDER BY pid; 252 | """ 253 | rows, headers = handle_read_resource("get_stat_activity", query, with_headers=True) 254 | if not rows: 255 | return "No queries found with current running status" 256 | result = ["\t".join(headers)] 257 | for row in rows: 258 | formatted_row = [str(val) if val is not None else "NULL" for val in row] 259 | result.append("\t".join(formatted_row)) 260 | return "\n".join(result) 261 | 262 | elif path_parts[0] == "query_log": 263 | rows = None 264 | headers = None 265 | if path_parts[1] == "latest" and len(path_parts) == 3: 266 | try: 267 | row_limits = int(path_parts[2]) 268 | if row_limits <= 0: 269 | return "Row limits must be a positive integer" 270 | query = f"SELECT * FROM hologres.hg_query_log ORDER BY query_start DESC LIMIT {row_limits}" 271 | rows, headers = handle_read_resource("get_latest_query_log", query, with_headers=True) 272 | except ValueError: 273 | return "Invalid row limits format, must be an integer" 274 | 275 | elif path_parts[1] == "user" and len(path_parts) == 4: 276 | user_name = path_parts[2] 277 | if not user_name: 278 | return "Username cannot be empty" 279 | try: 280 | row_limits = int(path_parts[3]) 281 | if row_limits <= 0: 282 | return "Row limits must be a positive integer" 283 | query = f"SELECT * FROM hologres.hg_query_log WHERE usename = '{user_name}' ORDER BY query_start DESC LIMIT {row_limits}" 284 | rows, headers = handle_read_resource("get_user_query_log", query, with_headers=True) 285 | except ValueError: 286 | return "Invalid row limits format, must be an integer" 287 | 288 | elif path_parts[1] == "application" and len(path_parts) == 4: 289 | application_name = path_parts[2] 290 | if not application_name: 291 | return "Application name cannot be empty" 292 | try: 293 | row_limits = int(path_parts[3]) 294 | if row_limits <= 0: 295 | return "Row limits must be a positive integer" 296 | query = f"SELECT * FROM hologres.hg_query_log WHERE application_name = '{application_name}' ORDER BY query_start DESC LIMIT {row_limits}" 297 | rows, headers = handle_read_resource("get_application_query_log", query, with_headers=True) 298 | except ValueError: 299 | return "Invalid row limits format, must be an integer" 300 | 301 | elif path_parts[1] == "failed" and len(path_parts) == 4: 302 | interval = path_parts[2] 303 | if not interval: 304 | return "Interval cannot be empty" 305 | try: 306 | row_limits = int(path_parts[3]) 307 | if row_limits <= 0: 308 | return "Row limits must be a positive integer" 309 | query = f"SELECT * FROM hologres.hg_query_log WHERE status = 'FAILED' AND query_start >= NOW() - INTERVAL '{interval}' ORDER BY query_start DESC LIMIT {row_limits}" 310 | rows, headers = handle_read_resource("get_failed_query_log", query, with_headers=True) 311 | except ValueError: 312 | return "Invalid row limits format, must be an integer" 313 | 314 | else: 315 | raise ValueError(f"Invalid query log URI format: {uri_str}") 316 | 317 | if not rows: 318 | return "No query logs found" 319 | 320 | result = ["\t".join(headers)] 321 | for row in rows: 322 | formatted_row = [str(val) if val is not None else "NULL" for val in row] 323 | result.append("\t".join(formatted_row)) 324 | return "\n".join(result) 325 | 326 | elif path_parts[0] == "guc_value": 327 | if len(path_parts) != 2: 328 | raise ValueError(f"Invalid GUC URI format: {uri_str}") 329 | guc_name = path_parts[1] 330 | if not guc_name: 331 | return "GUC name cannot be empty" 332 | query = f"SHOW {guc_name};" 333 | rows = handle_read_resource("get_guc_value", query) 334 | if not rows: 335 | return f"No GUC found with name {guc_name}" 336 | result = [f"{guc_name}: {rows[0][0]}"] 337 | return "\n".join(result) 338 | 339 | raise ValueError(f"Invalid resource URI format: {uri_str}") 340 | 341 | # 定义 Tools 342 | @app.list_tools() 343 | async def list_tools() -> list[Tool]: 344 | """List available Hologres tools.""" 345 | # logger.info("Listing tools...") 346 | return [ 347 | Tool( 348 | name="execute_hg_select_sql", 349 | description="Execute SELECT SQL to query data from Hologres database.", 350 | inputSchema={ 351 | "type": "object", 352 | "properties": { 353 | "query": { 354 | "type": "string", 355 | "description": "The (SELECT) SQL query to execute in Hologres database." 356 | } 357 | }, 358 | "required": ["query"] 359 | } 360 | ), 361 | # 新增 execute_hg_select_sql_with_serverless 工具 362 | Tool( 363 | name="execute_hg_select_sql_with_serverless", 364 | description="Use Serverless Computing resources to execute SELECT SQL to query data in Hologres database. When the error like \"Total memory used by all existing queries exceeded memory limitation\" occurs during execute_hg_select_sql execution, you can re-execute the SQL with the tool execute_hg_select_sql_with_serverless.", 365 | inputSchema={ 366 | "type": "object", 367 | "properties": { 368 | "query": { 369 | "type": "string", 370 | "description": "The (SELECT) SQL query to execute with serverless computing in Hologres database" 371 | } 372 | }, 373 | "required": ["query"] 374 | } 375 | ), 376 | Tool( 377 | name="execute_hg_dml_sql", 378 | description="Execute (INSERT, UPDATE, DELETE) SQL to insert, update, and delete data in Hologres databse.", 379 | inputSchema={ 380 | "type": "object", 381 | "properties": { 382 | "query": { 383 | "type": "string", 384 | "description": "The DML SQL query to execute in Hologres database" 385 | } 386 | }, 387 | "required": ["query"] 388 | } 389 | ), 390 | Tool( 391 | name="execute_hg_ddl_sql", 392 | description="Execute (CREATE, ALTER, DROP) SQL statements to CREATE, ALTER, or DROP tables, views, procedures, GUCs etc. in Hologres databse.", 393 | inputSchema={ 394 | "type": "object", 395 | "properties": { 396 | "query": { 397 | "type": "string", 398 | "description": "The DDL SQL query to execute in Hologres database" 399 | } 400 | }, 401 | "required": ["query"] 402 | } 403 | ), 404 | Tool( 405 | name="gather_hg_table_statistics", 406 | description="Execute the ANALYZE TABLE command to have Hologres collect table statistics, enabling QO to generate better query plans", 407 | inputSchema={ 408 | "type": "object", 409 | "properties": { 410 | "schema": { 411 | "type": "string", 412 | "description": "Schema name in Hologres database" 413 | }, 414 | "table": { 415 | "type": "string", 416 | "description": "Table name in Hologres database" 417 | } 418 | }, 419 | "required": ["schema", "table"] 420 | } 421 | ), 422 | Tool( 423 | name="get_hg_query_plan", 424 | description="Get query plan for a SQL query in Hologres database", 425 | inputSchema={ 426 | "type": "object", 427 | "properties": { 428 | "query": { 429 | "type": "string", 430 | "description": "The SQL query to analyze in Hologres database" 431 | } 432 | }, 433 | "required": ["query"] 434 | } 435 | ), 436 | Tool( 437 | name="get_hg_execution_plan", 438 | description="Get actual execution plan with runtime statistics for a SQL query in Hologres database", 439 | inputSchema={ 440 | "type": "object", 441 | "properties": { 442 | "query": { 443 | "type": "string", 444 | "description": "The SQL query to analyze in Hologres database" 445 | } 446 | }, 447 | "required": ["query"] 448 | } 449 | ), 450 | Tool( 451 | name="call_hg_procedure", 452 | description="Call a stored procedure in Hologres database.", 453 | inputSchema={ 454 | "type": "object", 455 | "properties": { 456 | "procedure_name": { 457 | "type": "string", 458 | "description": "The name of the stored procedure to call in Hologres database" 459 | }, 460 | "arguments": { 461 | "type": "array", 462 | "items": { 463 | "type": "string" 464 | }, 465 | "description": "The arguments to pass to the stored procedure in Hologres database" 466 | } 467 | }, 468 | "required": ["procedure_name"] 469 | } 470 | ), 471 | Tool( 472 | name="create_hg_maxcompute_foreign_table", 473 | description="Create a MaxCompute foreign table in Hologres database to accelerate queries on MaxCompute data.", 474 | inputSchema={ 475 | "type": "object", 476 | "properties": { 477 | "maxcompute_project": { 478 | "type": "string", 479 | "description": "The MaxCompute project name (required)" 480 | }, 481 | "maxcompute_schema": { 482 | "type": "string", 483 | "default": "default", 484 | "description": "The MaxCompute schema name (optional, default: 'default')" 485 | }, 486 | "maxcompute_tables": { 487 | "type": "array", 488 | "items": { 489 | "type": "string" 490 | }, 491 | "description": "The MaxCompute table names (required)" 492 | }, 493 | "local_schema": { 494 | "type": "string", 495 | "default": "public", 496 | "description": "The local schema name in Hologres (optional, default: 'public')" 497 | } 498 | }, 499 | "required": ["maxcompute_project", "maxcompute_tables"] 500 | } 501 | ), 502 | # 新增 list_hg_schemas 工具 503 | Tool( 504 | name="list_hg_schemas", 505 | description="List all schemas in the current Hologres database, excluding system schemas.", 506 | inputSchema={ 507 | "type": "object", 508 | "properties": {}, 509 | "required": [] 510 | } 511 | ), 512 | # 新增 list_hg_tables_in_a_schema 工具 513 | Tool( 514 | name="list_hg_tables_in_a_schema", 515 | description="List all tables in a specific schema in the current Hologres database, including their types (table, view, foreign table, partitioned table).", 516 | inputSchema={ 517 | "type": "object", 518 | "properties": { 519 | "schema": { 520 | "type": "string", 521 | "description": "Schema name to list tables from in Hologres database" 522 | } 523 | }, 524 | "required": ["schema"] 525 | } 526 | ), 527 | # 新增 show_hg_table_ddl 工具 528 | Tool( 529 | name="show_hg_table_ddl", 530 | description="Show DDL script for a table, view, or foreign table in Hologres database.", 531 | inputSchema={ 532 | "type": "object", 533 | "properties": { 534 | "schema": { 535 | "type": "string", 536 | "description": "Schema name in Hologres database" 537 | }, 538 | "table": { 539 | "type": "string", 540 | "description": "Table name in Hologres database" 541 | } 542 | }, 543 | "required": ["schema", "table"] 544 | } 545 | ) 546 | ] 547 | 548 | @app.call_tool() 549 | async def call_tool(name: str, arguments: dict) -> list[TextContent]: 550 | """Execute SQL commands.""" 551 | serverless = False 552 | if name == "execute_hg_select_sql": 553 | query = arguments.get("query") 554 | if not query: 555 | raise ValueError("Query is required") 556 | if not re.match(r"^\s*WITH\s+.*?SELECT\b", query, re.IGNORECASE) and not re.match(r"^\s*SELECT\b", query, re.IGNORECASE): 557 | raise ValueError("Query must be a SELECT statement or start with WITH followed by a SELECT statement") 558 | elif name == "execute_hg_select_sql_with_serverless": 559 | query = arguments.get("query") 560 | if not query: 561 | raise ValueError("Query is required") 562 | if not query.strip().upper().startswith("SELECT"): 563 | raise ValueError("Query must be a SELECT statement") 564 | # 修改 serverless computing 设置方式 565 | serverless = True 566 | elif name == "execute_hg_dml_sql": 567 | query = arguments.get("query") 568 | if not query: 569 | raise ValueError("Query is required") 570 | if not any(query.strip().upper().startswith(keyword) for keyword in ["INSERT", "UPDATE", "DELETE"]): 571 | raise ValueError("Query must be a DML statement (INSERT, UPDATE, DELETE)") 572 | elif name == "execute_hg_ddl_sql": 573 | query = arguments.get("query") 574 | if not query: 575 | raise ValueError("Query is required") 576 | if not any(query.strip().upper().startswith(keyword) for keyword in ["CREATE", "ALTER", "DROP", "COMMENT ON"]): 577 | raise ValueError("Query must be a DDL statement (CREATE, ALTER, DROP, COMMENT ON)") 578 | elif name == "gather_hg_table_statistics": 579 | schema = arguments.get("schema") 580 | table = arguments.get("table") 581 | if not all([schema, table]): 582 | raise ValueError("Schema and table are required") 583 | query = f"ANALYZE {schema}.{table}" 584 | elif name == "get_hg_query_plan": 585 | query = arguments.get("query") 586 | if not query: 587 | raise ValueError("Query is required") 588 | query = f"EXPLAIN {query}" 589 | elif name == "get_hg_execution_plan": 590 | query = arguments.get("query") 591 | if not query: 592 | raise ValueError("Query is required") 593 | query = f"EXPLAIN ANALYZE {query}" 594 | elif name == "call_hg_procedure": 595 | procedure_name = arguments.get("procedure_name") 596 | arguments_list = arguments.get("arguments") 597 | if not procedure_name: 598 | raise ValueError("Procedure name are required") 599 | query = f"CALL {procedure_name}({', '.join(arguments_list)})" 600 | elif name == "create_hg_maxcompute_foreign_table": 601 | maxcompute_project = arguments.get("maxcompute_project") 602 | maxcompute_schema = arguments.get("maxcompute_schema", "default") 603 | maxcompute_tables = arguments.get("maxcompute_tables") 604 | local_schema = arguments.get("local_schema", "public") 605 | if not all([maxcompute_project, maxcompute_tables]): 606 | raise ValueError("maxcompute_project and maxcompute_tables are required") 607 | maxcompute_table_list = ", ".join(maxcompute_tables) 608 | # 修复 SQL 语句,确保正确拼接项目名称和 schema 609 | query = f""" 610 | IMPORT FOREIGN SCHEMA "{maxcompute_project}#{maxcompute_schema}" 611 | LIMIT TO ({maxcompute_table_list}) 612 | FROM SERVER odps_server 613 | INTO {local_schema}; 614 | """ 615 | # 处理list_hg_schemas工具 616 | elif name == "list_hg_schemas": 617 | query = """ 618 | SELECT table_schema 619 | FROM information_schema.tables 620 | WHERE table_schema NOT IN ('pg_catalog', 'information_schema','hologres','hologres_statistic','hologres_streaming_mv') 621 | GROUP BY table_schema 622 | ORDER BY table_schema; 623 | """ 624 | # 处理list_hg_tables_in_a_schema工具 625 | elif name == "list_hg_tables_in_a_schema": 626 | schema = arguments.get("schema") 627 | if not schema: 628 | raise ValueError("Schema name is required") 629 | query = f""" 630 | SELECT 631 | tab.table_name, 632 | CASE WHEN tab.table_type = 'VIEW' THEN ' (view)' 633 | WHEN tab.table_type = 'FOREIGN' THEN ' (foreign table)' 634 | WHEN p.partrelid IS NOT NULL THEN ' (partitioned table)' 635 | ELSE '' 636 | END AS table_type_info 637 | FROM 638 | information_schema.tables AS tab 639 | LEFT JOIN pg_class AS cls ON tab.table_name = cls.relname 640 | LEFT JOIN pg_namespace AS ns ON tab.table_schema = ns.nspname 641 | LEFT JOIN pg_inherits AS inh ON cls.oid = inh.inhrelid 642 | LEFT JOIN pg_partitioned_table AS p ON cls.oid = p.partrelid 643 | WHERE 644 | tab.table_schema NOT IN ('pg_catalog', 'information_schema', 'hologres', 'hologres_statistic', 'hologres_streaming_mv') 645 | AND tab.table_schema = '{schema}' 646 | AND (inh.inhrelid IS NULL OR NOT EXISTS ( 647 | SELECT 1 648 | FROM pg_inherits 649 | WHERE inh.inhrelid = pg_inherits.inhrelid 650 | )) 651 | ORDER BY 652 | tab.table_name; 653 | """ 654 | elif name == "show_hg_table_ddl": 655 | schema = arguments.get("schema") 656 | table = arguments.get("table") 657 | if not all([schema, table]): 658 | raise ValueError("Schema and table are required") 659 | query = f"SELECT hg_dump_script('\"{schema}\".\"{table}\"')" 660 | else: 661 | raise ValueError(f"Unknown tool: {name}") 662 | 663 | res = handle_call_tool(name, query, serverless) 664 | return [TextContent(type="text", text=f"{str(res)}")] 665 | 666 | async def main(): 667 | """Main entry point to run the MCP server.""" 668 | from mcp.server.stdio import stdio_server 669 | 670 | # logger.info("Starting Hologres MCP server...") 671 | # config = get_db_config() 672 | # logger.info(f"Database config: {config['host']}:{config['port']}/{config['database']} as {config['user']}") 673 | 674 | async with stdio_server() as (read_stream, write_stream): 675 | try: 676 | await app.run( 677 | read_stream, 678 | write_stream, 679 | app.create_initialization_options() 680 | ) 681 | except Exception as e: 682 | # logger.error(f"Server error: {str(e)}", exc_info=True) 683 | raise 684 | 685 | if __name__ == "__main__": 686 | asyncio.run(main()) 687 | -------------------------------------------------------------------------------- /src/hologres_mcp_server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings module for Hologres MCP Server. 3 | """ 4 | 5 | import os 6 | 7 | 8 | SERVER_VERSION = "0.1.9" 9 | 10 | 11 | def get_db_config(): 12 | """Get database configuration from environment variables.""" 13 | user = os.getenv("HOLOGRES_USER") 14 | password = os.getenv("HOLOGRES_PASSWORD") 15 | options = None 16 | if user is None or password is None: 17 | user = os.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID") 18 | password = os.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET") 19 | sts_token = os.getenv("ALIBABA_CLOUD_SECURITY_TOKEN") 20 | if sts_token: 21 | options = f"sts_token={sts_token}" 22 | 23 | config = { 24 | "host": os.getenv("HOLOGRES_HOST", "localhost"), 25 | "port": os.getenv("HOLOGRES_PORT", "5432"), 26 | "user": user, 27 | "password": password, 28 | "options": options, 29 | "dbname": os.getenv("HOLOGRES_DATABASE"), 30 | "application_name": f"hologres-mcp-server-{SERVER_VERSION}" 31 | } 32 | if not all([config["user"], config["password"], config["dbname"]]): 33 | raise ValueError("Missing required database configuration.") 34 | 35 | return config -------------------------------------------------------------------------------- /src/hologres_mcp_server/utils.py: -------------------------------------------------------------------------------- 1 | import psycopg 2 | from psycopg import sql 3 | import pglast 4 | import time 5 | from hologres_mcp_server.settings import get_db_config 6 | 7 | 8 | def connect_with_retry(retries=3): 9 | attempt = 0 10 | err_msg = "" 11 | while attempt <= retries: 12 | try: 13 | config = get_db_config() 14 | conn = psycopg.connect(**config) 15 | conn.autocommit = True 16 | with conn.cursor() as cursor: 17 | cursor.execute("SELECT 1;") 18 | cursor.fetchone() 19 | return conn 20 | except psycopg.Error as e: 21 | err_msg = f"Connection failed: {e}" 22 | attempt += 1 23 | if attempt <= retries: 24 | print(f"Retrying connection (attempt {attempt + 1} of {retries + 1})...") 25 | time.sleep(5) # 等待 2 秒后再次尝试连接 26 | raise psycopg.Error(f"Failed to connect to Hologres database after retrying: {err_msg}") 27 | 28 | 29 | # 处理resource的通用函数 30 | def handle_read_resource(resource_name, query, with_headers = False): 31 | """Handle readResource method.""" 32 | config = get_db_config() 33 | try: 34 | with connect_with_retry() as conn: 35 | with conn.cursor() as cursor: 36 | cursor.execute(query) 37 | rows = cursor.fetchall() 38 | headers = [desc[0] for desc in cursor.description] 39 | if with_headers: 40 | return rows, headers 41 | else: 42 | return rows 43 | except Exception as e: 44 | return f"Error executing query: {str(e)}" 45 | 46 | 47 | # 处理tool的通用函数 48 | def handle_call_tool(tool_name, query, serverless = False): 49 | """Handle callTool method.""" 50 | config = get_db_config() 51 | try: 52 | with connect_with_retry() as conn: 53 | with conn.cursor() as cursor: 54 | 55 | # 特殊处理 serverless computing 查询 56 | if serverless: 57 | cursor.execute("set hg_computing_resource='serverless'") 58 | 59 | # Execute the query 60 | cursor.execute(query) 61 | 62 | # 特殊处理 ANALYZE 命令 63 | if tool_name == "gather_hg_table_statistics": 64 | return f"Successfully {query}" 65 | 66 | # 处理其他有返回结果的查询 67 | if cursor.description: # SELECT query 68 | columns = [desc[0] for desc in cursor.description] 69 | rows = cursor.fetchall() 70 | result = [",".join(map(str, row)) for row in rows] 71 | return "\n".join([",".join(columns)] + result) 72 | elif tool_name == "execute_dml_sql": # Non-SELECT query 73 | row_count = cursor.rowcount 74 | return f"Query executed successfully. {row_count} rows affected." 75 | else: 76 | return "Query executed successfully" 77 | except Exception as e: 78 | return f"Error executing query: {str(e)}" 79 | 80 | def get_view_definition(cursor, schema_name, view_name): 81 | cursor.execute(sql.SQL(""" 82 | SELECT definition 83 | FROM pg_views 84 | WHERE schemaname = %s AND viewname = %s 85 | """), [schema_name, view_name]) 86 | result = cursor.fetchone() 87 | return result[0] if result else None 88 | 89 | def get_column_comment(cursor, schema_name, table_name, column_name): 90 | cursor.execute(sql.SQL(""" 91 | SELECT col_description(att.attrelid, att.attnum) 92 | FROM pg_attribute att 93 | JOIN pg_class cls ON att.attrelid = cls.oid 94 | JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid 95 | WHERE cls.relname = %s AND att.attname = %s AND nsp.nspname = %s 96 | """), [table_name, column_name, schema_name]) 97 | result = cursor.fetchone() 98 | return result[0] if result else None 99 | 100 | def try_infer_view_comments(schema_name, view_name): 101 | try: 102 | config = get_db_config() 103 | with psycopg.connect(**config) as conn: 104 | conn.autocommit = True 105 | with conn.cursor() as cursor: 106 | view_definition = get_view_definition(cursor, schema_name, view_name) 107 | if not view_definition: 108 | print(f"View '{view_name}' not found.") 109 | return "" 110 | comment_statements = [] 111 | parsed = pglast.parser.parse_sql(view_definition) 112 | 113 | for raw_stmt in parsed: 114 | stmt = raw_stmt.stmt 115 | if isinstance(stmt, pglast.ast.SelectStmt): 116 | for target in stmt.targetList: 117 | if isinstance(target, pglast.ast.ResTarget): 118 | if isinstance(target.val, pglast.ast.ColumnRef): 119 | source_table = target.val.fields[0].sval 120 | source_column = target.val.fields[1].sval 121 | target_column = target.name or source_column 122 | column_comment = get_column_comment(cursor, schema_name, source_table, source_column) 123 | if column_comment: 124 | cursor.execute(sql.SQL(""" 125 | SELECT col_description((SELECT oid FROM pg_class WHERE relname = %s AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = %s)), attnum) 126 | FROM pg_attribute 127 | WHERE attname = %s AND attrelid = (SELECT oid FROM pg_class WHERE relname = %s AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = %s)) 128 | """), [view_name, schema_name, target_column, view_name, schema_name]) 129 | view_column_comment = cursor.fetchone() 130 | if not view_column_comment or view_column_comment[0] is None: 131 | statement = f"COMMENT ON COLUMN {schema_name}.{view_name}.{target_column} IS '{column_comment}';" 132 | comment_statements.append(statement) 133 | if comment_statements: 134 | comment_statements.insert(0, "-- Infer view column comments from related tables") 135 | return "\n".join(comment_statements) 136 | 137 | except Exception as e: 138 | return "" -------------------------------------------------------------------------------- /tests/.test_mcp_client_env_example: -------------------------------------------------------------------------------- 1 | # Hologres 数据库连接信息 2 | HOLOGRES_HOST=localhost 3 | HOLOGRES_PORT=5432 4 | HOLOGRES_USER=your_access_id 5 | HOLOGRES_PASSWORD=your_access_key 6 | HOLOGRES_DATABASE=your_database -------------------------------------------------------------------------------- /tests/TEST_CASE_README.md: -------------------------------------------------------------------------------- 1 | # Hologres MCP Server 测试 2 | 3 | 本目录包含用于测试 Hologres MCP Server 的客户端测试脚本。这些测试脚本使用 MCP Python SDK 连接到 Hologres MCP Server,并测试其提供的资源和工具。 4 | 5 | ## 测试脚本 6 | 7 | - `test_mcp_client.py`: 基础测试脚本,测试 MCP Server 的基本功能 8 | - `test_mcp_client_comprehensive.py`: 全面测试脚本,测试 MCP Server 的所有资源和工具 9 | 10 | ## 环境配置 11 | 12 | 测试脚本使用 `.test_mcp_client_env` 文件中的环境变量连接到 Hologres 数据库。在运行测试前,请先配置此文件: 13 | 14 | ```bash 15 | # 复制环境变量文件 16 | cp .test_mcp_client_env.example .test_mcp_client_env 17 | # 编辑环境变量文件 18 | vim .test_mcp_client_env 19 | ``` 20 | 21 | `.test_mcp_client_env` 文件内容示例: 22 | 23 | ``` 24 | HOLOGRES_HOST=your_host 25 | HOLOGRES_PORT=your_port 26 | HOLOGRES_USER=your_user 27 | HOLOGRES_PASSWORD=your_password 28 | HOLOGRES_DATABASE=your_database 29 | ``` 30 | 31 | 请将上述配置替换为您的 Hologres 数据库连接信息。 32 | 33 | ## 安装依赖 34 | 35 | 在运行测试前,请确保已安装所需的依赖: 36 | 37 | ```bash 38 | pip install -r requirements.txt 39 | ``` 40 | 41 | ## 运行测试 42 | 43 | ### 基础测试 44 | 45 | ```bash 46 | python test_mcp_client.py 47 | ``` 48 | 49 | ### 全面测试 50 | 51 | ```bash 52 | python test_mcp_client_comprehensive.py 53 | ``` 54 | 55 | 默认情况下,测试脚本会使用项目中的 `src/hologres_mcp_server/server.py` 作为服务器脚本。您也可以通过命令行参数指定服务器脚本路径: 56 | 57 | ```bash 58 | python test_mcp_client.py /path/to/server.py 59 | ``` 60 | 61 | ## 测试输出 62 | 63 | 测试脚本会输出每个测试步骤的结果,包括: 64 | 65 | - 资源列表 66 | - 资源模板列表 67 | - 资源内容 68 | - 工具列表 69 | - 工具调用结果 70 | 71 | 如果测试过程中出现错误,脚本会输出错误信息,但会继续执行后续测试。 72 | 73 | ## 注意事项 74 | 75 | 测试脚本中包含了一个 `to_serializable()` 辅助函数,用于将 MCP 对象转换为可 JSON 序列化的格式。这是因为 MCP 返回的对象(如 `ListResourcesResult`)默认不是 JSON 可序列化的。该函数会递归地将对象的属性转换为基本数据类型,以便进行 JSON 序列化。 -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | # 测试依赖包 2 | mcp>=0.1.0 3 | python-dotenv>=1.0.0 4 | psycopg>=3.1.12 -------------------------------------------------------------------------------- /tests/test_mcp_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Hologres MCP Client测试脚本 6 | 7 | 该脚本用于测试Hologres MCP Server提供的资源和工具。 8 | 参考知乎文章:https://zhuanlan.zhihu.com/p/21166726702 9 | """ 10 | 11 | import asyncio 12 | import os 13 | import sys 14 | import json 15 | from pathlib import Path 16 | from dotenv import load_dotenv 17 | from mcp import ClientSession, StdioServerParameters 18 | from mcp.client.stdio import stdio_client 19 | import inspect 20 | from typing import Any, Dict, List, Union 21 | 22 | # 加载环境变量 23 | env_path = Path(__file__).parent / ".test_mcp_client_env" 24 | load_dotenv(dotenv_path=env_path) 25 | 26 | # 设置服务器连接参数 27 | def get_server_params(): 28 | """ 29 | 获取服务器连接参数 30 | 31 | 如果命令行参数提供了服务器脚本路径,则使用命令行参数 32 | 否则使用默认路径 33 | """ 34 | if len(sys.argv) > 1: 35 | server_script = sys.argv[1] 36 | else: 37 | # 默认路径,指向项目中的server.py 38 | server_script = str(Path(__file__).parent.parent / "src" / "hologres_mcp_server" / "server.py") 39 | 40 | # 获取Hologres数据库连接环境变量 41 | hologres_env = { 42 | "HOLOGRES_HOST": os.getenv("HOLOGRES_HOST"), 43 | "HOLOGRES_PORT": os.getenv("HOLOGRES_PORT"), 44 | "HOLOGRES_USER": os.getenv("HOLOGRES_USER"), 45 | "HOLOGRES_PASSWORD": os.getenv("HOLOGRES_PASSWORD"), 46 | "HOLOGRES_DATABASE": os.getenv("HOLOGRES_DATABASE") 47 | } 48 | 49 | # 确保所有必需的环境变量都已设置 50 | if not all(hologres_env.values()): 51 | print("错误:缺少必要的数据库配置环境变量,请检查.test_mcp_client_env文件") 52 | sys.exit(1) 53 | 54 | return StdioServerParameters( 55 | command="python", # 运行命令 56 | args=[server_script], # 服务器脚本路径 57 | env=hologres_env # 传递Hologres数据库连接环境变量 58 | ) 59 | 60 | def to_serializable(obj: Any) -> Union[Dict, List, str, int, float, bool, None]: 61 | """ 62 | 将MCP对象转换为可JSON序列化的格式 63 | """ 64 | if obj is None: 65 | return None 66 | elif isinstance(obj, (str, int, float, bool)): 67 | return obj 68 | elif isinstance(obj, (list, tuple)): 69 | return [to_serializable(item) for item in obj] 70 | elif isinstance(obj, dict): 71 | return {k: to_serializable(v) for k, v in obj.items()} 72 | elif hasattr(obj, '__dict__'): 73 | # 对于自定义对象,转换其属性为字典 74 | result = {} 75 | for key, value in obj.__dict__.items(): 76 | # 跳过私有属性 77 | if not key.startswith('_'): 78 | result[key] = to_serializable(value) 79 | return result 80 | elif hasattr(obj, '_asdict'): # 处理namedtuple 81 | return to_serializable(obj._asdict()) 82 | else: 83 | # 尝试获取对象的所有公共属性 84 | result = {} 85 | for key in dir(obj): 86 | if not key.startswith('_') and not inspect.ismethod(getattr(obj, key)): 87 | try: 88 | value = getattr(obj, key) 89 | result[key] = to_serializable(value) 90 | except Exception: 91 | pass # 忽略无法序列化的属性 92 | return result if result else str(obj) 93 | 94 | async def test_list_resources(session): 95 | """ 96 | 测试列出资源 97 | """ 98 | print("\n===== 测试列出资源 =====") 99 | resources = await session.list_resources() 100 | serializable_resources = to_serializable(resources) 101 | print(f"资源列表: {json.dumps(serializable_resources, indent=2, ensure_ascii=False)}") 102 | return resources 103 | 104 | async def test_list_resource_templates(session): 105 | """ 106 | 测试列出资源模板 107 | """ 108 | print("\n===== 测试列出资源模板 =====") 109 | templates = await session.list_resource_templates() 110 | serializable_templates = to_serializable(templates) 111 | print(f"资源模板列表: {json.dumps(serializable_templates, indent=2, ensure_ascii=False)}") 112 | return templates 113 | 114 | async def test_read_resource(session, uri): 115 | """ 116 | 测试读取资源 117 | """ 118 | print(f"\n===== 测试读取资源: {uri} =====") 119 | try: 120 | content = await session.read_resource(uri) 121 | print(f"资源内容:\n{content}") 122 | return content 123 | except Exception as e: 124 | print(f"读取资源失败: {e}") 125 | return None 126 | 127 | async def test_list_tools(session): 128 | """ 129 | 测试列出工具 130 | """ 131 | print("\n===== 测试列出工具 =====") 132 | tools = await session.list_tools() 133 | serializable_tools = to_serializable(tools) 134 | print(f"工具列表: {json.dumps(serializable_tools, indent=2, ensure_ascii=False)}") 135 | return tools 136 | 137 | async def test_call_tool(session, tool_name, args): 138 | """ 139 | 测试调用工具 140 | """ 141 | print(f"\n===== 测试调用工具: {tool_name} =====") 142 | print(f"参数: {json.dumps(args, indent=2, ensure_ascii=False)}") 143 | try: 144 | result = await session.call_tool(tool_name, args) 145 | serializable_result = to_serializable(result) 146 | print(f"调用结果:\n{json.dumps(serializable_result, indent=2, ensure_ascii=False) if isinstance(serializable_result, (dict, list)) else serializable_result}") 147 | return result 148 | except Exception as e: 149 | print(f"调用工具失败: {e}") 150 | return None 151 | 152 | async def run_tests(): 153 | """ 154 | 运行所有测试 155 | """ 156 | server_params = get_server_params() 157 | 158 | print("开始测试 Hologres MCP Client...") 159 | print(f"服务器脚本路径: {server_params.args[0]}") 160 | 161 | # 建立连接 162 | async with stdio_client(server_params) as (read, write): 163 | async with ClientSession(read, write) as session: 164 | # 初始化连接 165 | await session.initialize() 166 | print("成功连接到 MCP 服务器") 167 | 168 | # 测试列出资源 169 | resources = await test_list_resources(session) 170 | 171 | # 测试列出资源模板 172 | templates = await test_list_resource_templates(session) 173 | 174 | # 测试读取资源 - 列出所有schema 175 | await test_read_resource(session, "hologres:///schemas") 176 | 177 | # 测试读取系统信息 178 | await test_read_resource(session, "system:///hg_instance_version") 179 | 180 | # 测试列出工具 181 | tools = await test_list_tools(session) 182 | 183 | # 测试调用工具 - 列出所有schema 184 | await test_call_tool(session, "list_hg_schemas", {}) 185 | 186 | # 测试调用工具 - 执行SELECT查询 187 | await test_call_tool(session, "execute_hg_select_sql", { 188 | "query": "SELECT current_database() AS current_db, current_user AS current_user;" 189 | }) 190 | 191 | # 测试调用工具 - 获取查询计划 192 | await test_call_tool(session, "get_hg_query_plan", { 193 | "query": "SELECT * FROM information_schema.tables LIMIT 5;" 194 | }) 195 | 196 | print("\n===== 所有测试完成 =====") 197 | 198 | def main(): 199 | """ 200 | 主函数 201 | """ 202 | asyncio.run(run_tests()) 203 | 204 | if __name__ == "__main__": 205 | main() -------------------------------------------------------------------------------- /tests/test_mcp_client_comprehensive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Hologres MCP Client全面测试脚本 6 | 7 | 该脚本提供了对Hologres MCP Server所有资源和工具的全面测试。 8 | 参考知乎文章:https://zhuanlan.zhihu.com/p/21166726702 9 | """ 10 | 11 | import asyncio 12 | import os 13 | import sys 14 | import json 15 | from pathlib import Path 16 | from dotenv import load_dotenv 17 | from mcp import ClientSession, StdioServerParameters 18 | from mcp.client.stdio import stdio_client 19 | 20 | # 加载环境变量 21 | env_path = Path(__file__).parent / ".test_mcp_client_env" 22 | load_dotenv(dotenv_path=env_path) 23 | 24 | # 设置服务器连接参数 25 | def get_server_params(): 26 | """ 27 | 获取服务器连接参数 28 | 29 | 如果命令行参数提供了服务器脚本路径,则使用命令行参数 30 | 否则使用默认路径 31 | """ 32 | if len(sys.argv) > 1: 33 | server_script = sys.argv[1] 34 | else: 35 | # 默认路径,指向项目中的server.py 36 | server_script = str(Path(__file__).parent.parent / "src" / "hologres_mcp_server" / "server.py") 37 | 38 | # 获取Hologres数据库连接环境变量 39 | hologres_env = { 40 | "HOLOGRES_HOST": os.getenv("HOLOGRES_HOST"), 41 | "HOLOGRES_PORT": os.getenv("HOLOGRES_PORT"), 42 | "HOLOGRES_USER": os.getenv("HOLOGRES_USER"), 43 | "HOLOGRES_PASSWORD": os.getenv("HOLOGRES_PASSWORD"), 44 | "HOLOGRES_DATABASE": os.getenv("HOLOGRES_DATABASE") 45 | } 46 | 47 | # 确保所有必需的环境变量都已设置 48 | if not all(hologres_env.values()): 49 | print("错误:缺少必要的数据库配置环境变量,请检查.test_mcp_client_env文件") 50 | sys.exit(1) 51 | 52 | return StdioServerParameters( 53 | command="python", # 运行命令 54 | args=[server_script], # 服务器脚本路径 55 | env=hologres_env # 传递Hologres数据库连接环境变量 56 | ) 57 | 58 | async def test_list_resources(session): 59 | """ 60 | 测试列出资源 61 | """ 62 | print("\n===== 测试列出资源 =====") 63 | resources = await session.list_resources() 64 | print(f"资源列表: {json.dumps(resources, indent=2, ensure_ascii=False)}") 65 | return resources 66 | 67 | async def test_list_resource_templates(session): 68 | """ 69 | 测试列出资源模板 70 | """ 71 | print("\n===== 测试列出资源模板 =====") 72 | templates = await session.list_resource_templates() 73 | print(f"资源模板列表: {json.dumps(templates, indent=2, ensure_ascii=False)}") 74 | return templates 75 | 76 | async def test_read_resource(session, uri): 77 | """ 78 | 测试读取资源 79 | """ 80 | print(f"\n===== 测试读取资源: {uri} =====") 81 | try: 82 | content = await session.read_resource(uri) 83 | print(f"资源内容:\n{content}") 84 | return content 85 | except Exception as e: 86 | print(f"读取资源失败: {e}") 87 | return None 88 | 89 | async def test_list_tools(session): 90 | """ 91 | 测试列出工具 92 | """ 93 | print("\n===== 测试列出工具 =====") 94 | tools = await session.list_tools() 95 | print(f"工具列表: {json.dumps(tools, indent=2, ensure_ascii=False)}") 96 | return tools 97 | 98 | async def test_call_tool(session, tool_name, args): 99 | """ 100 | 测试调用工具 101 | """ 102 | print(f"\n===== 测试调用工具: {tool_name} =====") 103 | print(f"参数: {json.dumps(args, indent=2, ensure_ascii=False)}") 104 | try: 105 | result = await session.call_tool(tool_name, args) 106 | print(f"调用结果:\n{result}") 107 | return result 108 | except Exception as e: 109 | print(f"调用工具失败: {e}") 110 | return None 111 | 112 | async def run_tests(): 113 | """ 114 | 运行所有测试 115 | """ 116 | server_params = get_server_params() 117 | 118 | print("开始测试 Hologres MCP Client...") 119 | print(f"服务器脚本路径: {server_params.args[0]}") 120 | 121 | # 建立连接 122 | async with stdio_client(server_params) as (read, write): 123 | async with ClientSession(read, write) as session: 124 | # 初始化连接 125 | await session.initialize() 126 | print("成功连接到 MCP 服务器") 127 | 128 | # 测试列出资源 129 | resources = await test_list_resources(session) 130 | 131 | # 测试列出资源模板 132 | templates = await test_list_resource_templates(session) 133 | 134 | # 测试读取资源 - 列出所有schema 135 | schemas_content = await test_read_resource(session, "hologres:///schemas") 136 | 137 | # 如果有schema,则测试读取该schema下的表 138 | if schemas_content: 139 | schemas = schemas_content.strip().split('\n') 140 | if schemas: 141 | schema = schemas[0] 142 | # 测试读取schema下的表 143 | await test_read_resource(session, f"hologres:///{schema}/tables") 144 | 145 | # 测试调用工具 - 列出schema下的表 146 | tables_result = await test_call_tool(session, "list_hg_tables_in_a_schema", { 147 | "schema": schema 148 | }) 149 | 150 | # 如果有表,则测试读取表的DDL和统计信息 151 | if tables_result: 152 | tables_lines = tables_result.strip().split('\n') 153 | if len(tables_lines) > 1: # 跳过标题行 154 | # 提取第一个表名(去除可能的表类型信息) 155 | table_info = tables_lines[1].split(',') 156 | if table_info: 157 | table = table_info[0].strip('"') 158 | # 测试读取表的DDL 159 | await test_read_resource(session, f"hologres:///{schema}/{table}/ddl") 160 | 161 | # 测试读取表的统计信息 162 | await test_read_resource(session, f"hologres:///{schema}/{table}/statistic") 163 | 164 | # 测试读取表的分区信息 165 | await test_read_resource(session, f"hologres:///{schema}/{table}/partitions") 166 | 167 | # 测试调用工具 - 获取表的DDL 168 | await test_call_tool(session, "show_hg_table_ddl", { 169 | "schema": schema, 170 | "table": table 171 | }) 172 | 173 | # 测试读取系统信息 174 | await test_read_resource(session, "system:///hg_instance_version") 175 | await test_read_resource(session, "system:///missing_stats_tables") 176 | await test_read_resource(session, "system:///stat_activity") 177 | await test_read_resource(session, "system:///query_log/latest/5") 178 | await test_read_resource(session, "system:///guc_value/hg_version") 179 | 180 | # 测试列出工具 181 | tools = await test_list_tools(session) 182 | 183 | # 测试调用工具 - 列出所有schema 184 | await test_call_tool(session, "list_hg_schemas", {}) 185 | 186 | # 测试调用工具 - 执行SELECT查询 187 | await test_call_tool(session, "execute_hg_select_sql", { 188 | "query": "SELECT current_database() AS current_db, current_user AS current_user;" 189 | }) 190 | 191 | # 测试调用工具 - 使用Serverless执行SELECT查询 192 | await test_call_tool(session, "execute_hg_select_sql_with_serverless", { 193 | "query": "SELECT current_database() AS current_db, current_user AS current_user;" 194 | }) 195 | 196 | # 测试调用工具 - 获取查询计划 197 | await test_call_tool(session, "get_hg_query_plan", { 198 | "query": "SELECT * FROM information_schema.tables LIMIT 5;" 199 | }) 200 | 201 | # 测试调用工具 - 获取执行计划 202 | await test_call_tool(session, "get_hg_execution_plan", { 203 | "query": "SELECT * FROM information_schema.tables LIMIT 5;" 204 | }) 205 | 206 | print("\n===== 所有测试完成 =====") 207 | 208 | def main(): 209 | """ 210 | 主函数 211 | """ 212 | asyncio.run(run_tests()) 213 | 214 | if __name__ == "__main__": 215 | main() -------------------------------------------------------------------------------- /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 = "hologres-mcp-server" 79 | version = "0.1.9" 80 | source = { editable = "." } 81 | dependencies = [ 82 | { name = "httpx" }, 83 | { name = "mcp" }, 84 | { name = "pglast" }, 85 | { name = "psycopg" }, 86 | { name = "psycopg-binary" }, 87 | { name = "python-dotenv" }, 88 | ] 89 | 90 | [package.metadata] 91 | requires-dist = [ 92 | { name = "httpx", specifier = ">=0.28.1" }, 93 | { name = "mcp", specifier = ">=1.4.1" }, 94 | { name = "pglast", specifier = ">=7.5" }, 95 | { name = "psycopg", specifier = ">=3.1.0" }, 96 | { name = "psycopg-binary", specifier = ">=3.1.0" }, 97 | { name = "python-dotenv", specifier = ">=1.0.1" }, 98 | ] 99 | 100 | [[package]] 101 | name = "httpcore" 102 | version = "1.0.7" 103 | source = { registry = "https://pypi.org/simple" } 104 | dependencies = [ 105 | { name = "certifi" }, 106 | { name = "h11" }, 107 | ] 108 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 109 | wheels = [ 110 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 111 | ] 112 | 113 | [[package]] 114 | name = "httpx" 115 | version = "0.28.1" 116 | source = { registry = "https://pypi.org/simple" } 117 | dependencies = [ 118 | { name = "anyio" }, 119 | { name = "certifi" }, 120 | { name = "httpcore" }, 121 | { name = "idna" }, 122 | ] 123 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 124 | wheels = [ 125 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 126 | ] 127 | 128 | [[package]] 129 | name = "httpx-sse" 130 | version = "0.4.0" 131 | source = { registry = "https://pypi.org/simple" } 132 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 133 | wheels = [ 134 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 135 | ] 136 | 137 | [[package]] 138 | name = "idna" 139 | version = "3.10" 140 | source = { registry = "https://pypi.org/simple" } 141 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 142 | wheels = [ 143 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 144 | ] 145 | 146 | [[package]] 147 | name = "mcp" 148 | version = "1.4.1" 149 | source = { registry = "https://pypi.org/simple" } 150 | dependencies = [ 151 | { name = "anyio" }, 152 | { name = "httpx" }, 153 | { name = "httpx-sse" }, 154 | { name = "pydantic" }, 155 | { name = "pydantic-settings" }, 156 | { name = "sse-starlette" }, 157 | { name = "starlette" }, 158 | { name = "uvicorn" }, 159 | ] 160 | sdist = { url = "https://files.pythonhosted.org/packages/50/cc/5c5bb19f1a0f8f89a95e25cb608b0b07009e81fd4b031e519335404e1422/mcp-1.4.1.tar.gz", hash = "sha256:b9655d2de6313f9d55a7d1df62b3c3fe27a530100cc85bf23729145b0dba4c7a", size = 154942 } 161 | wheels = [ 162 | { url = "https://files.pythonhosted.org/packages/e8/0e/885f156ade60108e67bf044fada5269da68e29d758a10b0c513f4d85dd76/mcp-1.4.1-py3-none-any.whl", hash = "sha256:a7716b1ec1c054e76f49806f7d96113b99fc1166fc9244c2c6f19867cb75b593", size = 72448 }, 163 | ] 164 | 165 | [[package]] 166 | name = "pglast" 167 | version = "7.5" 168 | source = { registry = "https://pypi.org/simple" } 169 | dependencies = [ 170 | { name = "setuptools" }, 171 | ] 172 | sdist = { url = "https://files.pythonhosted.org/packages/78/10/65a1997e0afa801fcc09750024db710509adc0a4281f83463104599c65d2/pglast-7.5.tar.gz", hash = "sha256:a585cc8715b8929338ca1cd0cdb74098ac4be6dc5ba5bbfcc725e42fabfd2a64", size = 3368642 } 173 | wheels = [ 174 | { url = "https://files.pythonhosted.org/packages/03/0d/41d028d52f911bcbc8adead7b868fa0163f1b603be11ea420acccf9fd15f/pglast-7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb96bab5fde16a4d5fb720f86fe37380b491c4a5a08af456f34d14819ce2a745", size = 1145040 }, 175 | { url = "https://files.pythonhosted.org/packages/77/05/0d81d89564b4f06f7aa418e1667e89ffc19f081c22d7b7cda690a3a84e8e/pglast-7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:265d1d103be86b65388ae3857be9a1404efee8c104ba4a35f319c2409359becc", size = 1080620 }, 176 | { url = "https://files.pythonhosted.org/packages/19/d5/3444ad9208770f851a181f5a551c75299e53c61624df49463fd502f8a2d8/pglast-7.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a917c5b5956537de6760bec21dce234ac62068ab6ed6f3957a6fe2019e724db", size = 5433103 }, 177 | { url = "https://files.pythonhosted.org/packages/cd/35/2553c64bb399865a094f3487e6f0bc0977f3c5dac77f21936b94b6d0a4ee/pglast-7.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd41e23f347376006b2aae7ee40144d115b36a371b04c7468de71b89149b95a1", size = 5476295 }, 178 | { url = "https://files.pythonhosted.org/packages/f2/ee/cfd1cab011a1aed458f9a54aec2322e51e2f228dd7023a6c80642ea669bf/pglast-7.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e614b8d7abd4bc09c4a93ac41082c8e09c29c48d9b7f989854dfec4a8f649f8d", size = 5315139 }, 179 | { url = "https://files.pythonhosted.org/packages/8b/d8/0017bc0e7453e42a418b690aa0623435e32de9f7f86ec058c46e6412eadd/pglast-7.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:64f3810db3bd878acf18fb462a4cfec687a103f983bc22027d8d59dedc27b5fa", size = 5231999 }, 180 | { url = "https://files.pythonhosted.org/packages/c8/ae/380ab5eda98553e3a4d9a5fedd871907fdfad49415ebbc83ab35092811ff/pglast-7.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7bd22fbd2e90d46d9beddb123183f1bb0b6bdddc7cc7911bd3fbb1a2efa3edac", size = 5246303 }, 181 | { url = "https://files.pythonhosted.org/packages/7e/ec/d82e50bcdcb21e652bf7dd695754f3b80cae1f66d2fb18f9be0af060e746/pglast-7.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f474b13637f8307d9ff8a02a9c4e41c2ab9ee446ef9b0342e7071e7d3cb65b99", size = 5371587 }, 182 | { url = "https://files.pythonhosted.org/packages/d8/07/018ecb161dbc35dd1eb5792cc135947010a6030b40b18187fb305d03b93c/pglast-7.5-cp310-cp310-win32.whl", hash = "sha256:7234d365cd4c727e39a5d616f3e91a6aa5ac9e75901b9f18e65e9e936123455c", size = 1016896 }, 183 | { url = "https://files.pythonhosted.org/packages/93/f5/4038e2b4f3879867dddf9250b27f143eaeadff2d4e096cb12ecf48855624/pglast-7.5-cp310-cp310-win_amd64.whl", hash = "sha256:de56c2540ee7c46f0d813860d0c75b53a0e8dabcfc4e95fbdb89f60c5bae02fd", size = 1078965 }, 184 | { url = "https://files.pythonhosted.org/packages/c5/e5/d5e34a7cb635dd95aac887fd12b012905fc57c5330661e4be4749dce7763/pglast-7.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dee2ea76b80bb4b93b7ebbd31fc4f213adef5aa917aa7c66df84df0255129aa0", size = 1145500 }, 185 | { url = "https://files.pythonhosted.org/packages/d6/0b/02858512540519eae61ea4d0c7997ac8f75c22f40a9288016e687484a0c9/pglast-7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:73781f982de692e1f2a88df2284bc6763b1568106e145d767922813e7216c673", size = 1080853 }, 186 | { url = "https://files.pythonhosted.org/packages/11/84/a2aa1b470ba65d9c36e63d5bed625f776755fe43d5443124cb57190acc75/pglast-7.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddfb33a45445ba46c4d4bf7126e5366d6da21b8dd93633e16968f7f9e5bbfe5c", size = 5497712 }, 187 | { url = "https://files.pythonhosted.org/packages/90/bd/777e06a925a68815397e4175275b0770e87a183d3e17be897cdf6332f7eb/pglast-7.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5ad49ef6e66a73dc4e3b399e6034256b7eb6219734e5eb1b85f1da5cf95349", size = 5536073 }, 188 | { url = "https://files.pythonhosted.org/packages/ee/77/400a67bae53a129aa13ef86150f28ecf668478b796789b6ca7f891f9b20c/pglast-7.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e480bb3eaefd67bd566b11b9633c09ebeaeb2b4ef283eb388267f8040b597838", size = 5390706 }, 189 | { url = "https://files.pythonhosted.org/packages/86/bb/310bc702b999798b9f529f8999af695c4afb62cb44d970ac35e11e5dee4e/pglast-7.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a68874befb07f37b5e97a68bd07b1ea648ebdf20ca9bbc48e5d1ab50f673399", size = 5290425 }, 190 | { url = "https://files.pythonhosted.org/packages/50/0a/4b5ba70fb7b2f390448fbc96a53c6c980ec79470337cde49db289cbc026d/pglast-7.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f63b141389ca1b9cc5dedccfe8636eb0ca35e08004566fa773d6ea791d29ec1b", size = 5277300 }, 191 | { url = "https://files.pythonhosted.org/packages/c9/4f/2baf5228d14744e3e823f1a7663856224b120f58564d8e97aed169372323/pglast-7.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:48666ccaf9d2061ac3fcc0e8d0ca5c2e2e0b9985fd00ca0b151d96419da9ba84", size = 5415760 }, 192 | { url = "https://files.pythonhosted.org/packages/b5/7e/75b084ca8d2d875e279e34f111d9eee2432996766e83e10440e89d977cc7/pglast-7.5-cp311-cp311-win32.whl", hash = "sha256:d0fe328b36d9fbb8307c45537404227c0b2be366282121d0808fd2fb5a2a93db", size = 1016999 }, 193 | { url = "https://files.pythonhosted.org/packages/01/a9/4a86d7ee1837acf9fa4f74650a27d43a411b11ef8cdf23478cdc9ce49638/pglast-7.5-cp311-cp311-win_amd64.whl", hash = "sha256:83487be6e3b935191e59a2819ea0cc433aff1ac06d47c9a5f7e8c6641590dc16", size = 1079499 }, 194 | { url = "https://files.pythonhosted.org/packages/63/9d/0c45a032adc5c207b481817088d0b2f099202a12dc3c48b06948d5ac970e/pglast-7.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:500023c41d87a1add91cfc09b5860ac922b65773610f65d4bc9d2a7a8694b9bc", size = 1147246 }, 195 | { url = "https://files.pythonhosted.org/packages/58/9a/87e50f9e93ea1f0193aab7846fb362f993831b805cd75b5300cf5faf9528/pglast-7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fe6b32221577a846a844d167954c1a1fe18bfa1b09a525b6e56d3d530ee69197", size = 1083850 }, 196 | { url = "https://files.pythonhosted.org/packages/0d/3e/c11ff677f91b68ffb36d4ce709ae471adaa4fd6a503d1b0f96ff5d9d4085/pglast-7.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98b55c88fd15aa8dc6b8faf28bdcbb382a644b46ee3a3b57625580fc64b28102", size = 5541038 }, 197 | { url = "https://files.pythonhosted.org/packages/10/3e/b55c860098d7ea2bd760d7a66f7bc6815ae9db9ff5a8cf77f175563928f1/pglast-7.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0bab6a880f13588882c4aef0917ee1cd8f32abc8309845199d392f409c7a238", size = 5641195 }, 198 | { url = "https://files.pythonhosted.org/packages/65/41/fdeebaa74b72686843ab717b4a61faed62956141ea4f23621e3c46629671/pglast-7.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5482ca58e3a1311a09e067d3f116f32219d8cf0d690b41a1f3dc232fa2b1c17a", size = 5411410 }, 199 | { url = "https://files.pythonhosted.org/packages/95/4c/2c167fdf4b95a40a7ab19207cdcee4381d0848f2e5366577e9ea08dda0d9/pglast-7.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b2a4694422d9021ac747990d5d9199b036fb18eba249ad1f2a6acc2104cdfad0", size = 5298493 }, 200 | { url = "https://files.pythonhosted.org/packages/1e/89/4690376622f9a3a63443e4968d62a521e6d4b0fc76fbd2a181258a860b45/pglast-7.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ac8b2b75b27145dc0f1f1775f4cac57248465d0a6d256ea167dda3ec5d2818d5", size = 5254300 }, 201 | { url = "https://files.pythonhosted.org/packages/a9/90/108f4db095eb692ae081bb2eeb4293e190b3663d0a8a4217222303ed26e7/pglast-7.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:545c812603947dd2eb1803792b2a2a01fd1d4f5d3fba39a7bec51d30c9f7378b", size = 5433501 }, 202 | { url = "https://files.pythonhosted.org/packages/c8/5f/2e56612bbeb9b430401722d82db61dc3e289df942653e1f29fba2c855c0c/pglast-7.5-cp312-cp312-win32.whl", hash = "sha256:38107f9f47e29936d3afa5a75038d70d8c6034b89ea93ab0481f882295c86a4f", size = 1010630 }, 203 | { url = "https://files.pythonhosted.org/packages/ca/92/d7824b71b8413139221ff294f413b351084fe54e6210a7e3cbb11134a65e/pglast-7.5-cp312-cp312-win_amd64.whl", hash = "sha256:f3f3d9776f6b91042ae5d3aac4a5513abc9445732a9398916a86f714a28eaa65", size = 1055807 }, 204 | { url = "https://files.pythonhosted.org/packages/2d/87/e7326c1f09109cee3e39a17b44ea600592957b15f5fd575f04e37d82ebc8/pglast-7.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76e3d678523c37590ae6a310e94a5ac75958ca14c807eb2f1d12f69185ccc919", size = 1141764 }, 205 | { url = "https://files.pythonhosted.org/packages/e6/4e/5688fdcc605c3742a69926d6b2b00eaf17691077b946778e7e22c413292d/pglast-7.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb947af1ed01423c7973157b895e4aee3ac4200dd7cbd626de223d07485f0f8c", size = 1080664 }, 206 | { url = "https://files.pythonhosted.org/packages/ea/9f/2aff6a06d8976c144cb021cea8364292ec75072691948d6c7c5b2958efb9/pglast-7.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31177574b2d0e625928b8be806bc8cdda154fe9b0b6f40d729ad469b86d94750", size = 5541606 }, 207 | { url = "https://files.pythonhosted.org/packages/62/1c/a6c927a4f2ff44c2c57a43759ca40229c06e0b3a3749ba0c4eca2c4a8467/pglast-7.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc7ab330a6e59778b1571f8bd027f9c50006ffc41fc9a8c69a06f35c23bc316", size = 5639285 }, 208 | { url = "https://files.pythonhosted.org/packages/1d/0d/63d24574c5036747db2f0b7aacb2d9c870fdaecddd5980821d085f38e4ec/pglast-7.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45bb6fcd99d4b429b6e6edfea860654aab7c5b5e203057900e2206b0308a9a23", size = 5412870 }, 209 | { url = "https://files.pythonhosted.org/packages/88/e7/e3ce46205e562f006b93389e2feaac1fc86799f9f6a1856a97a0fc059bfa/pglast-7.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12a9aa8ef86a3470f60b82128bba003bd616459f7b61f26cb2a01515f3231205", size = 5301392 }, 210 | { url = "https://files.pythonhosted.org/packages/ab/ae/8f67dccfc5233065f6b174134d90b529dbbbeeeec415383b196391b74507/pglast-7.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bfa75e48d626dfe924d95742b196f3aacc5502b8d1e7e7ed1fa9c5bd38c85f35", size = 5258627 }, 211 | { url = "https://files.pythonhosted.org/packages/77/03/e43f4f310e6b6e85f05c737620c1b72bd982e8e656c1615f4103e66d4545/pglast-7.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:89d589face8b645e92d9e52dd2b4b6c033fb5cc939247dab224de8056b60a4bb", size = 5441687 }, 212 | { url = "https://files.pythonhosted.org/packages/fa/63/8fdbcc07bb1e4f46473ea2dfefbf7065cbda50f8842782cd34dd349ef0da/pglast-7.5-cp313-cp313-win32.whl", hash = "sha256:1737c7cfc08861cf22c491cadac2e8553ef4ffb694736c819377e440a34f6b24", size = 1009443 }, 213 | { url = "https://files.pythonhosted.org/packages/59/76/47dda827a4942f098249e793bb50a0925f0e449b915fa925320f5b3ec2a1/pglast-7.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff00725e2b697125a818690ee86606baddcfcf898e665a55752139572796dd30", size = 1051351 }, 214 | ] 215 | 216 | [[package]] 217 | name = "psycopg" 218 | version = "3.2.6" 219 | source = { registry = "https://pypi.org/simple" } 220 | dependencies = [ 221 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 222 | { name = "tzdata", marker = "sys_platform == 'win32'" }, 223 | ] 224 | sdist = { url = "https://files.pythonhosted.org/packages/67/97/eea08f74f1c6dd2a02ee81b4ebfe5b558beb468ebbd11031adbf58d31be0/psycopg-3.2.6.tar.gz", hash = "sha256:16fa094efa2698f260f2af74f3710f781e4a6f226efe9d1fd0c37f384639ed8a", size = 156322 } 225 | wheels = [ 226 | { url = "https://files.pythonhosted.org/packages/d7/7d/0ba52deff71f65df8ec8038adad86ba09368c945424a9bd8145d679a2c6a/psycopg-3.2.6-py3-none-any.whl", hash = "sha256:f3ff5488525890abb0566c429146add66b329e20d6d4835662b920cbbf90ac58", size = 199077 }, 227 | ] 228 | 229 | [[package]] 230 | name = "psycopg-binary" 231 | version = "3.2.6" 232 | source = { registry = "https://pypi.org/simple" } 233 | wheels = [ 234 | { url = "https://files.pythonhosted.org/packages/4b/7b/48afdcb14bf828c4006f573845fbbd98df701bff9043fbb0b8caab261b6f/psycopg_binary-3.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1b639acb3e24243c23f75700bf6e3af7b76da92523ec7c3196a13aaf0b578453", size = 3868985 }, 235 | { url = "https://files.pythonhosted.org/packages/de/45/9e777c61ef3ac5e7fb42618afbd9f41464c1c396ec85c79c48086ace437a/psycopg_binary-3.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1b5c359173726b38d7acbb9f73270f269591d8031d099c1a70dd3f3d22b0e8a8", size = 3938244 }, 236 | { url = "https://files.pythonhosted.org/packages/d6/93/e48962aca19af1f3d2cb0c2ff890ca305c51d1759a2e89c90a527228cf1d/psycopg_binary-3.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3434efe7c00f505f4c1e531519dac6c701df738ba7a1328eac81118d80019132", size = 4523096 }, 237 | { url = "https://files.pythonhosted.org/packages/fe/52/21f4a9bb7123e07e06a712338eb6cc5794a23a56813deb4a8cd3de8ec91c/psycopg_binary-3.2.6-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bca8d9643191b13193940bbf84d51ac5a747e965c230177258fb02b8043fb7a", size = 4329659 }, 238 | { url = "https://files.pythonhosted.org/packages/9e/72/8da1c98b4e0d4c3649f037101b70ae52e4f821597919dabc72c889e60ca9/psycopg_binary-3.2.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55fa40f11d37e6e5149a282a5fd7e0734ce55c623673bfba638480914fd1414c", size = 4575359 }, 239 | { url = "https://files.pythonhosted.org/packages/83/5a/a85c98a5b2b3f771d7478ac0081b48749d4c07ce41d51f89f592f87cfbeb/psycopg_binary-3.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0690ac1061c655b1bcbe9284d07bf5276bc9c0d788a6c74aaf3b042e64984b83", size = 4287138 }, 240 | { url = "https://files.pythonhosted.org/packages/b0/c3/0abafd3f300e5ff952dd9b3be95b4e2527ae1e2ea7fd7a7421e6bc1c0e37/psycopg_binary-3.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9a4a9967ff650d2821d5fad6bec7b15f4c2072603e9fa3f89a39f351ade1fd3", size = 3872142 }, 241 | { url = "https://files.pythonhosted.org/packages/0f/16/029aa400c4b7f4b7042307d8a854768463a65326d061ad2145f7b3989ca5/psycopg_binary-3.2.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d6f2894cc7aee8a15fe591e8536911d9c015cb404432cf7bdac2797e54cb2ba8", size = 3340033 }, 242 | { url = "https://files.pythonhosted.org/packages/cd/a1/28e86b832d696ba5fd79c4d704b8ca46b827428f7ea063063ca280a678a4/psycopg_binary-3.2.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:05560c81312d7c2bee95a9860cd25198677f2320fb4a3527bc04e8cae7fcfb64", size = 3438823 }, 243 | { url = "https://files.pythonhosted.org/packages/93/31/73546c999725b397bb7e7fd55f83a9c78787c6fe7fe457e4179d19a115dc/psycopg_binary-3.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4269cd23a485d6dd6eb6b10841c94551a53091cf0b1b6d5247a6a341f53f0d95", size = 3464031 }, 244 | { url = "https://files.pythonhosted.org/packages/85/38/957bd4bdde976c9a38d61896bf9d2c8f5752b98d8f4d879a7902588a8583/psycopg_binary-3.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:7942f35a6f314608720116bcd9de240110ceadffd2ac5c34f68f74a31e52e46a", size = 2792159 }, 245 | { url = "https://files.pythonhosted.org/packages/5a/71/5bfa1ffc4d59f0454b114ce0d017eca269b079ca2753a96302c2117067c7/psycopg_binary-3.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7afe181f6b3eb714362e9b6a2dc2a589bff60471a1d8639fd231a4e426e01523", size = 3876608 }, 246 | { url = "https://files.pythonhosted.org/packages/7e/07/1724d842b876af7bef442f0853d6cbf951264229414e4d0a57b8e3787847/psycopg_binary-3.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bb0fceba0773dc0bfb53224bb2c0b19dc97ea0a997a223615484cf02cae55c", size = 3942785 }, 247 | { url = "https://files.pythonhosted.org/packages/09/51/a251a356f10c7947bcc2285ebf1541e1c2d851b8db20eb8f29ed3a5974bf/psycopg_binary-3.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54120122d2779dcd307f49e1f921d757fe5dacdced27deab37f277eef0c52a5b", size = 4519448 }, 248 | { url = "https://files.pythonhosted.org/packages/6e/cf/0c92ab1270664a1341e52f5794ecc636b1f4ac67bf1743075091795151f8/psycopg_binary-3.2.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:816aa556f63b2303e66ba6c8888a8b3f3e6e4e47049ec7a4d62c84ac60b091ca", size = 4324382 }, 249 | { url = "https://files.pythonhosted.org/packages/bf/2b/6921bd4a57fe19d4618798a8a8648e1a516c92563c37b2073639fffac5d5/psycopg_binary-3.2.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d19a0ba351eda9a59babf8c7c9d89c7bbc5b26bf096bc349b096bd0dd2482088", size = 4578720 }, 250 | { url = "https://files.pythonhosted.org/packages/5c/30/1034d164e2be09f650a86eccc93625e51568e307c855bf6f94759c298303/psycopg_binary-3.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6e197e01290ef818a092c877025fc28096adbb6d0743e313491a21aab31bd96", size = 4281871 }, 251 | { url = "https://files.pythonhosted.org/packages/c4/d0/67fdf0174c334a9a85a9672590d7da83e85d9cedfc35f473a557e310a1ca/psycopg_binary-3.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:274794b4b29ef426e09086404446b61a146f5e756da71366c5a6d57abec31f7d", size = 3870582 }, 252 | { url = "https://files.pythonhosted.org/packages/9f/4e/3a4fd2d1fd715b11e7287023edde916e1174b58b37873c531f782a49803b/psycopg_binary-3.2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:69845bdc0db519e1dfc27932cd3d5b1ecb3f72950af52a1987508ab0b52b3b55", size = 3334464 }, 253 | { url = "https://files.pythonhosted.org/packages/4a/22/90a8032276fa5b215ce26cefb44abafa8fb09de396c6dc6f62a5e53fe2ad/psycopg_binary-3.2.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:66c3bed2caf0d1cabcb9365064de183b5209a7cbeaa131e79e68f350c9c963c2", size = 3431945 }, 254 | { url = "https://files.pythonhosted.org/packages/e7/b0/e547e9a851ab19c79869c1d84a533e225d284e70c222720fed4529fcda60/psycopg_binary-3.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e3ae3201fe85c7f901349a2cf52f02ceca4cb97a5e2e2ac8b8a1c9a6eb747bed", size = 3463278 }, 255 | { url = "https://files.pythonhosted.org/packages/e7/ce/e555bd8dd6fce8b34bbc3856125600f0842c85a8364702ebe0dc39372087/psycopg_binary-3.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:58f443b4df2adb59937c96775fadf4967f93d952fbcc82394446985faec11041", size = 2795094 }, 256 | { url = "https://files.pythonhosted.org/packages/a3/c7/220b1273f0befb2cd9fe83d379b3484ae029a88798a90bc0d36f10bea5df/psycopg_binary-3.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f27a46ff0497e882e8c0286e8833c785b4d1a80f23e1bf606f4c90e5f9f3ce75", size = 3857986 }, 257 | { url = "https://files.pythonhosted.org/packages/8a/d8/30176532826cf87c608a6f79dd668bf9aff0cdf8eb80209eddf4c5aa7229/psycopg_binary-3.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b30ee4821ded7de48b8048b14952512588e7c5477b0a5965221e1798afba61a1", size = 3940060 }, 258 | { url = "https://files.pythonhosted.org/packages/54/7c/fa7cd1f057f33f7ae483d6bc5a03ec6eff111f8aa5c678d9aaef92705247/psycopg_binary-3.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e57edf3b1f5427f39660225b01f8e7b97f5cfab132092f014bf1638bc85d81d2", size = 4499082 }, 259 | { url = "https://files.pythonhosted.org/packages/b8/81/1606966f6146187c273993ea6f88f2151b26741df8f4e01349a625983be9/psycopg_binary-3.2.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c5172ce3e4ae7a4fd450070210f801e2ce6bc0f11d1208d29268deb0cda34de", size = 4307509 }, 260 | { url = "https://files.pythonhosted.org/packages/69/ad/01c87aab17a4b89128b8036800d11ab296c7c2c623940cc7e6f2668f375a/psycopg_binary-3.2.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfab3804c43571a6615e559cdc4c4115785d258a4dd71a721be033f5f5f378d", size = 4547813 }, 261 | { url = "https://files.pythonhosted.org/packages/65/30/f93a193846ee738ffe5d2a4837e7ddeb7279707af81d088cee96cae853a0/psycopg_binary-3.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fa1c920cce16f1205f37b20c685c58b9656b170b8b4c93629100d342d0d118e", size = 4259847 }, 262 | { url = "https://files.pythonhosted.org/packages/8e/73/65c4ae71be86675a62154407c92af4b917146f9ff3baaf0e4166c0734aeb/psycopg_binary-3.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e118d818101c1608c6b5ba52a6c977614d8f05aa89467501172ba4d10588e11", size = 3846550 }, 263 | { url = "https://files.pythonhosted.org/packages/53/cc/a24626cac3f208c776bb22e15e9a5e483aa81145221e6427e50381f40811/psycopg_binary-3.2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:763319a8bfeca77d31512da71f5a33459b9568a7621c481c3828c62f9c38f351", size = 3320269 }, 264 | { url = "https://files.pythonhosted.org/packages/55/e6/68c76fb9d6c53d5e4170a0c9216c7aa6c2903808f626d84d002b47a16931/psycopg_binary-3.2.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2fbc05819560389dbece046966bc88e0f2ea77673497e274c4293b8b4c1d0703", size = 3399365 }, 265 | { url = "https://files.pythonhosted.org/packages/b4/2c/55b140f5a2c582dae42ef38502c45ef69c938274242a40bd04c143081029/psycopg_binary-3.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a57f99bb953b4bd6f32d0a9844664e7f6ca5ead9ba40e96635be3cd30794813", size = 3438908 }, 266 | { url = "https://files.pythonhosted.org/packages/ae/f6/589c95cceccee2ab408b6b2e16f1ed6db4536fb24f2f5c9ce568cf43270c/psycopg_binary-3.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:5de6809e19a465dcb9c269675bded46a135f2d600cd99f0735afbb21ddad2af4", size = 2782886 }, 267 | { url = "https://files.pythonhosted.org/packages/bf/32/3d06c478fd3070ac25a49c2e8ca46b6d76b0048fa9fa255b99ee32f32312/psycopg_binary-3.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54af3fbf871baa2eb19df96fd7dc0cbd88e628a692063c3d1ab5cdd00aa04322", size = 3852672 }, 268 | { url = "https://files.pythonhosted.org/packages/34/97/e581030e279500ede3096adb510f0e6071874b97cfc047a9a87b7d71fc77/psycopg_binary-3.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad5da1e4636776c21eaeacdec42f25fa4612631a12f25cd9ab34ddf2c346ffb9", size = 3936562 }, 269 | { url = "https://files.pythonhosted.org/packages/74/b6/6a8df4cb23c3d327403a83406c06c9140f311cb56c4e4d720ee7abf6fddc/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7956b9ea56f79cd86eddcfbfc65ae2af1e4fe7932fa400755005d903c709370", size = 4499167 }, 270 | { url = "https://files.pythonhosted.org/packages/e4/5b/950eafef61e5e0b8ddb5afc5b6b279756411aa4bf70a346a6f091ad679bb/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e2efb763188008cf2914820dcb9fb23c10fe2be0d2c97ef0fac7cec28e281d8", size = 4311651 }, 271 | { url = "https://files.pythonhosted.org/packages/72/b9/b366c49afc854c26b3053d4d35376046eea9aebdc48ded18ea249ea1f80c/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b3aab3451679f1e7932270e950259ed48c3b79390022d3f660491c0e65e4838", size = 4547852 }, 272 | { url = "https://files.pythonhosted.org/packages/ab/d4/0e047360e2ea387dc7171ca017ffcee5214a0762f74b9dd982035f2e52fb/psycopg_binary-3.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849a370ac4e125f55f2ad37f928e588291a67ccf91fa33d0b1e042bb3ee1f986", size = 4261725 }, 273 | { url = "https://files.pythonhosted.org/packages/e3/ea/a1b969804250183900959ebe845d86be7fed2cbd9be58f64cd0fc24b2892/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:566d4ace928419d91f1eb3227fc9ef7b41cf0ad22e93dd2c3368d693cf144408", size = 3850073 }, 274 | { url = "https://files.pythonhosted.org/packages/e5/71/ec2907342f0675092b76aea74365b56f38d960c4c635984dcfe25d8178c8/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1981f13b10de2f11cfa2f99a8738b35b3f0a0f3075861446894a8d3042430c0", size = 3320323 }, 275 | { url = "https://files.pythonhosted.org/packages/d7/d7/0d2cb4b42f231e2efe8ea1799ce917973d47486212a2c4d33cd331e7ac28/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:36f598300b55b3c983ae8df06473ad27333d2fd9f3e2cfdb913b3a5aaa3a8bcf", size = 3402335 }, 276 | { url = "https://files.pythonhosted.org/packages/66/92/7050c372f78e53eba14695cec6c3a91b2d9ca56feaf0bfe95fe90facf730/psycopg_binary-3.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0f4699fa5fe1fffb0d6b2d14b31fd8c29b7ea7375f89d5989f002aaf21728b21", size = 3440442 }, 277 | { url = "https://files.pythonhosted.org/packages/5f/4c/bebcaf754189283b2f3d457822a3d9b233d08ff50973d8f1e8d51f4d35ed/psycopg_binary-3.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:afe697b8b0071f497c5d4c0f41df9e038391534f5614f7fb3a8c1ca32d66e860", size = 2783465 }, 278 | ] 279 | 280 | [[package]] 281 | name = "pydantic" 282 | version = "2.10.6" 283 | source = { registry = "https://pypi.org/simple" } 284 | dependencies = [ 285 | { name = "annotated-types" }, 286 | { name = "pydantic-core" }, 287 | { name = "typing-extensions" }, 288 | ] 289 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 290 | wheels = [ 291 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 292 | ] 293 | 294 | [[package]] 295 | name = "pydantic-core" 296 | version = "2.27.2" 297 | source = { registry = "https://pypi.org/simple" } 298 | dependencies = [ 299 | { name = "typing-extensions" }, 300 | ] 301 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 302 | wheels = [ 303 | { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, 304 | { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, 305 | { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, 306 | { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, 307 | { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, 308 | { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, 309 | { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, 310 | { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, 311 | { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, 312 | { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, 313 | { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, 314 | { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, 315 | { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, 316 | { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, 317 | { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, 318 | { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, 319 | { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, 320 | { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, 321 | { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, 322 | { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, 323 | { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, 324 | { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, 325 | { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, 326 | { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, 327 | { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, 328 | { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, 329 | { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, 330 | { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, 331 | { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, 332 | { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, 333 | { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, 334 | { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, 335 | { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, 336 | { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, 337 | { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, 338 | { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, 339 | { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, 340 | { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, 341 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, 342 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, 343 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, 344 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 345 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 346 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 347 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 348 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 349 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 350 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 351 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 352 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 353 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 354 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 355 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 356 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 357 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 358 | { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, 359 | { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, 360 | { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, 361 | { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, 362 | { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, 363 | { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, 364 | { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, 365 | { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, 366 | { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, 367 | ] 368 | 369 | [[package]] 370 | name = "pydantic-settings" 371 | version = "2.8.1" 372 | source = { registry = "https://pypi.org/simple" } 373 | dependencies = [ 374 | { name = "pydantic" }, 375 | { name = "python-dotenv" }, 376 | ] 377 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } 378 | wheels = [ 379 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, 380 | ] 381 | 382 | [[package]] 383 | name = "python-dotenv" 384 | version = "1.0.1" 385 | source = { registry = "https://pypi.org/simple" } 386 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 387 | wheels = [ 388 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 389 | ] 390 | 391 | [[package]] 392 | name = "setuptools" 393 | version = "78.0.2" 394 | source = { registry = "https://pypi.org/simple" } 395 | sdist = { url = "https://files.pythonhosted.org/packages/4c/f4/aa8d364f0dc1f33b2718938648c31202e2db5cd6479a73f0a9ca5a88372d/setuptools-78.0.2.tar.gz", hash = "sha256:137525e6afb9022f019d6e884a319017f9bf879a0d8783985d32cbc8683cab93", size = 1367747 } 396 | wheels = [ 397 | { url = "https://files.pythonhosted.org/packages/aa/db/2fd473dfe436ad19fda190f4079162d400402aedfcc41e048d38c0a375c6/setuptools-78.0.2-py3-none-any.whl", hash = "sha256:4a612c80e1f1d71b80e4906ce730152e8dec23df439f82731d9d0b608d7b700d", size = 1255965 }, 398 | ] 399 | 400 | [[package]] 401 | name = "sniffio" 402 | version = "1.3.1" 403 | source = { registry = "https://pypi.org/simple" } 404 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 405 | wheels = [ 406 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 407 | ] 408 | 409 | [[package]] 410 | name = "sse-starlette" 411 | version = "2.2.1" 412 | source = { registry = "https://pypi.org/simple" } 413 | dependencies = [ 414 | { name = "anyio" }, 415 | { name = "starlette" }, 416 | ] 417 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 418 | wheels = [ 419 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 420 | ] 421 | 422 | [[package]] 423 | name = "starlette" 424 | version = "0.46.1" 425 | source = { registry = "https://pypi.org/simple" } 426 | dependencies = [ 427 | { name = "anyio" }, 428 | ] 429 | sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } 430 | wheels = [ 431 | { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, 432 | ] 433 | 434 | [[package]] 435 | name = "typing-extensions" 436 | version = "4.12.2" 437 | source = { registry = "https://pypi.org/simple" } 438 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 439 | wheels = [ 440 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 441 | ] 442 | 443 | [[package]] 444 | name = "tzdata" 445 | version = "2025.2" 446 | source = { registry = "https://pypi.org/simple" } 447 | sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } 448 | wheels = [ 449 | { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, 450 | ] 451 | 452 | [[package]] 453 | name = "uvicorn" 454 | version = "0.34.0" 455 | source = { registry = "https://pypi.org/simple" } 456 | dependencies = [ 457 | { name = "click" }, 458 | { name = "h11" }, 459 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 460 | ] 461 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 462 | wheels = [ 463 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 464 | ] 465 | --------------------------------------------------------------------------------