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