diff --git a/.github/workflows/pypi-publish.yaml b/.github/workflows/pypi-publish.yaml new file mode 100644 index 00000000..110d512c --- /dev/null +++ b/.github/workflows/pypi-publish.yaml @@ -0,0 +1,21 @@ +name: Publish + +on: + push: + tags: + - 'v*' + +jobs: + pypi-publish: + name: upload release to PyPI + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v3 + + - uses: pdm-project/setup-pdm@v3 + + - name: Publish package distributions to PyPI + run: pdm publish \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b67ddfe7..f9ec1364 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-alpine +FROM reg.liteyuki.icu/dockerhub/python:3.12-alpine WORKDIR /liteyukibot diff --git a/cli/__main__.py b/cli/__main__.py new file mode 100644 index 00000000..e69de29b diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index 00fbccc3..a3d45764 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -6,5 +6,6 @@ services: volumes: - ./plugins:/liteyukibot/plugins - ./data:/liteyukibot/data + - ./configs:/liteyukibot/configs ports: - "8090:8080" \ No newline at end of file diff --git a/docker-compose-example.yaml b/docker-compose-example.yaml index 43747b3f..a8bb5583 100644 --- a/docker-compose-example.yaml +++ b/docker-compose-example.yaml @@ -8,7 +8,8 @@ services: image: reg.liteyuki.icu/bot/app:latest restart: always volumes: - - ./plugins:/liteyukibot/plugins # 外部插件目录,插件开发者可用 + - ./configs:/liteyukibot/configs # 配置目录,包含配置文件 - ./data:/liteyukibot/data # 数据目录,包含下载器安装的插件目录 + - ./plugins:/liteyukibot/plugins # 外部插件目录,插件开发者可用 ports: - "8090:8080" \ No newline at end of file diff --git a/liteyukibot/__init__.py b/liteyukibot/__init__.py index e69de29b..3946a849 100644 --- a/liteyukibot/__init__.py +++ b/liteyukibot/__init__.py @@ -0,0 +1,3 @@ +from .daemon import Daemon + +__all__ = ["Daemon"] \ No newline at end of file diff --git a/liteyukibot/asgi/__init__.py b/liteyukibot/asgi/__init__.py new file mode 100644 index 00000000..e41edf07 --- /dev/null +++ b/liteyukibot/asgi/__init__.py @@ -0,0 +1,20 @@ +import asyncio + +from fastapi import FastAPI +import hypercorn +import hypercorn.run + +app = FastAPI() + +@app.get("/") +async def root(): + return {"message": "Hello LiteyukiBot"} + + +async def run_app(): + """liteyukibot入口函数 + """ + hypercorn.run.serve(app, config=hypercorn.Config.from_mapping( + bind=["localhost:8000"], + workers=1, + )) \ No newline at end of file diff --git a/liteyukibot/config.py b/liteyukibot/config.py index e3c17988..9470a6b9 100644 --- a/liteyukibot/config.py +++ b/liteyukibot/config.py @@ -1,10 +1,11 @@ - from typing import Any import json import yaml import tomllib +config: dict[str, Any] = {} # 全局配置map +flat_config: dict[str, Any] = {} # 扁平化配置map def load_from_yaml(file_path: str) -> dict[str, Any]: """从yaml文件中加载配置并返回字典 @@ -41,4 +42,57 @@ def load_from_toml(file_path: str) -> dict[str, Any]: dict[str, Any]: 配置字典 """ with open(file_path, "rb") as file: - return tomllib.load(file) \ No newline at end of file + return tomllib.load(file) + +def merge_to_config(new_config: dict[str, Any], warn: bool=True) -> None: + """加载配置到全局配置字典,该函数有副作用,开发者尽量不要在多份配置文件中使用重复的配置项,否则会被覆盖 + + Args: + new_config (dict[str, Any]): 新的字典 + warn (bool, optional): 是否启用重复键警告. 默认 True. + """ + global config, flat_config + config.update(new_config) + flat_config = flatten_dict(config) + +def flatten_dict(d: dict[str, Any], parent_key: str = '', sep: str = '.') -> dict[str, Any]: + """将嵌套字典扁平化 + + Args: + d (dict[str, Any]): 嵌套字典 + parent_key (str, optional): 父键名. 默认值为 '' + sep (str, optional): 分隔符. 默认值为 '.' + + Returns: + dict[str, Any]: 扁平化字典 + + Example: + input_dict = { + "server": { + "host": "localhost", + "port: 8080 + } + } + + output_dict = flatten_dict(input_dict) + output_dict = { + "server.host": "localhost", + "server.port": 8080 + } + """ + items: list[tuple[str, Any]] = [] + for k, v in d.items(): + new_key = f"{parent_key}{sep}{k}" if parent_key else k + if isinstance(v, dict): + items.extend(flatten_dict(v, new_key, sep=sep).items()) + else: + items.append((new_key, v)) + return dict(items) + +def load_config_to_global(reset: bool = False) -> None: + """加载配置到全局配置字典 + + Args: + reset (bool, optional): 是否重置配置项. 默认 False. + """ + \ No newline at end of file diff --git a/mo.py b/mo.py deleted file mode 100644 index d25d49e0..00000000 --- a/mo.py +++ /dev/null @@ -1 +0,0 @@ -a = 1 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5a97138a..382f625d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,8 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "fastapi>=0.115.12", + "loguru>=0.7.3", + "nonecorn>=0.17.3", "pydantic>=2.11.3", "pyyaml>=6.0.2", "uvicorn>=0.34.2", @@ -14,6 +16,7 @@ dependencies = [ [dependency-groups] dev = [ "mypy>=1.15.0", + "pip>=25.1", "pytest>=8.3.5", "ruff>=0.11.7", ] diff --git a/tests/test_config.py b/tests/test_config.py index a8c1ce9e..909e4dd7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -40,4 +40,30 @@ def test_load_from_toml(): result = config.load_from_toml(temp_file_path) assert result["info"]["name"] == "LiteyukiBot" - assert result["info"]["version"] == "7.0.0" \ No newline at end of file + assert result["info"]["version"] == "7.0.0" + + +def test_flatten_dict(): + nested_dict = { + "name": "LiteyukiBot", + "version": { + "major": 7, + "minor": 0, + "patch": 0 + }, + "server": { + "db": { + "host": "localhost", + "port": 8080 + }, + "tags": ["tag1", "tag2"] + } + } + flat_dict = config.flatten_dict(nested_dict) + assert flat_dict["name"] == "LiteyukiBot" + assert flat_dict["version.major"] == 7 + assert flat_dict["version.minor"] == 0 + assert flat_dict["version.patch"] == 0 + assert flat_dict["server.db.host"] == "localhost" + assert flat_dict["server.db.port"] == 8080 + assert flat_dict["server.tags"] == ["tag1", "tag2"] \ No newline at end of file diff --git a/uv.lock b/uv.lock index d9647f28..a94ec165 100644 --- a/uv.lock +++ b/uv.lock @@ -46,6 +46,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + [[package]] name = "fastapi" version = "0.115.12" @@ -69,6 +78,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, ] +[[package]] +name = "h2" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957 }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, +] + [[package]] name = "idna" version = "3.10" @@ -93,6 +133,8 @@ version = "7.0.0" source = { virtual = "." } dependencies = [ { name = "fastapi" }, + { name = "loguru" }, + { name = "nonecorn" }, { name = "pydantic" }, { name = "pyyaml" }, { name = "uvicorn" }, @@ -101,6 +143,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "mypy" }, + { name = "pip" }, { name = "pytest" }, { name = "ruff" }, ] @@ -108,6 +151,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.115.12" }, + { name = "loguru", specifier = ">=0.7.3" }, + { name = "nonecorn", specifier = ">=0.17.3" }, { name = "pydantic", specifier = ">=2.11.3" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "uvicorn", specifier = ">=0.34.2" }, @@ -116,10 +161,24 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = ">=1.15.0" }, + { name = "pip", specifier = ">=25.1" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "ruff", specifier = ">=0.11.7" }, ] +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, +] + [[package]] name = "mypy" version = "1.15.0" @@ -154,6 +213,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, ] +[[package]] +name = "nonecorn" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup" }, + { name = "h11" }, + { name = "h2" }, + { name = "priority" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/51/3bff01e452f1d12848d1c88be8a59c361d2a7e1b16e1e99c968af62ae8d4/nonecorn-0.17.3.tar.gz", hash = "sha256:8c75c366979f5b11c2bea646133ca1c52f6e20ee03948c192518c496f387ec55", size = 180485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/cb/a3a50d0e82554845fb93d8a20c119b648f27fa818e4352ca8e6d0f6dda72/nonecorn-0.17.3-py3-none-any.whl", hash = "sha256:8c391be6ae61655805ab2b5c4637953f040db1268ddc75be720f450a7ebe2ad0", size = 72065 }, +] + [[package]] name = "packaging" version = "25.0" @@ -163,6 +238,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] +[[package]] +name = "pip" +version = "25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/67/c06f625e2968c417052b3a4a0eef40656d5d4d44033e57b40ec474af1d28/pip-25.1.tar.gz", hash = "sha256:272bdd1289f80165e9070a4f881e8f9e1001bbb50378561d1af20e49bf5a2200", size = 1939624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f0/8a2806114cd36e282823fd4d8e88e3b94dc943c2569c350d0c826a49db38/pip-25.1-py3-none-any.whl", hash = "sha256:13b4aa0aaad055020a11bec8a1c2a70a2b2d080e12d89b962266029fff0a16ba", size = 1824948 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -172,6 +256,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "priority" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/3c/eb7c35f4dcede96fca1842dac5f4f5d15511aa4b52f3a961219e68ae9204/priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0", size = 24792 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", size = 8946 }, +] + [[package]] name = "pydantic" version = "2.11.3" @@ -349,3 +442,24 @@ sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9 wheels = [ { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 }, ] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 }, +]