first init
This commit is contained in:
commit
cc74e2b880
14
.gitattributes
vendored
Normal file
14
.gitattributes
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# 自动处理所有文本文件
|
||||
* text=auto
|
||||
|
||||
# 指定特定文件使用 LF
|
||||
*.sh text eol=lf
|
||||
|
||||
# Windows 脚本使用 CRLF
|
||||
*.bat text eol=crlf
|
||||
|
||||
# 图片、视频、压缩包
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.zip binary
|
||||
*.exe binary
|
||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
.vscode/
|
||||
tmp/
|
||||
40
.pre-commit-config.yaml
Normal file
40
.pre-commit-config.yaml
Normal file
@ -0,0 +1,40 @@
|
||||
repos:
|
||||
# 1️⃣ 自定义 fullwidth-to-halfwidth 脚本
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: fullwidth-to-halfwidth
|
||||
name: Convert fullwidth characters to halfwidth
|
||||
entry: python tools/fullwidth_to_halfwidth_converter.py
|
||||
language: system
|
||||
types: [python]
|
||||
verbose: true
|
||||
|
||||
# 3️⃣ autopep8 自动格式化
|
||||
- repo: https://github.com/pre-commit/mirrors-autopep8
|
||||
rev: v2.0.4
|
||||
hooks:
|
||||
- id: autopep8
|
||||
args:
|
||||
["--in-place", "--aggressive", "--aggressive", "--max-line-length=100"]
|
||||
|
||||
# 2️⃣ Ruff 检查 & 自动修复
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
# 忽略全角字符相关的警告,忽略行长度限制
|
||||
# args: ["--fix"]
|
||||
args: ["--fix", "--ignore=RUF001,RUF002,RUF003,RUF022", "--line-length=100"]
|
||||
|
||||
# 4️⃣ 常用 pre-commit hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-merge-conflict
|
||||
- id: detect-private-key
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: check-added-large-files
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.12
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
123
README.md
Normal file
123
README.md
Normal file
@ -0,0 +1,123 @@
|
||||
# Python 项目模板
|
||||
|
||||
一个标准化的 Python 项目开发模板,集成了配置管理、日志系统和 Pydantic 数据验证。
|
||||
|
||||
## 特性
|
||||
|
||||
- 🔧 **配置管理**: 基于 YAML 的配置文件,使用 Pydantic 进行数据验证
|
||||
- 📝 **日志系统**: 集成 Loguru,支持控制台和文件输出,自动轮转和压缩
|
||||
- 🏗️ **标准结构**: 清晰的项目目录结构,便于维护和扩展
|
||||
- ✅ **类型安全**: 使用 Pydantic 模型确保配置数据的类型安全
|
||||
- 🔄 **单例模式**: 日志管理器采用单例模式,确保全局唯一实例
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
├── config/ # 配置文件
|
||||
│ └── config.yaml # 主配置文件
|
||||
├── examples/ # 使用示例
|
||||
│ ├── example_config_loader.py
|
||||
│ └── example_logger.py
|
||||
├── src/ # 源代码
|
||||
│ ├── core/ # 核心功能模块
|
||||
│ ├── models/ # 数据模型
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── config_models.py # 配置数据模型
|
||||
│ ├── modules/ # 业务模块
|
||||
│ └── utils/ # 工具类
|
||||
│ ├── config_loader.py # 配置加载器
|
||||
│ └── logger.py # 日志管理器
|
||||
├── tmp/ # 临时文件
|
||||
│ └── log/ # 日志文件
|
||||
├── main.py # 程序入口
|
||||
├── pyproject.toml # 项目配置
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Python >= 3.12
|
||||
- uv (推荐) 或 pip
|
||||
|
||||
### 安装依赖
|
||||
|
||||
使用 uv (推荐):
|
||||
```bash
|
||||
uv sync
|
||||
pre-commit install # 可选
|
||||
```
|
||||
|
||||
或使用 pip:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 运行项目
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 1. 配置管理
|
||||
|
||||
配置系统使用 Pydantic 进行数据验证,确保配置的正确性。
|
||||
|
||||
```python
|
||||
from src.utils.config_loader import get_config_loader
|
||||
|
||||
# 获取配置加载器
|
||||
loader = get_config_loader()
|
||||
|
||||
# 验证并加载配置
|
||||
config = loader.validate_config()
|
||||
|
||||
# 获取日志配置
|
||||
log_config = loader.get_log_config()
|
||||
```
|
||||
|
||||
### 2. 日志系统
|
||||
|
||||
基于 Loguru 的日志系统,支持多种输出格式和自动轮转。
|
||||
|
||||
```python
|
||||
from src.utils.logger import get_logger
|
||||
|
||||
# 获取日志记录器
|
||||
logger = get_logger("MODULE_NAME")
|
||||
|
||||
# 记录日志
|
||||
logger.info("这是一条信息日志")
|
||||
logger.error("这是一条错误日志")
|
||||
```
|
||||
## 开发
|
||||
|
||||
### 添加新模块
|
||||
|
||||
1. 在 `src/modules/` 下创建新的业务模块
|
||||
2. 在 `src/core/` 下添加核心功能
|
||||
3. 在 `src/utils/` 下添加工具函数
|
||||
|
||||
### 添加新配置
|
||||
|
||||
1. 在 `src/models/config_models.py` 中定义新的配置模型
|
||||
2. 在 `config/config.yaml` 中添加对应配置
|
||||
3. 更新配置加载器以支持新配置
|
||||
|
||||
### 日志使用规范
|
||||
|
||||
- 使用有意义的模块标签: `get_logger("API")`, `get_logger("DATABASE")`
|
||||
- 合理使用日志级别: DEBUG < INFO < WARNING < ERROR < CRITICAL
|
||||
- 记录关键操作和错误信息
|
||||
|
||||
### 代码风格
|
||||
|
||||
遵循 PEP 8 代码风格指南,保持代码整洁和一致性。基于ruff进行代码检查和格式化。
|
||||
|
||||
|
||||
## 作者
|
||||
|
||||
wds @ (wdsnpshy@163.com)
|
||||
22
config/config.yaml
Normal file
22
config/config.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
log:
|
||||
# 设置控制台输出的格式,时间、版本、名称,级别,,模块标签,消息(带颜色)
|
||||
log_format: "<green>{time:YY-MM-DD HH:mm:ss}</green> | <yellow> version: {extra[version]}</yellow> | <level>{level: <3}</level> | <cyan>{name}</cyan> | <light-blue>{extra[tag]}</light-blue> | <light-green>{message}</light-green>"
|
||||
# 设置日志文件输出的格式,时间、版本、名称,级别,,模块标签,消息
|
||||
log_format_file: "{time:YYYY-MM-DD HH:mm:ss} - {extra[version]} - {name} - {level} - {extra[tag]} - {message}"
|
||||
|
||||
version: "0.1" # 系统的版本要一起输出,直接在config.yaml中设置
|
||||
# 设置日志等级:INFO、DEBUG
|
||||
log_level: DEBUG
|
||||
# 设置日志路径
|
||||
log_dir: ./tmp/log
|
||||
# 设置日志文件
|
||||
log_file: "server.log"
|
||||
# 日志轮转
|
||||
rotation: "10 MB"
|
||||
retention: "30 days"
|
||||
compression: "gz"
|
||||
|
||||
|
||||
agent:
|
||||
prompt: "你现在是心镜,根据主人的皮肤状态以及心情来安慰用户以及对话! "
|
||||
|
||||
28
examples/example_config_loader.py
Normal file
28
examples/example_config_loader.py
Normal file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
配置加载器简单使用示例
|
||||
"""
|
||||
|
||||
from src.utils.config_loader import get_config_loader
|
||||
|
||||
|
||||
def main():
|
||||
"""简单使用示例"""
|
||||
# 获取配置加载器
|
||||
loader = get_config_loader()
|
||||
|
||||
# 验证配置
|
||||
loader.validate_config()
|
||||
|
||||
# 获取日志配置
|
||||
log_config = loader.get_log_config()
|
||||
|
||||
# 使用配置
|
||||
print(f"日志等级: {log_config.log_level}")
|
||||
print(f"日志目录: {log_config.log_dir}")
|
||||
print(f"日志文件: {log_config.log_file}")
|
||||
print(f"系统版本: {log_config.version}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
30
examples/example_logger.py
Normal file
30
examples/example_logger.py
Normal file
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
日志记录器简单使用示例
|
||||
"""
|
||||
from src.utils.logger import get_logger
|
||||
|
||||
|
||||
def main():
|
||||
"""简单使用示例"""
|
||||
# 获取不同模块的日志记录器
|
||||
system_logger = get_logger("SYSTEM")
|
||||
api_logger = get_logger("API")
|
||||
db_logger = get_logger("DATABASE")
|
||||
|
||||
# 记录不同级别的日志
|
||||
system_logger.info("系统启动")
|
||||
system_logger.debug("调试信息")
|
||||
|
||||
api_logger.info("API服务启动")
|
||||
api_logger.warning("API请求频率过高")
|
||||
|
||||
db_logger.info("数据库连接成功")
|
||||
db_logger.error("数据库查询失败")
|
||||
|
||||
# 记录带有额外信息的日志
|
||||
api_logger.info("用户登录", extra={"user_id": 12345, "ip": "192.168.1.1"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
6
main.py
Normal file
6
main.py
Normal file
@ -0,0 +1,6 @@
|
||||
def main():
|
||||
print("Hello from Heart_Mirror_Agent!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
34
pyproject.toml
Normal file
34
pyproject.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[project]
|
||||
name = "Heart_Mirror_Agent"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"agentscope>=1.0.10",
|
||||
"loguru>=0.7.3",
|
||||
"pre-commit>=4.3.0",
|
||||
"pydantic>=2.11.7",
|
||||
"pyyaml>=6.0.2",
|
||||
"ruff>=0.12.11",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
lint.select = [
|
||||
"E", # pycodestyle(比如 E501 行过长)
|
||||
"W", # pycodestyle warnings(比如 W293 空白行含空格)
|
||||
"F", # pyflakes(未使用变量等)
|
||||
"B", # bugbear(潜在 bug)
|
||||
"UP", # pyupgrade(自动升级语法)
|
||||
"I", # isort(导入顺序)
|
||||
"RUF", # Ruff 自己的规则(比如 Unicode 混淆检查)
|
||||
|
||||
]
|
||||
exclude = [
|
||||
".git",
|
||||
"__pycache__",
|
||||
"build",
|
||||
"dist",
|
||||
".venv",
|
||||
]
|
||||
28
run.sh
Executable file
28
run.sh
Executable file
@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# 使用此方式规范导包流程,全部必须从项目根目录开始导入
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
|
||||
# 检查是否提供了参数
|
||||
if [ $# -eq 0 ]; then
|
||||
# echo "用法: $0 <python_script> [args...]"
|
||||
# echo "例如: $0 tests/client_example.py"
|
||||
# echo " $0 main.py --port 8080"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 获取要执行的 Python 脚本路径(第一个参数)
|
||||
PYTHON_SCRIPT="$1"
|
||||
# 移除第一个参数,剩下的参数传递给 Python 脚本
|
||||
shift
|
||||
|
||||
# 检查指定的 Python 脚本是否存在
|
||||
if [ ! -f "${PROJECT_ROOT}/${PYTHON_SCRIPT}" ]; then
|
||||
echo "错误: Python 脚本 '${PROJECT_ROOT}/${PYTHON_SCRIPT}' 不存在。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# # 执行 Python 脚本,并传递剩余参数
|
||||
# echo "执行 Python 脚本: ${PROJECT_ROOT}/${PYTHON_SCRIPT} $*"
|
||||
uv run python "${PROJECT_ROOT}/${PYTHON_SCRIPT}" "$@"
|
||||
3
src/models/__init__.py
Normal file
3
src/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
配置数据模型包
|
||||
"""
|
||||
82
src/models/config_models.py
Normal file
82
src/models/config_models.py
Normal file
@ -0,0 +1,82 @@
|
||||
"""
|
||||
配置文件的Pydantic数据模型
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class LogConfig(BaseModel):
|
||||
"""日志配置模型"""
|
||||
log_format: str = Field(
|
||||
...,
|
||||
description="控制台输出格式",
|
||||
min_length=1
|
||||
)
|
||||
log_format_file: str = Field(
|
||||
...,
|
||||
description="日志文件输出格式",
|
||||
min_length=1
|
||||
)
|
||||
version: str = Field(
|
||||
default="0.1",
|
||||
description="系统版本"
|
||||
)
|
||||
log_level: str = Field(
|
||||
default="INFO",
|
||||
description="日志等级"
|
||||
)
|
||||
log_dir: str = Field(
|
||||
default="./tmp/log",
|
||||
description="日志路径"
|
||||
)
|
||||
log_file: str = Field(
|
||||
default="server.log",
|
||||
description="日志文件名"
|
||||
)
|
||||
rotation: str = Field(
|
||||
default="100 MB",
|
||||
description="日志轮转大小"
|
||||
)
|
||||
retention: str = Field(
|
||||
default="30 days",
|
||||
description="日志保留时间"
|
||||
)
|
||||
compression: str = Field(
|
||||
default="gz",
|
||||
description="日志压缩格式"
|
||||
)
|
||||
|
||||
@field_validator('log_level')
|
||||
def validate_log_level(cls, v):
|
||||
"""验证日志等级"""
|
||||
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
||||
if v.upper() not in valid_levels:
|
||||
raise ValueError(f'日志等级必须是以下之一: {valid_levels}')
|
||||
return v.upper()
|
||||
|
||||
@field_validator('log_dir')
|
||||
def validate_log_dir(cls, v):
|
||||
"""验证日志目录路径"""
|
||||
try:
|
||||
Path(v).resolve()
|
||||
return v
|
||||
except Exception as e:
|
||||
raise ValueError(f'无效的日志目录路径: {v}') from e
|
||||
|
||||
@field_validator('compression')
|
||||
def validate_compression(cls, v):
|
||||
"""验证压缩格式"""
|
||||
valid_formats = ['gz', 'bz2', 'xz', 'zip']
|
||||
if v.lower() not in valid_formats:
|
||||
raise ValueError(f'压缩格式必须是以下之一: {valid_formats}')
|
||||
return v.lower()
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
"""完整配置模型"""
|
||||
log: LogConfig = Field(..., description="日志配置")
|
||||
|
||||
class Config:
|
||||
# 允许额外字段
|
||||
extra = "allow"
|
||||
56
src/utils/config_loader.py
Normal file
56
src/utils/config_loader.py
Normal file
@ -0,0 +1,56 @@
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from pydantic import ValidationError
|
||||
|
||||
from ..models.config_models import Config, LogConfig
|
||||
|
||||
|
||||
class ConfigLoader:
|
||||
def __init__(self, config_path):
|
||||
self.config_path = Path(config_path)
|
||||
self.config_model: Config = None
|
||||
|
||||
def validate_config(self) -> Config:
|
||||
"""使用Pydantic验证配置文件"""
|
||||
try:
|
||||
with open(self.config_path, encoding='utf-8') as file:
|
||||
config_data = yaml.safe_load(file)
|
||||
|
||||
# 使用Pydantic模型验证配置
|
||||
self.config_model = Config(**config_data)
|
||||
return self.config_model
|
||||
|
||||
except FileNotFoundError as e:
|
||||
raise FileNotFoundError(f"配置文件未找到: {self.config_path}") from e
|
||||
except yaml.YAMLError as e:
|
||||
raise ValueError(f"配置文件格式错误: {e}") from e
|
||||
except ValidationError as e:
|
||||
raise ValueError(f"配置验证失败: {e}") from e
|
||||
|
||||
def get_log_config(self) -> LogConfig:
|
||||
"""获取日志配置"""
|
||||
if self.config_model is None:
|
||||
self.validate_config()
|
||||
|
||||
return self.config_model.log
|
||||
|
||||
def get_config(self) -> Config:
|
||||
"""获取完整配置"""
|
||||
if self.config_model is None:
|
||||
self.validate_config()
|
||||
return self.config_model
|
||||
|
||||
|
||||
# 返回全局唯一实例
|
||||
instance = None
|
||||
|
||||
|
||||
def get_config_loader() -> ConfigLoader:
|
||||
"""获取配置加载器"""
|
||||
global instance
|
||||
if instance is None:
|
||||
base_path = Path(__file__).parent.parent.parent
|
||||
config_path = base_path / "config" / "config.yaml"
|
||||
instance = ConfigLoader(config_path)
|
||||
return instance
|
||||
140
src/utils/logger.py
Normal file
140
src/utils/logger.py
Normal file
@ -0,0 +1,140 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from .config_loader import get_config_loader
|
||||
|
||||
|
||||
class LoggerManager:
|
||||
"""日志管理器单例类"""
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not self._initialized:
|
||||
self._base_logger = None
|
||||
self._initialized = True
|
||||
|
||||
def init_logger(self) -> logger:
|
||||
"""
|
||||
初始化日志记录器,全局唯一实例
|
||||
从配置文件中读取日志配置并设置
|
||||
"""
|
||||
if self._base_logger is not None:
|
||||
return self._base_logger
|
||||
|
||||
try:
|
||||
# 获取配置加载器
|
||||
config_loader = get_config_loader()
|
||||
log_config = config_loader.get_log_config()
|
||||
|
||||
# 移除默认处理器
|
||||
logger.remove()
|
||||
|
||||
# 获取配置参数
|
||||
console_format = log_config.log_format
|
||||
file_format = log_config.log_format_file
|
||||
log_level = log_config.log_level
|
||||
log_dir = log_config.log_dir
|
||||
log_file = log_config.log_file
|
||||
rotation = log_config.rotation
|
||||
retention = log_config.retention
|
||||
compression = log_config.compression
|
||||
version = log_config.version
|
||||
|
||||
# 确保日志目录存在
|
||||
log_dir_path = Path(log_dir)
|
||||
log_dir_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 完整的日志文件路径
|
||||
log_file_path = log_dir_path / log_file
|
||||
|
||||
# 添加控制台输出处理器
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format=console_format,
|
||||
level=log_level,
|
||||
colorize=True,
|
||||
enqueue=True # 多进程安全
|
||||
)
|
||||
|
||||
# 添加文件输出处理器(带轮转)
|
||||
logger.add(
|
||||
str(log_file_path),
|
||||
format=file_format,
|
||||
level=log_level,
|
||||
rotation=rotation,
|
||||
retention=retention,
|
||||
compression=compression,
|
||||
enqueue=True, # 多进程安全
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
# 创建绑定了版本信息的基础logger
|
||||
self._base_logger = logger.bind(version=version)
|
||||
|
||||
# 记录初始化成功信息
|
||||
self._base_logger.bind(tag="SYSTEM").info("日志系统初始化成功")
|
||||
self._base_logger.bind(tag="SYSTEM").info(f"控制台日志级别: {log_level}")
|
||||
self._base_logger.bind(tag="SYSTEM").info(
|
||||
f"文件日志路径: {log_file_path}")
|
||||
self._base_logger.bind(tag="SYSTEM").info(f"日志轮转设置: {rotation}")
|
||||
self._base_logger.bind(tag="SYSTEM").info(f"日志保留设置: {retention}")
|
||||
|
||||
return self._base_logger
|
||||
|
||||
except Exception as e:
|
||||
# 如果初始化失败,使用默认配置
|
||||
logger.remove()
|
||||
logger.add(sys.stdout, level="INFO")
|
||||
fallback_logger = logger.bind(version="1.0.0")
|
||||
error_message = f"日志系统初始化失败,使用默认配置: {e}"
|
||||
fallback_logger.bind(tag="ERROR").error(error_message)
|
||||
self._base_logger = fallback_logger
|
||||
return self._base_logger
|
||||
|
||||
def get_logger(self, tag: str = "DEFAULT") -> logger:
|
||||
"""
|
||||
获取带标签的日志记录器
|
||||
|
||||
Args:
|
||||
tag: 模块标签,用于标识日志来源
|
||||
|
||||
Returns:
|
||||
绑定了标签的logger实例
|
||||
"""
|
||||
base_logger = self.init_logger()
|
||||
return base_logger.bind(tag=tag)
|
||||
|
||||
def reset_logger(self):
|
||||
"""重置日志记录器(主要用于测试)"""
|
||||
self._base_logger = None
|
||||
logger.remove()
|
||||
|
||||
|
||||
# 全局函数 保持向后兼容
|
||||
def get_logger(tag: str = "DEFAULT") -> logger:
|
||||
"""
|
||||
获取带标签的日志记录器
|
||||
|
||||
Args:
|
||||
tag: 模块标签,用于标识日志来源
|
||||
|
||||
Returns:
|
||||
绑定了标签的logger实例
|
||||
"""
|
||||
manager = LoggerManager()
|
||||
return manager.get_logger(tag)
|
||||
|
||||
|
||||
def reset_logger():
|
||||
"""重置日志记录器(主要用于测试)"""
|
||||
manager = LoggerManager()
|
||||
manager.reset_logger()
|
||||
1
tools/__init__.py
Normal file
1
tools/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tools package for FlowDiary Backend"""
|
||||
154
tools/fullwidth_to_halfwidth_converter.py
Normal file
154
tools/fullwidth_to_halfwidth_converter.py
Normal file
@ -0,0 +1,154 @@
|
||||
"""
|
||||
全角字符转半角字符工具脚本
|
||||
用于将Python文件中的全角括号、逗号等字符转换为半角
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def fullwidth_to_halfwidth(text: str) -> str:
|
||||
"""
|
||||
将文本中的全角字符转换为半角字符
|
||||
|
||||
Args:
|
||||
text: 输入文本
|
||||
|
||||
Returns:
|
||||
转换后的文本
|
||||
"""
|
||||
# 定义全角到半角的映射
|
||||
fullwidth_chars = {
|
||||
"(": "(",
|
||||
")": ")",
|
||||
",": ",",
|
||||
";": ";",
|
||||
":": ":",
|
||||
"。": ".",
|
||||
"?": "?",
|
||||
"!": "!",
|
||||
""": '"',
|
||||
"'": "'",
|
||||
"[": "[",
|
||||
"]": "]",
|
||||
"{": "{",
|
||||
"}": "}",
|
||||
" ": " ", # 全角空格
|
||||
}
|
||||
|
||||
# 处理每个字符
|
||||
result = []
|
||||
for char in text:
|
||||
result.append(fullwidth_chars.get(char, char))
|
||||
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def convert_file(file_path: Path) -> bool:
|
||||
"""
|
||||
转换单个文件中的全角字符
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
|
||||
Returns:
|
||||
如果文件被修改则返回True,否则返回False
|
||||
"""
|
||||
if not file_path.exists():
|
||||
print(f"文件不存在: {file_path}")
|
||||
return False
|
||||
|
||||
if file_path.suffix != ".py":
|
||||
print(f"跳过非Python文件: {file_path}")
|
||||
return False
|
||||
|
||||
# 忽略自身文件,防止无限循环
|
||||
if file_path.name == "fullwidth_to_halfwidth_converter.py":
|
||||
print(f"跳过自身文件: {file_path}")
|
||||
return False
|
||||
|
||||
# 读取原始内容
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
original_content = f.read()
|
||||
|
||||
# 转换全角字符
|
||||
converted_content = fullwidth_to_halfwidth(original_content)
|
||||
|
||||
# 如果内容发生了变化,则写回文件
|
||||
if original_content != converted_content:
|
||||
print(f"转换文件: {file_path}")
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(converted_content)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def convert_files_in_directory(directory: Path, pattern: str = "*.py") -> int:
|
||||
"""
|
||||
转换目录下所有匹配模式的文件
|
||||
|
||||
Args:
|
||||
directory: 目录路径
|
||||
pattern: 文件匹配模式
|
||||
|
||||
Returns:
|
||||
修改的文件数量
|
||||
"""
|
||||
if not directory.exists():
|
||||
print(f"目录不存在: {directory}")
|
||||
return 0
|
||||
|
||||
python_files = list(directory.rglob(pattern))
|
||||
modified_count = 0
|
||||
|
||||
for file_path in python_files:
|
||||
if convert_file(file_path):
|
||||
modified_count += 1
|
||||
|
||||
return modified_count
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数,处理命令行参数"""
|
||||
if len(sys.argv) < 2:
|
||||
print("使用示例:")
|
||||
print(" 处理单个文件: python fullwidth_to_halfwidth_converter.py ./main.py")
|
||||
print(" 处理多个文件: python fullwidth_to_halfwidth_converter.py "
|
||||
"file1.py file2.py file3.py")
|
||||
print(" 处理整个目录: python fullwidth_to_halfwidth_converter.py ./src")
|
||||
sys.exit(1)
|
||||
|
||||
modified_count = 0
|
||||
total_files = 0
|
||||
|
||||
# 处理所有传入的路径参数(支持多文件和目录)
|
||||
for path_arg in sys.argv[1:]:
|
||||
target_path = Path(path_arg)
|
||||
|
||||
if target_path.is_file():
|
||||
# 处理单个文件
|
||||
total_files += 1
|
||||
if convert_file(target_path):
|
||||
modified_count += 1
|
||||
elif target_path.is_dir():
|
||||
# 处理目录下的所有Python文件
|
||||
pattern = "*.py"
|
||||
dir_modified = convert_files_in_directory(target_path, pattern)
|
||||
modified_count += dir_modified
|
||||
# 统计目录中的文件数
|
||||
total_files += len(list(target_path.rglob(pattern)))
|
||||
else:
|
||||
print(f"⚠️ 路径不存在: {target_path}")
|
||||
|
||||
# 输出处理结果
|
||||
if total_files > 0:
|
||||
print(f"✅ 处理完成: 共检查 {total_files} 个文件,转换了 {modified_count} 个文件")
|
||||
else:
|
||||
print("⚠️ 没有找到需要处理的文件")
|
||||
|
||||
# 如果有文件被修改,返回非零状态码,通知pre-commit需要重新暂存
|
||||
sys.exit(1 if modified_count > 0 else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user