From d62a9d9304a39cdc43161db937b615307f87d5f3 Mon Sep 17 00:00:00 2001 From: MangoFanFanw Date: Thu, 28 May 2026 16:12:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=87=E6=8D=A2=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E8=87=B3=20PostgreSQL=E3=80=81=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 未来计划支持多种数据库,从 PostgreSQL 开始! 支持从环境变量读取启动设置,添加 nyahome env 命令用来持久化环境变量。 BREAKING CHANGE: 切换开发阶段的数据库,从 SQLite 到 PostgreSQL。 --- pyproject.toml | 2 + src/nyahome/cli/cli.py | 3 ++ src/nyahome/cli/cli_env.py | 61 ++++++++++++++++++++++++++++++ src/nyahome/database/engine.py | 6 +-- src/nyahome/manage.py | 26 ++++++++----- src/nyahome/server.py | 2 + uv.lock | 68 ++++++++++++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 src/nyahome/cli/cli.py create mode 100644 src/nyahome/cli/cli_env.py diff --git a/pyproject.toml b/pyproject.toml index ae68265..52a08ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,9 @@ dependencies = [ "jinja2>=3.1.6", "openai>=2.38.0", "passlib[bcrypt]>=1.7.4", + "psycopg[binary]>=3.3.4", "pydantic>=2.13.4", + "python-dotenv>=1.2.2", "python-jose[cryptography]>=3.5.0", "python-multipart>=0.0.29", "pyyaml>=6.0.3", diff --git a/src/nyahome/cli/cli.py b/src/nyahome/cli/cli.py new file mode 100644 index 0000000..5f8e6bd --- /dev/null +++ b/src/nyahome/cli/cli.py @@ -0,0 +1,3 @@ +from rich.console import Console + +console = Console() \ No newline at end of file diff --git a/src/nyahome/cli/cli_env.py b/src/nyahome/cli/cli_env.py new file mode 100644 index 0000000..43b4c14 --- /dev/null +++ b/src/nyahome/cli/cli_env.py @@ -0,0 +1,61 @@ +import os +from pathlib import Path +from typing import Annotated + +import typer +from dotenv import load_dotenv, set_key, unset_key +from rich.table import Table + +from .cli import console + +env_app = typer.Typer() + + +ENV_PATH = Path.cwd() / ".nyahome" / ".env" + + +@env_app.command(name="list") +def list_all_envs() -> None: + """ + 列出所有以 NYAHOME_ 开头的环境变量。 + + 这些变量可能配置在 .nyahome 内的 .env 文件中,或者通过其他方式预先设置。 + """ + if not load_dotenv(ENV_PATH): + console.print( + "[bright_black]未在 .nyahome 目录下读取到任何环境变量。如果这是有意为之,则请无需担心。[/bright_black]" + ) + table = Table(title="NyaHome 应用的环境变量") + table.add_column("Key", style="cyan", no_wrap=True) + table.add_column("Value", style="white") + + for env in os.environ.items(): + if env[0].startswith("NYAHOME_"): + table.add_row(env[0], env[1]) + + console.print(table) + + +@env_app.command(name="set") +def set_env( + key: Annotated[str, typer.Argument(help="不包含 NYAHOME_ 的键名")], + value: Annotated[str, typer.Argument(help="环境变量值")], +) -> None: + """ + 设置 NYAHOME_ 环境变量。请参考 NyaHome 文档以了解使用方法。 + + 保存在 .nyahome 内的 .env 文件。 + """ + set_key(ENV_PATH, f"NYAHOME_{key.upper()}", value) + console.print(f"[cyan]已设置环境变量 NYAHOME_{key}。[/cyan]") + + +@env_app.command(name="unset") +def unset_env(key: Annotated[str, typer.Argument(help="不包含 NYAHOME_ 的键名")]) -> None: + """ + 删除 NYAHOME_ 环境变量。 + + 操作在 .nyahome 内的 .env 文件。 + """ + unset_key(ENV_PATH, f"NYAHOME_{key.upper()}") + console.print(f"[cyan]已删除环境变量 NYAHOME_{key}。[/cyan]") diff --git a/src/nyahome/database/engine.py b/src/nyahome/database/engine.py index 56d789e..8201959 100644 --- a/src/nyahome/database/engine.py +++ b/src/nyahome/database/engine.py @@ -1,7 +1,5 @@ -from pathlib import Path +import os from sqlmodel import create_engine -sqlite_file_path = Path.cwd() / ".nyahome" / "nyahome.db" - -engine = create_engine(f"sqlite:///{sqlite_file_path!s}", connect_args={"check_same_thread": False}) +engine = create_engine(os.environ["NYAHOME_SQL_URL"]) diff --git a/src/nyahome/manage.py b/src/nyahome/manage.py index 84d1e3f..1e0ec3e 100644 --- a/src/nyahome/manage.py +++ b/src/nyahome/manage.py @@ -6,13 +6,13 @@ from typing import Annotated import typer -from rich.console import Console from nyahome import __version__ +from nyahome.cli.cli import console +from nyahome.cli.cli_env import ENV_PATH, env_app -console = Console() app = typer.Typer( - name="Nya Home", + name="NyaHome", help="🌸 为你而存在的故事之家 ~", rich_markup_mode="rich", no_args_is_help=True, @@ -21,7 +21,7 @@ app = typer.Typer( def version_callback(value: bool = False) -> None: if value: - console.print(f"[green]Nya Home[/green] version {__version__}") + console.print(f"[green]NyaHome[/green] version {__version__}") @app.callback(invoke_without_command=True) @@ -35,21 +35,26 @@ def main( is_eager=True, ), ) -> None: - console.print("[bright_black]Nya Home 仍然处于极早期的阶段。如果遇到任何问题,请告诉芒果帆帆喵![/bright_black]") + console.print("[bright_black]NyaHome 仍然处于极早期的阶段。如果遇到任何问题,请告诉芒果帆帆喵![/bright_black]") @app.command() def run() -> None: """ - 运行 Nya Home。 + 运行 NyaHome。 """ + import os + import uvicorn + from dotenv import load_dotenv + + load_dotenv(ENV_PATH) uvicorn.run( "nyahome.server:app", - reload=False, - host="0.0.0.0", - port=9000, + reload=os.getenv("NYAHOME_UVICORN_RELOAD", "false") in ["True", "true", "1"], + host=os.getenv("NYAHOME_UVICORN_HOST", "0.0.0.0"), + port=int(os.getenv("NYAHOME_UVICORN_PORT", "9000")), timeout_graceful_shutdown=2, log_config="logging.yaml", log_level="debug", @@ -69,5 +74,8 @@ def openapi( console.print(f"[cyan]已经保存 openapi.json 到 {path} 。[/cyan]") +app.add_typer(env_app, name="env", no_args_is_help=True, help="设置 NyaHome 应用的环境变量。") + + if __name__ == "__main__": app() diff --git a/src/nyahome/server.py b/src/nyahome/server.py index b7565f9..39ac20a 100644 --- a/src/nyahome/server.py +++ b/src/nyahome/server.py @@ -5,6 +5,7 @@ from contextlib import asynccontextmanager from pathlib import Path from typing import Any, AsyncGenerator +from dotenv import load_dotenv from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles @@ -21,6 +22,7 @@ logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app_: FastAPI) -> AsyncGenerator[None, Any]: + load_dotenv(Path.cwd() / ".nyahome" / ".env") logger.info("🚀 服务启动中...") create_db() await asyncio.gather(init_admin_user(), config_manager.async_load_config()) diff --git a/uv.lock b/uv.lock index 2e05cec..470e28c 100644 --- a/uv.lock +++ b/uv.lock @@ -736,7 +736,9 @@ dependencies = [ { name = "jinja2" }, { name = "openai" }, { name = "passlib", extra = ["bcrypt"] }, + { name = "psycopg", extra = ["binary"] }, { name = "pydantic" }, + { name = "python-dotenv" }, { name = "python-jose", extra = ["cryptography"] }, { name = "python-multipart" }, { name = "pyyaml" }, @@ -769,7 +771,9 @@ requires-dist = [ { name = "jinja2", specifier = ">=3.1.6" }, { name = "openai", specifier = ">=2.38.0" }, { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.3.4" }, { name = "pydantic", specifier = ">=2.13.4" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" }, { name = "python-multipart", specifier = ">=0.0.29" }, { name = "pyyaml", specifier = ">=6.0.3" }, @@ -848,6 +852,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" }, ] +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" }, +] + [[package]] name = "pyasn1" version = "0.6.3" @@ -946,6 +996,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + [[package]] name = "python-jose" version = "3.5.0" @@ -1290,6 +1349,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + [[package]] name = "uvicorn" version = "0.47.0"