first init

This commit is contained in:
DongShengWu 2026-01-01 17:48:45 +08:00
commit cc74e2b880
20 changed files with 3011 additions and 0 deletions

14
.gitattributes vendored Normal file
View 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
View 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
View 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
View File

@ -0,0 +1 @@
3.12

201
LICENSE Normal file
View 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
View 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
View 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: "你现在是心镜,根据主人的皮肤状态以及心情来安慰用户以及对话! "

View 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()

View 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
View File

@ -0,0 +1,6 @@
def main():
print("Hello from Heart_Mirror_Agent!")
if __name__ == "__main__":
main()

34
pyproject.toml Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
"""
配置数据模型包
"""

View 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"

View 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
View 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()

0
test.py Normal file
View File

1
tools/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Tools package for FlowDiary Backend"""

View 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()

2036
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff