“提交项目”
This commit is contained in:
438
hertz_studio_django_utils/README.md
Normal file
438
hertz_studio_django_utils/README.md
Normal file
@@ -0,0 +1,438 @@
|
||||
# Hertz Studio Django Utils 模块
|
||||
|
||||
## 概述
|
||||
|
||||
Hertz Studio Django Utils 是一个功能丰富的工具类模块,为 Hertz Server Django 项目提供核心的工具函数、响应格式、验证器和加密服务。该模块采用模块化设计,便于维护和扩展。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🔐 **加密工具**: 提供多种加密算法和密码哈希功能
|
||||
- 📧 **邮件服务**: 统一的邮件发送接口和验证码邮件模板
|
||||
- 📋 **响应格式**: 标准化的API响应格式和错误处理
|
||||
- ✅ **数据验证**: 邮箱、密码、手机号等格式验证
|
||||
- 🧩 **模块化设计**: 各功能模块独立,便于按需使用
|
||||
|
||||
## 模块结构
|
||||
|
||||
```
|
||||
hertz_studio_django_utils/
|
||||
├── __init__.py # 模块初始化
|
||||
├── crypto/ # 加密工具
|
||||
│ ├── __init__.py
|
||||
│ ├── encryption_utils.py # 通用加密工具
|
||||
│ └── password_hashers.py # 密码哈希器
|
||||
├── email/ # 邮件服务
|
||||
│ ├── __init__.py
|
||||
│ └── email_service.py # 邮件发送服务
|
||||
├── responses/ # 响应格式
|
||||
│ ├── __init__.py
|
||||
│ └── HertzResponse.py # 统一响应类
|
||||
└── validators/ # 验证器
|
||||
├── __init__.py
|
||||
├── email_validator.py # 邮箱验证器
|
||||
├── password_validator.py # 密码验证器
|
||||
└── phone_validator.py # 手机号验证器
|
||||
```
|
||||
|
||||
## 核心类说明
|
||||
|
||||
### 1. EncryptionUtils 加密工具类
|
||||
|
||||
提供多种加密算法和工具函数:
|
||||
|
||||
```python
|
||||
from hertz_studio_django_utils.crypto import EncryptionUtils
|
||||
|
||||
# MD5哈希
|
||||
hash_value = EncryptionUtils.md5_hash("password", "salt")
|
||||
|
||||
# SHA256哈希
|
||||
hash_value = EncryptionUtils.sha256_hash("password", "salt")
|
||||
|
||||
# 数据加密解密
|
||||
encrypted = EncryptionUtils.encrypt_data("敏感数据", "password")
|
||||
decrypted = EncryptionUtils.decrypt_data(encrypted, "password")
|
||||
|
||||
# 生成随机盐值
|
||||
salt = EncryptionUtils.generate_salt(32)
|
||||
```
|
||||
|
||||
### 2. MD5PasswordHasher MD5密码哈希器
|
||||
|
||||
兼容旧系统的MD5密码加密:
|
||||
|
||||
```python
|
||||
from hertz_studio_django_utils.crypto import MD5PasswordHasher
|
||||
|
||||
hasher = MD5PasswordHasher()
|
||||
encoded_password = hasher.encode("password", "salt")
|
||||
is_valid = hasher.verify("password", encoded_password)
|
||||
```
|
||||
|
||||
### 3. EmailService 邮件服务类
|
||||
|
||||
提供邮件发送功能:
|
||||
|
||||
```python
|
||||
from hertz_studio_django_utils.email import EmailService
|
||||
|
||||
# 发送普通邮件
|
||||
success = EmailService.send_email(
|
||||
recipient_email="user@example.com",
|
||||
subject="邮件主题",
|
||||
html_content="<h1>HTML内容</h1>",
|
||||
text_content="纯文本内容"
|
||||
)
|
||||
|
||||
# 发送验证码邮件
|
||||
success = EmailService.send_verification_code(
|
||||
recipient_email="user@example.com",
|
||||
recipient_name="用户名",
|
||||
verification_code="123456",
|
||||
code_type="register"
|
||||
)
|
||||
```
|
||||
|
||||
### 4. HertzResponse 统一响应类
|
||||
|
||||
标准化的API响应格式:
|
||||
|
||||
```python
|
||||
from hertz_studio_django_utils.responses import HertzResponse
|
||||
|
||||
# 成功响应
|
||||
return HertzResponse.success(data={"user": user_data}, message="操作成功")
|
||||
|
||||
# 失败响应
|
||||
return HertzResponse.fail(message="操作失败", data={"error": "详情"})
|
||||
|
||||
# 错误响应
|
||||
return HertzResponse.error(message="系统错误", error=str(e))
|
||||
|
||||
# 验证错误
|
||||
return HertzResponse.validation_error(message="参数错误", errors=serializer.errors)
|
||||
|
||||
# 自定义响应
|
||||
return HertzResponse.custom(
|
||||
success=True,
|
||||
message="自定义消息",
|
||||
data={"custom": "data"},
|
||||
code=200
|
||||
)
|
||||
```
|
||||
|
||||
### 5. 验证器类
|
||||
|
||||
提供数据格式验证功能:
|
||||
|
||||
```python
|
||||
from hertz_studio_django_utils.validators import (
|
||||
EmailValidator, PasswordValidator, PhoneValidator
|
||||
)
|
||||
|
||||
# 邮箱验证
|
||||
is_valid, message = EmailValidator.validate_email("test@example.com")
|
||||
normalized_email = EmailValidator.normalize_email(" Test@Example.COM ")
|
||||
|
||||
# 密码强度验证
|
||||
is_valid, errors = PasswordValidator.validate_password_strength("Password123!")
|
||||
score = PasswordValidator.calculate_password_score("Password123!")
|
||||
level = PasswordValidator.get_password_strength_level("Password123!")
|
||||
|
||||
# 手机号验证
|
||||
is_valid = PhoneValidator.is_valid_china_mobile("13800138000")
|
||||
is_valid, message = PhoneValidator.validate_china_mobile("13800138000")
|
||||
carrier = PhoneValidator.get_mobile_carrier("13800138000")
|
||||
```
|
||||
|
||||
## 安装和配置
|
||||
|
||||
### 1. 依赖安装
|
||||
|
||||
确保已安装以下依赖:
|
||||
|
||||
```bash
|
||||
pip install Django>=5.2.6
|
||||
pip install cryptography>=41.0.0
|
||||
```
|
||||
|
||||
### 2. 配置邮件服务
|
||||
|
||||
在 `settings.py` 中配置邮件服务:
|
||||
|
||||
```python
|
||||
# 邮件配置
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = 'smtp.example.com'
|
||||
EMAIL_PORT = 587
|
||||
EMAIL_USE_TLS = True
|
||||
EMAIL_HOST_USER = 'your-email@example.com'
|
||||
EMAIL_HOST_PASSWORD = 'your-password'
|
||||
DEFAULT_FROM_EMAIL = 'noreply@example.com'
|
||||
```
|
||||
|
||||
### 3. 配置密码哈希器
|
||||
|
||||
在 `settings.py` 中配置密码哈希器:
|
||||
|
||||
```python
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||
'hertz_studio_django_utils.crypto.password_hashers.MD5PasswordHasher', # MD5兼容
|
||||
]
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 导入模块
|
||||
|
||||
```python
|
||||
# 导入整个工具模块
|
||||
from hertz_studio_django_utils import (
|
||||
EncryptionUtils,
|
||||
MD5PasswordHasher,
|
||||
EmailService,
|
||||
HertzResponse,
|
||||
EmailValidator,
|
||||
PasswordValidator,
|
||||
PhoneValidator
|
||||
)
|
||||
|
||||
# 或按需导入特定功能
|
||||
from hertz_studio_django_utils.responses import HertzResponse
|
||||
from hertz_studio_django_utils.email import EmailService
|
||||
```
|
||||
|
||||
### 2. 使用示例
|
||||
|
||||
```python
|
||||
# 在Django视图中使用统一响应
|
||||
from rest_framework.decorators import api_view
|
||||
from hertz_studio_django_utils.responses import HertzResponse
|
||||
|
||||
@api_view(['GET'])
|
||||
def user_profile(request):
|
||||
try:
|
||||
user_data = {"username": "testuser", "email": "test@example.com"}
|
||||
return HertzResponse.success(data=user_data, message="获取用户信息成功")
|
||||
except Exception as e:
|
||||
return HertzResponse.error(message="获取用户信息失败", error=str(e))
|
||||
|
||||
# 发送验证码邮件
|
||||
from hertz_studio_django_utils.email import EmailService
|
||||
|
||||
success = EmailService.send_verification_code(
|
||||
recipient_email="user@example.com",
|
||||
recipient_name="张三",
|
||||
verification_code="654321",
|
||||
code_type="register"
|
||||
)
|
||||
|
||||
if success:
|
||||
print("验证码邮件发送成功")
|
||||
else:
|
||||
print("验证码邮件发送失败")
|
||||
```
|
||||
|
||||
## API接口
|
||||
|
||||
### 加密工具 API
|
||||
|
||||
| 方法 | 描述 | 参数 | 返回值 |
|
||||
|------|------|------|--------|
|
||||
| `md5_hash(data, salt)` | MD5哈希加密 | data: str, salt: str | str |
|
||||
| `sha256_hash(data, salt)` | SHA256哈希加密 | data: str, salt: str | str |
|
||||
| `encrypt_data(data, password)` | 加密数据 | data: str, password: str | Optional[str] |
|
||||
| `decrypt_data(encrypted_data, password)` | 解密数据 | encrypted_data: str, password: str | Optional[str] |
|
||||
| `generate_salt(length)` | 生成随机盐值 | length: int | str |
|
||||
|
||||
### 邮件服务 API
|
||||
|
||||
| 方法 | 描述 | 参数 | 返回值 |
|
||||
|------|------|------|--------|
|
||||
| `send_email()` | 发送邮件 | recipient_email, subject, html_content, text_content, from_email | bool |
|
||||
| `send_verification_code()` | 发送验证码邮件 | recipient_email, recipient_name, verification_code, code_type | bool |
|
||||
|
||||
### 响应格式 API
|
||||
|
||||
| 方法 | 描述 | 参数 | 返回值 |
|
||||
|------|------|------|--------|
|
||||
| `success()` | 成功响应 | data, message, code | JsonResponse |
|
||||
| `fail()` | 失败响应 | message, data, code | JsonResponse |
|
||||
| `error()` | 错误响应 | message, error, code | JsonResponse |
|
||||
| `unauthorized()` | 未授权响应 | message, code | JsonResponse |
|
||||
| `validation_error()` | 验证错误响应 | message, errors, code | JsonResponse |
|
||||
| `custom()` | 自定义响应 | success, message, data, code, **kwargs | JsonResponse |
|
||||
|
||||
### 验证器 API
|
||||
|
||||
| 类 | 方法 | 描述 |
|
||||
|----|------|------|
|
||||
| `EmailValidator` | `is_valid_email()` | 验证邮箱格式 |
|
||||
| | `validate_email()` | 验证邮箱并返回详细信息 |
|
||||
| | `normalize_email()` | 标准化邮箱地址 |
|
||||
| `PasswordValidator` | `validate_password_strength()` | 验证密码强度 |
|
||||
| | `calculate_password_score()` | 计算密码强度分数 |
|
||||
| | `get_password_strength_level()` | 获取密码强度等级 |
|
||||
| `PhoneValidator` | `is_valid_china_mobile()` | 验证中国大陆手机号 |
|
||||
| | `validate_china_mobile()` | 验证手机号并返回详细信息 |
|
||||
| | `normalize_phone()` | 标准化手机号 |
|
||||
|
||||
## 配置参数
|
||||
|
||||
### 邮件服务配置
|
||||
|
||||
| 参数 | 默认值 | 描述 |
|
||||
|------|--------|------|
|
||||
| `EMAIL_HOST` | - | SMTP服务器地址 |
|
||||
| `EMAIL_PORT` | 587 | SMTP端口 |
|
||||
| `EMAIL_USE_TLS` | True | 使用TLS加密 |
|
||||
| `EMAIL_HOST_USER` | - | SMTP用户名 |
|
||||
| `EMAIL_HOST_PASSWORD` | - | SMTP密码 |
|
||||
| `DEFAULT_FROM_EMAIL` | - | 默认发件人邮箱 |
|
||||
|
||||
### 密码验证配置
|
||||
|
||||
| 参数 | 默认值 | 描述 |
|
||||
|------|--------|------|
|
||||
| `min_length` | 8 | 密码最小长度 |
|
||||
| `max_length` | 128 | 密码最大长度 |
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 自定义邮件模板
|
||||
|
||||
```python
|
||||
from hertz_studio_django_utils.email import EmailService
|
||||
|
||||
# 自定义邮件内容
|
||||
custom_html = """
|
||||
<html>
|
||||
<body>
|
||||
<h1>自定义邮件</h1>
|
||||
<p>您好 {name},这是一封自定义邮件。</p>
|
||||
</body>
|
||||
</html>
|
||||
""".format(name="张三")
|
||||
|
||||
success = EmailService.send_email(
|
||||
recipient_email="user@example.com",
|
||||
subject="自定义邮件",
|
||||
html_content=custom_html
|
||||
)
|
||||
```
|
||||
|
||||
### 扩展响应格式
|
||||
|
||||
```python
|
||||
from hertz_studio_django_utils.responses import HertzResponse
|
||||
|
||||
# 扩展自定义响应
|
||||
class CustomResponse(HertzResponse):
|
||||
@staticmethod
|
||||
def paginated(data, total, page, page_size, message="查询成功"):
|
||||
"""分页响应"""
|
||||
pagination = {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
return HertzResponse.success(
|
||||
data={"items": data, "pagination": pagination},
|
||||
message=message
|
||||
)
|
||||
|
||||
# 使用扩展响应
|
||||
return CustomResponse.paginated(
|
||||
data=users,
|
||||
total=100,
|
||||
page=1,
|
||||
page_size=20,
|
||||
message="用户列表查询成功"
|
||||
)
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
### 单元测试示例
|
||||
|
||||
```python
|
||||
from django.test import TestCase
|
||||
from hertz_studio_django_utils.validators import EmailValidator
|
||||
|
||||
class EmailValidatorTest(TestCase):
|
||||
def test_valid_email(self):
|
||||
"""测试有效邮箱地址"""
|
||||
is_valid, message = EmailValidator.validate_email("test@example.com")
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(message, "邮箱地址格式正确")
|
||||
|
||||
def test_invalid_email(self):
|
||||
"""测试无效邮箱地址"""
|
||||
is_valid, message = EmailValidator.validate_email("invalid-email")
|
||||
self.assertFalse(is_valid)
|
||||
self.assertEqual(message, "邮箱地址格式不正确")
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
python manage.py test hertz_studio_django_utils.tests
|
||||
```
|
||||
|
||||
## 安全考虑
|
||||
|
||||
1. **密码安全**: 使用强密码哈希算法,避免明文存储密码
|
||||
2. **加密安全**: 使用安全的加密算法和随机盐值
|
||||
3. **输入验证**: 对所有输入数据进行严格验证
|
||||
4. **错误处理**: 避免泄露敏感错误信息
|
||||
5. **邮件安全**: 使用TLS加密邮件传输
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 邮件发送失败怎么办?
|
||||
A: 检查邮件配置是否正确,包括SMTP服务器、端口、用户名和密码。
|
||||
|
||||
### Q: MD5密码哈希是否安全?
|
||||
A: MD5被认为是不安全的哈希算法,仅用于兼容旧系统。新系统应使用更安全的算法如bcrypt或Argon2。
|
||||
|
||||
### Q: 如何自定义响应格式?
|
||||
A: 可以继承 `HertzResponse` 类并添加自定义方法。
|
||||
|
||||
### Q: 验证器是否支持国际手机号?
|
||||
A: 目前主要支持中国大陆手机号验证,国际手机号验证功能有限。
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-01)
|
||||
- 初始版本发布
|
||||
- 包含加密工具、邮件服务、响应格式、验证器等核心功能
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [🏠 返回主项目](../README.md) - Hertz Server Django 主项目
|
||||
- [🔐 认证授权模块](../hertz_studio_django_auth/README.md) - 用户管理和权限控制
|
||||
- [🖼️ 验证码模块](../hertz_studio_django_captcha/README.md) - 图片和邮箱验证码功能
|
||||
- [📋 代码风格指南](../CODING_STYLE_GUIDE.md) - 开发规范和最佳实践
|
||||
- [Django 官方文档](https://docs.djangoproject.com/)
|
||||
- [Django REST Framework 文档](https://www.django-rest-framework.org/)
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. Fork 本项目
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 打开 Pull Request
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
## 支持
|
||||
|
||||
如有问题或建议,请提交 [Issue](https://github.com/your-org/hertz-server-django/issues) 或联系开发团队。
|
||||
1
hertz_studio_django_utils/__init__.py
Normal file
1
hertz_studio_django_utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Hertz Server Django Utils Package
|
||||
39
hertz_studio_django_utils/code_generator/__init__.py
Normal file
39
hertz_studio_django_utils/code_generator/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Django代码生成器模块
|
||||
|
||||
该模块提供了Django应用代码自动生成功能,包括:
|
||||
- 模型(ORM)代码生成
|
||||
- 序列化器代码生成
|
||||
- 视图(CRUD)代码生成
|
||||
- URL路由代码生成
|
||||
|
||||
使用示例:
|
||||
from hertz_studio_django_utils.code_generator import DjangoCodeGenerator
|
||||
|
||||
generator = DjangoCodeGenerator()
|
||||
generator.generate_full_module(
|
||||
model_name='User',
|
||||
fields=[
|
||||
{'name': 'username', 'type': 'CharField', 'max_length': 150},
|
||||
{'name': 'email', 'type': 'EmailField'}
|
||||
]
|
||||
)
|
||||
"""
|
||||
|
||||
from .base_generator import BaseGenerator
|
||||
from .django_code_generator import DjangoCodeGenerator
|
||||
from .model_generator import ModelGenerator
|
||||
from .serializer_generator import SerializerGenerator
|
||||
from .view_generator import ViewGenerator
|
||||
from .url_generator import URLGenerator
|
||||
from .menu_generator import MenuGenerator
|
||||
|
||||
__all__ = [
|
||||
'BaseGenerator',
|
||||
'DjangoCodeGenerator',
|
||||
'ModelGenerator',
|
||||
'SerializerGenerator',
|
||||
'ViewGenerator',
|
||||
'URLGenerator',
|
||||
'MenuGenerator'
|
||||
]
|
||||
286
hertz_studio_django_utils/code_generator/app_generator.py
Normal file
286
hertz_studio_django_utils/code_generator/app_generator.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
Django应用代码生成器
|
||||
|
||||
该模块负责根据配置生成完整的Django应用代码
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
from .base_generator import BaseGenerator
|
||||
from .model_generator import ModelGenerator
|
||||
from .serializer_generator import SerializerGenerator
|
||||
from .view_generator import ViewGenerator
|
||||
from .url_generator import URLGenerator
|
||||
|
||||
|
||||
class AppGenerator(BaseGenerator):
|
||||
"""Django应用代码生成器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化应用生成器"""
|
||||
super().__init__()
|
||||
self.model_generator = ModelGenerator()
|
||||
self.serializer_generator = SerializerGenerator()
|
||||
self.view_generator = ViewGenerator()
|
||||
self.url_generator = URLGenerator()
|
||||
|
||||
def generate(self, app_name: str = None, models: List[Dict[str, Any]] = None, **kwargs) -> Dict[str, str]:
|
||||
"""
|
||||
生成完整的Django应用代码
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
models: 模型配置列表
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 生成的文件代码映射
|
||||
"""
|
||||
# 处理参数
|
||||
if app_name is None:
|
||||
app_name = kwargs.get('app_name', 'default_app')
|
||||
if models is None:
|
||||
models = kwargs.get('models', [])
|
||||
|
||||
api_version = kwargs.get('api_version', 'v1')
|
||||
include_admin = kwargs.get('include_admin', True)
|
||||
include_tests = kwargs.get('include_tests', True)
|
||||
|
||||
generated_files = {}
|
||||
|
||||
# 生成应用配置文件
|
||||
generated_files['apps.py'] = self.generate_apps_config(app_name)
|
||||
|
||||
# 生成__init__.py文件
|
||||
generated_files['__init__.py'] = self.generate_init_file(app_name)
|
||||
|
||||
# 生成模型、序列化器、视图和URL
|
||||
all_models = []
|
||||
for model_config in models:
|
||||
model_name = model_config.get('name', 'DefaultModel')
|
||||
fields = model_config.get('fields', [])
|
||||
operations = model_config.get('operations', ['list', 'create', 'retrieve', 'update', 'delete'])
|
||||
|
||||
# 生成模型代码
|
||||
model_code = self.model_generator.generate(
|
||||
model_name=model_name,
|
||||
fields=fields,
|
||||
**model_config
|
||||
)
|
||||
|
||||
# 生成序列化器代码
|
||||
serializer_code = self.serializer_generator.generate(
|
||||
model_name=model_name,
|
||||
fields=fields
|
||||
)
|
||||
|
||||
# 生成视图代码
|
||||
view_code = self.view_generator.generate(
|
||||
model_name=model_name,
|
||||
operations=operations
|
||||
)
|
||||
|
||||
# 生成URL代码
|
||||
url_code = self.url_generator.generate(
|
||||
model_name=model_name,
|
||||
operations=operations,
|
||||
app_name=app_name
|
||||
)
|
||||
|
||||
# 添加到生成的文件中
|
||||
generated_files[f'models/{model_name.lower()}_models.py'] = model_code
|
||||
generated_files[f'serializers/{model_name.lower()}_serializers.py'] = serializer_code
|
||||
generated_files[f'views/{model_name.lower()}_views.py'] = view_code
|
||||
generated_files[f'urls/{model_name.lower()}_urls.py'] = url_code
|
||||
|
||||
all_models.append(model_name)
|
||||
|
||||
# 生成主要文件
|
||||
generated_files['models.py'] = self.generate_models_init(all_models)
|
||||
generated_files['serializers/__init__.py'] = self.generate_serializers_init(all_models)
|
||||
generated_files['views/__init__.py'] = self.generate_views_init(all_models)
|
||||
generated_files['urls.py'] = self.generate_main_urls(app_name, all_models, api_version)
|
||||
|
||||
# 生成管理后台文件
|
||||
if include_admin:
|
||||
generated_files['admin.py'] = self.generate_admin_config(all_models)
|
||||
|
||||
# 生成测试文件
|
||||
if include_tests:
|
||||
generated_files['tests.py'] = self.generate_tests(all_models)
|
||||
|
||||
return generated_files
|
||||
|
||||
def generate_apps_config(self, app_name: str) -> str:
|
||||
"""生成应用配置文件"""
|
||||
context = {
|
||||
'app_name': app_name,
|
||||
'app_label': app_name.replace('hertz_studio_django_', ''),
|
||||
'verbose_name': app_name.replace('_', ' ').title()
|
||||
}
|
||||
return self.render_template('django/apps.mako', context)
|
||||
|
||||
def generate_init_file(self, app_name: str) -> str:
|
||||
"""生成__init__.py文件"""
|
||||
return f'"""\n{app_name} Django应用\n"""\n\ndefault_app_config = \'{app_name}.apps.{app_name.title().replace("_", "")}Config\'\n'
|
||||
|
||||
def generate_models_init(self, models: List[str]) -> str:
|
||||
"""生成models.py主文件"""
|
||||
imports = []
|
||||
for model in models:
|
||||
imports.append(f'from .models.{model.lower()}_models import {model}')
|
||||
|
||||
context = {
|
||||
'imports': imports,
|
||||
'models': models
|
||||
}
|
||||
return self.render_template('django/models_init.mako', context)
|
||||
|
||||
def generate_serializers_init(self, models: List[str]) -> str:
|
||||
"""生成serializers/__init__.py文件"""
|
||||
imports = []
|
||||
for model in models:
|
||||
imports.append(f'from .{model.lower()}_serializers import {model}Serializer')
|
||||
|
||||
return '\n'.join(imports) + '\n'
|
||||
|
||||
def generate_views_init(self, models: List[str]) -> str:
|
||||
"""生成views/__init__.py文件"""
|
||||
imports = []
|
||||
for model in models:
|
||||
imports.append(f'from .{model.lower()}_views import {model}ViewSet')
|
||||
|
||||
return '\n'.join(imports) + '\n'
|
||||
|
||||
def generate_main_urls(self, app_name: str, models: List[str], api_version: str) -> str:
|
||||
"""生成主URL配置文件"""
|
||||
context = {
|
||||
'app_name': app_name,
|
||||
'models': models,
|
||||
'api_version': api_version,
|
||||
'url_includes': [f'path(\'{model.lower()}/\', include(\'{app_name}.urls.{model.lower()}_urls\'))' for model in models]
|
||||
}
|
||||
return self.render_template('django/main_urls.mako', context)
|
||||
|
||||
def generate_admin_config(self, models: List[str]) -> str:
|
||||
"""生成管理后台配置"""
|
||||
context = {
|
||||
'models': models
|
||||
}
|
||||
return self.render_template('django/admin.mako', context)
|
||||
|
||||
def generate_tests(self, models: List[str]) -> str:
|
||||
"""生成测试文件"""
|
||||
context = {
|
||||
'models': models
|
||||
}
|
||||
return self.render_template('django/tests.mako', context)
|
||||
|
||||
def generate_full_app(
|
||||
self,
|
||||
app_name: str,
|
||||
models: List[Dict[str, Any]],
|
||||
output_dir: str = None,
|
||||
**kwargs
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
生成完整的Django应用并写入文件
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
models: 模型配置列表
|
||||
output_dir: 输出目录
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 生成的文件路径映射
|
||||
"""
|
||||
generated_files = self.generate(app_name, models, **kwargs)
|
||||
|
||||
if output_dir:
|
||||
file_paths = {}
|
||||
for file_name, content in generated_files.items():
|
||||
file_path = os.path.join(output_dir, app_name, file_name)
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
file_paths[file_name] = file_path
|
||||
|
||||
return file_paths
|
||||
|
||||
return generated_files
|
||||
|
||||
def generate_django_project(
|
||||
self,
|
||||
project_name: str,
|
||||
apps: List[Dict[str, Any]],
|
||||
output_dir: str = None,
|
||||
**kwargs
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
生成完整的Django项目
|
||||
|
||||
Args:
|
||||
project_name: 项目名称
|
||||
apps: 应用配置列表
|
||||
output_dir: 输出目录
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 生成的文件路径映射
|
||||
"""
|
||||
generated_files = {}
|
||||
|
||||
# 生成项目配置文件
|
||||
generated_files['manage.py'] = self.generate_manage_py(project_name)
|
||||
generated_files[f'{project_name}/settings.py'] = self.generate_settings(project_name, apps)
|
||||
generated_files[f'{project_name}/urls.py'] = self.generate_project_urls(project_name, apps)
|
||||
generated_files[f'{project_name}/wsgi.py'] = self.generate_wsgi(project_name)
|
||||
generated_files[f'{project_name}/asgi.py'] = self.generate_asgi(project_name)
|
||||
generated_files[f'{project_name}/__init__.py'] = ''
|
||||
|
||||
# 生成每个应用
|
||||
for app_config in apps:
|
||||
app_name = app_config.get('name', 'default_app')
|
||||
models = app_config.get('models', [])
|
||||
|
||||
app_files = self.generate(app_name, models, **app_config)
|
||||
for file_name, content in app_files.items():
|
||||
generated_files[f'{app_name}/{file_name}'] = content
|
||||
|
||||
return generated_files
|
||||
|
||||
def generate_manage_py(self, project_name: str) -> str:
|
||||
"""生成manage.py文件"""
|
||||
context = {'project_name': project_name}
|
||||
return self.render_template('django/manage.mako', context)
|
||||
|
||||
def generate_settings(self, project_name: str, apps: List[Dict[str, Any]]) -> str:
|
||||
"""生成settings.py文件"""
|
||||
app_names = [app.get('name', 'default_app') for app in apps]
|
||||
context = {
|
||||
'project_name': project_name,
|
||||
'apps': app_names
|
||||
}
|
||||
return self.render_template('django/settings.mako', context)
|
||||
|
||||
def generate_project_urls(self, project_name: str, apps: List[Dict[str, Any]]) -> str:
|
||||
"""生成项目主URL配置"""
|
||||
app_names = [app.get('name', 'default_app') for app in apps]
|
||||
context = {
|
||||
'project_name': project_name,
|
||||
'apps': app_names
|
||||
}
|
||||
return self.render_template('django/project_urls.mako', context)
|
||||
|
||||
def generate_wsgi(self, project_name: str) -> str:
|
||||
"""生成WSGI配置"""
|
||||
context = {'project_name': project_name}
|
||||
return self.render_template('django/wsgi.mako', context)
|
||||
|
||||
def generate_asgi(self, project_name: str) -> str:
|
||||
"""生成ASGI配置"""
|
||||
context = {'project_name': project_name}
|
||||
return self.render_template('django/asgi.mako', context)
|
||||
262
hertz_studio_django_utils/code_generator/base_generator.py
Normal file
262
hertz_studio_django_utils/code_generator/base_generator.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
基础代码生成器类
|
||||
|
||||
提供代码生成的基础功能和通用方法
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional
|
||||
from abc import ABC, abstractmethod
|
||||
from .template_engine import TemplateEngine
|
||||
|
||||
|
||||
class BaseGenerator(ABC):
|
||||
"""
|
||||
基础代码生成器抽象类
|
||||
"""
|
||||
|
||||
def __init__(self, template_dir: str = None):
|
||||
"""
|
||||
初始化基础生成器
|
||||
|
||||
Args:
|
||||
template_dir: 模板目录路径
|
||||
"""
|
||||
self.template_vars = {}
|
||||
self.template_engine = TemplateEngine(template_dir)
|
||||
|
||||
def render_template(self, template_name: str, context: Dict[str, Any] = None) -> str:
|
||||
"""
|
||||
渲染模板
|
||||
|
||||
Args:
|
||||
template_name: 模板文件名
|
||||
context: 模板上下文变量
|
||||
|
||||
Returns:
|
||||
str: 渲染后的代码字符串
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
# 合并模板变量和上下文
|
||||
merged_context = {**self.template_vars, **context}
|
||||
|
||||
return self.template_engine.render_template(template_name, merged_context)
|
||||
|
||||
def create_template_context(self, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
创建模板上下文
|
||||
|
||||
Args:
|
||||
**kwargs: 上下文变量
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 模板上下文
|
||||
"""
|
||||
context = {**self.template_vars, **kwargs}
|
||||
|
||||
# 添加常用的辅助函数到上下文
|
||||
context.update({
|
||||
'snake_to_camel': self.snake_to_camel,
|
||||
'camel_to_snake': self.camel_to_snake,
|
||||
'format_field_name': self.format_field_name,
|
||||
'format_class_name': self.format_class_name,
|
||||
'format_verbose_name': self.format_verbose_name,
|
||||
'get_django_field_type': self.get_django_field_type,
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
@abstractmethod
|
||||
def generate(self, **kwargs) -> str:
|
||||
"""
|
||||
生成代码的抽象方法
|
||||
|
||||
Returns:
|
||||
str: 生成的代码字符串
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_template_var(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
设置模板变量
|
||||
|
||||
Args:
|
||||
key: 变量名
|
||||
value: 变量值
|
||||
"""
|
||||
self.template_vars[key] = value
|
||||
|
||||
def get_template_var(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
获取模板变量
|
||||
|
||||
Args:
|
||||
key: 变量名
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
Any: 变量值
|
||||
"""
|
||||
return self.template_vars.get(key, default)
|
||||
|
||||
def snake_to_camel(self, snake_str: str) -> str:
|
||||
"""
|
||||
将蛇形命名转换为驼峰命名
|
||||
|
||||
Args:
|
||||
snake_str: 蛇形命名字符串
|
||||
|
||||
Returns:
|
||||
str: 驼峰命名字符串
|
||||
"""
|
||||
components = snake_str.split('_')
|
||||
return ''.join(word.capitalize() for word in components)
|
||||
|
||||
def camel_to_snake(self, camel_str: str) -> str:
|
||||
"""
|
||||
将驼峰命名转换为蛇形命名
|
||||
|
||||
Args:
|
||||
camel_str: 驼峰命名字符串
|
||||
|
||||
Returns:
|
||||
str: 蛇形命名字符串
|
||||
"""
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel_str)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
def format_field_name(self, field_name: str) -> str:
|
||||
"""
|
||||
格式化字段名称
|
||||
|
||||
Args:
|
||||
field_name: 原始字段名
|
||||
|
||||
Returns:
|
||||
str: 格式化后的字段名
|
||||
"""
|
||||
return field_name.lower().replace(' ', '_').replace('-', '_')
|
||||
|
||||
def format_class_name(self, name: str) -> str:
|
||||
"""
|
||||
格式化类名
|
||||
|
||||
Args:
|
||||
name: 原始名称
|
||||
|
||||
Returns:
|
||||
str: 格式化后的类名
|
||||
"""
|
||||
return self.snake_to_camel(self.format_field_name(name))
|
||||
|
||||
def format_verbose_name(self, field_name: str) -> str:
|
||||
"""
|
||||
格式化verbose_name
|
||||
|
||||
Args:
|
||||
field_name: 字段名
|
||||
|
||||
Returns:
|
||||
str: 格式化后的verbose_name
|
||||
"""
|
||||
return field_name.replace('_', ' ').title()
|
||||
|
||||
def generate_imports(self, imports: List[str]) -> str:
|
||||
"""
|
||||
生成导入语句
|
||||
|
||||
Args:
|
||||
imports: 导入列表
|
||||
|
||||
Returns:
|
||||
str: 导入语句字符串
|
||||
"""
|
||||
if not imports:
|
||||
return ''
|
||||
|
||||
return '\n'.join(imports) + '\n\n'
|
||||
|
||||
def indent_code(self, code: str, indent_level: int = 1) -> str:
|
||||
"""
|
||||
为代码添加缩进
|
||||
|
||||
Args:
|
||||
code: 代码字符串
|
||||
indent_level: 缩进级别
|
||||
|
||||
Returns:
|
||||
str: 缩进后的代码
|
||||
"""
|
||||
indent = ' ' * indent_level
|
||||
lines = code.split('\n')
|
||||
return '\n'.join(indent + line if line.strip() else line for line in lines)
|
||||
|
||||
def write_to_file(self, file_path: str, content: str) -> bool:
|
||||
"""
|
||||
将内容写入文件
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
content: 文件内容
|
||||
|
||||
Returns:
|
||||
bool: 是否写入成功
|
||||
"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"写入文件失败: {e}")
|
||||
return False
|
||||
|
||||
def validate_field_config(self, field_config: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
验证字段配置
|
||||
|
||||
Args:
|
||||
field_config: 字段配置字典
|
||||
|
||||
Returns:
|
||||
bool: 配置是否有效
|
||||
"""
|
||||
required_keys = ['name', 'type']
|
||||
return all(key in field_config for key in required_keys)
|
||||
|
||||
def get_django_field_type(self, field_type: str) -> str:
|
||||
"""
|
||||
获取Django字段类型
|
||||
|
||||
Args:
|
||||
field_type: 字段类型
|
||||
|
||||
Returns:
|
||||
str: Django字段类型
|
||||
"""
|
||||
field_mapping = {
|
||||
'string': 'CharField',
|
||||
'text': 'TextField',
|
||||
'integer': 'IntegerField',
|
||||
'float': 'FloatField',
|
||||
'decimal': 'DecimalField',
|
||||
'boolean': 'BooleanField',
|
||||
'date': 'DateField',
|
||||
'datetime': 'DateTimeField',
|
||||
'time': 'TimeField',
|
||||
'email': 'EmailField',
|
||||
'url': 'URLField',
|
||||
'file': 'FileField',
|
||||
'image': 'ImageField',
|
||||
'json': 'JSONField',
|
||||
'uuid': 'UUIDField',
|
||||
'foreign_key': 'ForeignKey',
|
||||
'many_to_many': 'ManyToManyField',
|
||||
'one_to_one': 'OneToOneField'
|
||||
}
|
||||
return field_mapping.get(field_type.lower(), 'CharField')
|
||||
@@ -0,0 +1,401 @@
|
||||
# Hertz Server Django 主项目配置
|
||||
|
||||
## 📋 项目概述
|
||||
|
||||
Hertz Server Django 是一个基于 Django 和 Django REST Framework 构建的现代化后端服务框架,提供认证授权、验证码、工具类等核心功能模块。主项目配置模块负责整个项目的全局配置、路由管理和基础设置。
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
- **模块化架构**: 采用微服务架构设计,各功能模块独立开发部署
|
||||
- **RESTful API**: 基于DRF构建的标准REST API接口
|
||||
- **OpenAPI 3.0文档**: 自动生成的API文档,支持Swagger UI和ReDoc
|
||||
- **多数据库支持**: 支持SQLite和MySQL,可配置Redis作为缓存和会话存储
|
||||
- **跨域支持**: 内置CORS跨域请求处理
|
||||
- **WebSocket支持**: 基于Channels的实时通信能力
|
||||
- **环境配置**: 使用python-decouple进行环境变量管理
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
hertz_server_django/ # 项目根目录
|
||||
├── hertz_server_django/ # 主项目配置模块
|
||||
│ ├── __init__.py # 包初始化文件
|
||||
│ ├── settings.py # 项目全局配置
|
||||
│ ├── urls.py # 主URL路由配置
|
||||
│ ├── asgi.py # ASGI应用配置
|
||||
│ ├── wsgi.py # WSGI应用配置
|
||||
│ └── views.py # 根视图函数
|
||||
├── hertz_demo/ # 演示模块
|
||||
├── hertz_studio_django_captcha/ # 验证码模块
|
||||
├── hertz_studio_django_auth/ # 认证授权模块
|
||||
├── hertz_studio_django_utils/ # 工具类模块
|
||||
├── manage.py # Django管理脚本
|
||||
├── requirements.txt # 项目依赖
|
||||
├── .env # 环境变量配置
|
||||
└── data/ # 数据目录(SQLite数据库等)
|
||||
```
|
||||
|
||||
## ⚙️ 核心配置文件
|
||||
|
||||
### settings.py - 项目全局配置
|
||||
|
||||
#### 基础配置
|
||||
```python
|
||||
# 项目根目录
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# 安全密钥(从环境变量读取)
|
||||
SECRET_KEY = config('SECRET_KEY', default='django-insecure-...')
|
||||
|
||||
# 调试模式
|
||||
DEBUG = config('DEBUG', default=True, cast=bool)
|
||||
|
||||
# 允许的主机
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1')
|
||||
```
|
||||
|
||||
#### 应用配置
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
# Django核心应用
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# 第三方应用
|
||||
'rest_framework', # Django REST Framework
|
||||
'corsheaders', # CORS跨域支持
|
||||
'channels', # WebSocket支持
|
||||
'drf_spectacular', # OpenAPI文档生成
|
||||
|
||||
# 本地应用模块
|
||||
'hertz_demo', # 演示模块
|
||||
'hertz_studio_django_captcha', # 验证码模块
|
||||
'hertz_studio_django_auth', # 认证授权模块
|
||||
]
|
||||
```
|
||||
|
||||
#### 数据库配置
|
||||
```python
|
||||
# 数据库切换配置
|
||||
USE_REDIS_AS_DB = config('USE_REDIS_AS_DB', default=True, cast=bool)
|
||||
|
||||
if USE_REDIS_AS_DB:
|
||||
# 开发环境使用SQLite
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'data/db.sqlite3',
|
||||
}
|
||||
}
|
||||
else:
|
||||
# 生产环境使用MySQL
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': config('DB_NAME', default='hertz_server'),
|
||||
'USER': config('DB_USER', default='root'),
|
||||
'PASSWORD': config('DB_PASSWORD', default='root'),
|
||||
'HOST': config('DB_HOST', default='localhost'),
|
||||
'PORT': config('DB_PORT', default='3306'),
|
||||
'OPTIONS': {'charset': 'utf8mb4'},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Redis缓存配置
|
||||
```python
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django_redis.cache.RedisCache',
|
||||
'LOCATION': config('REDIS_URL', default='redis://127.0.0.1:6379/0'),
|
||||
'OPTIONS': {
|
||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### DRF配置
|
||||
```python
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.AllowAny',
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 20,
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
#### OpenAPI文档配置
|
||||
```python
|
||||
SPECTACULAR_SETTINGS = {
|
||||
'TITLE': 'Hertz Server API',
|
||||
'DESCRIPTION': 'API documentation for Hertz Server Django project',
|
||||
'VERSION': '1.0.0',
|
||||
'SERVE_INCLUDE_SCHEMA': False,
|
||||
'COMPONENT_SPLIT_REQUEST': True,
|
||||
'SCHEMA_PATH_PREFIX': '/api/',
|
||||
}
|
||||
```
|
||||
|
||||
### urls.py - 主路由配置
|
||||
|
||||
```python
|
||||
from django.urls import path, include
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
# 首页路由
|
||||
path('', views.index, name='index'),
|
||||
|
||||
# Hertz Captcha路由
|
||||
path('api/captcha/', include('hertz_studio_django_captcha.urls')),
|
||||
|
||||
# Hertz Auth路由
|
||||
path('api/', include('hertz_studio_django_auth.urls')),
|
||||
|
||||
# Demo应用路由
|
||||
path('', include('hertz_demo.urls')),
|
||||
|
||||
# API文档
|
||||
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
]
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
```bash
|
||||
# 创建虚拟环境
|
||||
python -m venv venv
|
||||
|
||||
# 激活虚拟环境
|
||||
# Windows:
|
||||
venv\Scripts\activate
|
||||
# Linux/Mac:
|
||||
source venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 环境配置
|
||||
|
||||
创建 `.env` 文件:
|
||||
|
||||
```ini
|
||||
# 基础配置
|
||||
DEBUG=True
|
||||
SECRET_KEY=your-secret-key-here
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
|
||||
# 数据库配置
|
||||
USE_REDIS_AS_DB=True
|
||||
REDIS_URL=redis://127.0.0.1:6379/0
|
||||
|
||||
# MySQL配置(如果USE_REDIS_AS_DB=False)
|
||||
DB_NAME=hertz_server
|
||||
DB_USER=root
|
||||
DB_PASSWORD=root
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
```
|
||||
|
||||
### 3. 数据库初始化
|
||||
|
||||
```bash
|
||||
# 创建数据库迁移
|
||||
python manage.py makemigrations
|
||||
|
||||
# 应用数据库迁移
|
||||
python manage.py migrate
|
||||
|
||||
# 创建超级用户(可选)
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
### 4. 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 启动Django开发服务器
|
||||
python manage.py runserver
|
||||
|
||||
# 访问应用
|
||||
# 首页: http://localhost:8000/
|
||||
# API文档: http://localhost:8000/api/docs/
|
||||
# 演示页面: http://localhost:8000/demo/captcha/
|
||||
```
|
||||
|
||||
## 🔧 配置详解
|
||||
|
||||
### 环境变量管理
|
||||
|
||||
项目使用 `python-decouple` 进行环境变量管理,支持:
|
||||
- 从 `.env` 文件读取配置
|
||||
- 类型转换和默认值设置
|
||||
- 开发和生产环境分离
|
||||
|
||||
### 数据库配置策略
|
||||
|
||||
**开发环境**: 使用SQLite + Redis缓存
|
||||
- 快速启动和开发测试
|
||||
- 数据存储在SQLite文件
|
||||
- 会话和缓存使用Redis
|
||||
|
||||
**生产环境**: 使用MySQL + Redis缓存
|
||||
- 高性能数据库支持
|
||||
- 数据持久化存储
|
||||
- Redis用于缓存和会话
|
||||
|
||||
### 中间件配置
|
||||
|
||||
```python
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware', # CORS处理
|
||||
'django.middleware.security.SecurityMiddleware', # 安全中间件
|
||||
'django.contrib.sessions.middleware.SessionMiddleware', # 会话管理
|
||||
'django.middleware.common.CommonMiddleware', # 通用处理
|
||||
'django.middleware.csrf.CsrfViewMiddleware', # CSRF保护
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证
|
||||
'hertz_studio_django_auth.utils.middleware.AuthMiddleware', # 自定义认证
|
||||
'django.contrib.messages.middleware.MessageMiddleware', # 消息框架
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持保护
|
||||
]
|
||||
```
|
||||
|
||||
## 🌐 API文档访问
|
||||
|
||||
项目提供完整的OpenAPI 3.0文档:
|
||||
|
||||
- **Swagger UI**: http://localhost:8000/api/docs/
|
||||
- **ReDoc**: http://localhost:8000/api/redoc/
|
||||
- **Schema JSON**: http://localhost:8000/api/schema/
|
||||
|
||||
## 🚢 部署配置
|
||||
|
||||
### 生产环境部署
|
||||
|
||||
1. **环境变量配置**:
|
||||
```ini
|
||||
DEBUG=False
|
||||
SECRET_KEY=your-production-secret-key
|
||||
ALLOWED_HOSTS=your-domain.com,api.your-domain.com
|
||||
USE_REDIS_AS_DB=False
|
||||
```
|
||||
|
||||
2. **静态文件收集**:
|
||||
```bash
|
||||
python manage.py collectstatic
|
||||
```
|
||||
|
||||
3. **WSGI部署**:
|
||||
```python
|
||||
# 使用Gunicorn + Nginx
|
||||
# gunicorn hertz_server_django.wsgi:application
|
||||
```
|
||||
|
||||
4. **ASGI部署**:
|
||||
```python
|
||||
# 使用Daphne + Nginx
|
||||
# daphne hertz_server_django.asgi:application
|
||||
```
|
||||
|
||||
## 🔒 安全配置
|
||||
|
||||
### 生产环境安全设置
|
||||
|
||||
```python
|
||||
# 强制HTTPS
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
# CSRF保护
|
||||
CSRF_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
|
||||
# 会话安全
|
||||
SESSION_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
|
||||
# 安全头部
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
X_FRAME_OPTIONS = 'DENY'
|
||||
```
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 缓存策略
|
||||
|
||||
```python
|
||||
# 数据库查询缓存
|
||||
from django.core.cache import cache
|
||||
|
||||
# 使用缓存
|
||||
cache.set('key', 'value', timeout=300)
|
||||
value = cache.get('key')
|
||||
```
|
||||
|
||||
### 数据库优化
|
||||
|
||||
```python
|
||||
# 使用select_related减少查询
|
||||
users = User.objects.select_related('profile').filter(is_active=True)
|
||||
|
||||
# 使用prefetch_related优化多对多关系
|
||||
users = User.objects.prefetch_related('groups', 'permissions')
|
||||
```
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **Redis连接失败**: 检查Redis服务是否启动,配置是否正确
|
||||
2. **数据库迁移错误**: 删除数据库文件重新迁移,或检查MySQL连接
|
||||
3. **静态文件404**: 运行 `python manage.py collectstatic`
|
||||
4. **CORS问题**: 检查CORS配置和中间件顺序
|
||||
|
||||
### 日志配置
|
||||
|
||||
```python
|
||||
# settings.py 中添加日志配置
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [🎮 演示模块](../hertz_demo/README.md) - 功能演示和测试页面
|
||||
- [🔐 认证授权模块](../hertz_studio_django_auth/README.md) - 用户管理和权限控制
|
||||
- [📸 验证码模块](../hertz_studio_django_captcha/README.md) - 验证码生成和验证
|
||||
- [🛠️ 工具类模块](../hertz_studio_django_utils/README.md) - 加密、邮件和验证工具
|
||||
- [🐍 Django文档](https://docs.djangoproject.com/) - Django官方文档
|
||||
- [🔌 DRF文档](https://www.django-rest-framework.org/) - Django REST Framework文档
|
||||
|
||||
---
|
||||
|
||||
💡 **提示**: 此配置模块是整个项目的核心,负责协调各功能模块的协同工作。生产部署前请务必检查所有安全配置。
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
ASGI config for hertz_server_django project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
|
||||
from hertz_demo import routing
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
|
||||
|
||||
# Initialize Django ASGI application early to ensure the AppRegistry
|
||||
# is populated before importing code that may import ORM models.
|
||||
django_asgi_app = get_asgi_application()
|
||||
|
||||
# Import websocket routing
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": django_asgi_app,
|
||||
"websocket": AllowedHostsOriginValidator(
|
||||
AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
routing.websocket_urlpatterns
|
||||
)
|
||||
)
|
||||
),
|
||||
})
|
||||
@@ -0,0 +1,335 @@
|
||||
"""
|
||||
Django settings for hertz_server_django project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.2.6.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from decouple import config
|
||||
|
||||
# 修复DRF的ip_address_validators函数
|
||||
def fix_drf_ip_validators():
|
||||
"""
|
||||
修复DRF的ip_address_validators函数返回值问题
|
||||
"""
|
||||
try:
|
||||
from rest_framework import fields
|
||||
|
||||
# 保存原始函数
|
||||
original_ip_address_validators = fields.ip_address_validators
|
||||
|
||||
def fixed_ip_address_validators(protocol, unpack_ipv4):
|
||||
"""
|
||||
修复后的ip_address_validators函数,确保返回两个值
|
||||
"""
|
||||
validators = original_ip_address_validators(protocol, unpack_ipv4)
|
||||
# 如果只返回了validators,添加默认的error_message
|
||||
if isinstance(validators, list):
|
||||
return validators, 'Enter a valid IP address.'
|
||||
else:
|
||||
# 如果已经返回了两个值,直接返回
|
||||
return validators
|
||||
|
||||
# 应用猴子补丁
|
||||
fields.ip_address_validators = fixed_ip_address_validators
|
||||
|
||||
except ImportError:
|
||||
# 如果DRF未安装,忽略错误
|
||||
pass
|
||||
|
||||
# 应用修复
|
||||
fix_drf_ip_validators()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = config('SECRET_KEY', default='django-insecure-0a1bx*8!97l^4z#ml#ufn_*9ut*)zlso$*k-g^h&(2=p@^51md')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = config('DEBUG', default=True, cast=bool)
|
||||
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||
|
||||
# Database switch configuration
|
||||
USE_REDIS_AS_DB = config('USE_REDIS_AS_DB', default=True, cast=bool)
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# Third party apps
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
'channels',
|
||||
'drf_spectacular',
|
||||
|
||||
# Local apps
|
||||
'hertz_demo',
|
||||
'hertz_studio_django_captcha',
|
||||
'hertz_studio_django_auth', # 权限管理系统
|
||||
'hertz_studio_django_notice', # 通知公告模块
|
||||
'hertz_studio_django_ai',
|
||||
'hertz_studio_django_system_monitor', # 系统监测模块
|
||||
'hertz_studio_django_log', # 日志管理模块
|
||||
'hertz_studio_django_wiki', # 知识管理模块
|
||||
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'hertz_studio_django_auth.utils.middleware.AuthMiddleware', # 权限认证中间件
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'hertz_server_django.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / 'templates']
|
||||
,
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'hertz_server_django.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
if USE_REDIS_AS_DB:
|
||||
# Redis as primary database (for caching and session storage)
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'data/db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
# Use Redis for sessions
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
||||
SESSION_CACHE_ALIAS = 'default'
|
||||
|
||||
else:
|
||||
# MySQL database configuration
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': config('DB_NAME', default='hertz_server'),
|
||||
'USER': config('DB_USER', default='root'),
|
||||
'PASSWORD': config('DB_PASSWORD', default='root'),
|
||||
'HOST': config('DB_HOST', default='localhost'),
|
||||
'PORT': config('DB_PORT', default='3306'),
|
||||
'OPTIONS': {
|
||||
'charset': 'utf8mb4',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Redis
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django_redis.cache.RedisCache',
|
||||
'LOCATION': config('REDIS_URL', default='redis://127.0.0.1:6379/0'),
|
||||
'OPTIONS': {
|
||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / 'static',
|
||||
]
|
||||
|
||||
# Media files (User uploaded files)
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Django REST Framework configuration
|
||||
# 使用自定义AuthMiddleware进行认证,不使用DRF的认证和权限系统
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [], # 不使用DRF认证类
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.AllowAny', # 所有接口默认允许访问,由AuthMiddleware控制权限
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 20,
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
],
|
||||
}
|
||||
|
||||
# Spectacular (OpenAPI 3.0) configuration
|
||||
SPECTACULAR_SETTINGS = {
|
||||
'TITLE': 'Hertz Server API',
|
||||
'DESCRIPTION': 'API documentation for Hertz Server Django project',
|
||||
'VERSION': '1.0.0',
|
||||
'SERVE_INCLUDE_SCHEMA': False,
|
||||
'COMPONENT_SPLIT_REQUEST': True,
|
||||
'SCHEMA_PATH_PREFIX': '/api/',
|
||||
}
|
||||
|
||||
# CORS configuration
|
||||
CORS_ALLOWED_ORIGINS = config(
|
||||
'CORS_ALLOWED_ORIGINS',
|
||||
default='http://localhost:3000,http://127.0.0.1:3000',
|
||||
cast=lambda v: [s.strip() for s in v.split(',')]
|
||||
)
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = config('CORS_ALLOW_ALL_ORIGINS', default=False, cast=bool)
|
||||
|
||||
# Captcha settings
|
||||
CAPTCHA_IMAGE_SIZE = (
|
||||
config('CAPTCHA_IMAGE_SIZE_WIDTH', default=120, cast=int),
|
||||
config('CAPTCHA_IMAGE_SIZE_HEIGHT', default=50, cast=int)
|
||||
)
|
||||
CAPTCHA_LENGTH = config('CAPTCHA_LENGTH', default=4, cast=int)
|
||||
CAPTCHA_TIMEOUT = config('CAPTCHA_TIMEOUT', default=5, cast=int) # minutes
|
||||
CAPTCHA_FONT_SIZE = config('CAPTCHA_FONT_SIZE', default=40, cast=int)
|
||||
CAPTCHA_BACKGROUND_COLOR = config('CAPTCHA_BACKGROUND_COLOR', default='#ffffff')
|
||||
CAPTCHA_FOREGROUND_COLOR = config('CAPTCHA_FOREGROUND_COLOR', default='#000000')
|
||||
# 验证码词典文件路径
|
||||
CAPTCHA_WORDS_DICTIONARY = str(BASE_DIR / 'captcha_words.txt')
|
||||
# 验证码挑战函数配置
|
||||
CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' # 默认使用随机字符
|
||||
# 数学验证码配置
|
||||
CAPTCHA_MATH_CHALLENGE_OPERATOR = '+-*'
|
||||
# 验证码噪声和过滤器
|
||||
CAPTCHA_NOISE_FUNCTIONS = (
|
||||
'captcha.helpers.noise_arcs',
|
||||
'captcha.helpers.noise_dots',
|
||||
)
|
||||
CAPTCHA_FILTER_FUNCTIONS = (
|
||||
'captcha.helpers.post_smooth',
|
||||
)
|
||||
|
||||
# Email configuration
|
||||
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
|
||||
EMAIL_HOST = config('EMAIL_HOST', default='smtp.qq.com')
|
||||
EMAIL_PORT = config('EMAIL_PORT', default=465, cast=int)
|
||||
EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=True, cast=bool)
|
||||
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
|
||||
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='563161210@qq.com')
|
||||
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='563161210@qq.com')
|
||||
|
||||
# Channels configuration for WebSocket support
|
||||
ASGI_APPLICATION = 'hertz_server_django.asgi.application'
|
||||
|
||||
# Channel layers configuration
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
"hosts": [config('REDIS_URL', default='redis://127.0.0.1:6379/2')],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# 自定义用户模型
|
||||
AUTH_USER_MODEL = 'hertz_studio_django_auth.HertzUser'
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET_KEY = config('JWT_SECRET_KEY', default=SECRET_KEY)
|
||||
JWT_ALGORITHM = 'HS256'
|
||||
JWT_ACCESS_TOKEN_LIFETIME = config('JWT_ACCESS_TOKEN_LIFETIME', default=60 * 60 * 24, cast=int) # 24小时
|
||||
JWT_REFRESH_TOKEN_LIFETIME = config('JWT_REFRESH_TOKEN_LIFETIME', default=60 * 60 * 24 * 7, cast=int) # 7天
|
||||
|
||||
# 权限系统配置
|
||||
HERTZ_AUTH_SETTINGS = {
|
||||
'SUPER_ADMIN_PERMISSIONS': ['*'], # 超级管理员拥有所有权限
|
||||
'DEFAULT_PERMISSIONS': [], # 默认权限
|
||||
}
|
||||
|
||||
# AuthMiddleware配置 - 不需要登录验证的URL模式(支持正则表达式)
|
||||
NO_AUTH_PATTERNS = config(
|
||||
'NO_AUTH_PATTERNS',
|
||||
default=r'^/api/auth/login/?$,^/api/auth/register/?$,^/api/auth/email/code/?$,^/api/auth/send-email-code/?$,^/api/auth/password/reset/?$,^/api/captcha/.*$,^/api/docs/.*$,^/api/redoc/.*$,^/api/schema/.*$,^/admin/.*$,^/static/.*$,^/media/.*$,^/demo/.*$,^/websocket/.*$,^/api/system/.*$',
|
||||
cast=lambda v: [s.strip() for s in v.split(',')]
|
||||
)
|
||||
|
||||
# 密码加密配置
|
||||
PASSWORD_HASHERS = [
|
||||
'hertz_studio_django_utils.crypto.MD5PasswordHasher', # 使用MD5加密
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||
]
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
URL configuration for hertz_server_django project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
# 首页路由
|
||||
path('', views.index, name='index'),
|
||||
|
||||
# Hertz Captcha routes
|
||||
path('api/captcha/', include('hertz_studio_django_captcha.urls')),
|
||||
|
||||
# Hertz Auth routes
|
||||
path('api/', include('hertz_studio_django_auth.urls')),
|
||||
|
||||
# Demo app routes
|
||||
path('', include('hertz_demo.urls')),
|
||||
|
||||
# Hertz AI routes
|
||||
path('api/ai/', include('hertz_studio_django_ai.urls')),
|
||||
|
||||
# Hertz System Monitor routes
|
||||
path('api/system/', include('hertz_studio_django_system_monitor.urls')),
|
||||
|
||||
# Hertz System Notification routes
|
||||
path('api/notice/', include('hertz_studio_django_notice.urls')),
|
||||
|
||||
# Hertz Log routes
|
||||
path('api/log/', include('hertz_studio_django_log.urls')),
|
||||
|
||||
# Hertz Wiki routes
|
||||
path('api/wiki/', include('hertz_studio_django_wiki.urls')),
|
||||
|
||||
|
||||
|
||||
# OpenAPI documentation
|
||||
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
]
|
||||
|
||||
# 在开发环境下提供媒体文件服务
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])
|
||||
@@ -0,0 +1,13 @@
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.shortcuts import render
|
||||
|
||||
from hertz_studio_django_auth.utils.decorators import no_login_required
|
||||
|
||||
|
||||
@no_login_required
|
||||
def index(request):
|
||||
"""
|
||||
系统首页视图
|
||||
展示系统的基础介绍和功能特性
|
||||
"""
|
||||
return render(request, 'index.html')
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for hertz_server_django project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -0,0 +1,687 @@
|
||||
"""
|
||||
Django代码生成器主入口类
|
||||
|
||||
该模块提供了一个统一的Django代码生成器入口,
|
||||
整合了模型、序列化器、视图、URL路由等所有生成器功能。
|
||||
|
||||
使用示例:
|
||||
generator = DjangoCodeGenerator()
|
||||
|
||||
# 生成完整的CRUD模块
|
||||
generator.generate_full_module(
|
||||
model_name='User',
|
||||
fields=[
|
||||
{'name': 'username', 'type': 'CharField', 'max_length': 150},
|
||||
{'name': 'email', 'type': 'EmailField'},
|
||||
{'name': 'phone', 'type': 'CharField', 'max_length': 20}
|
||||
],
|
||||
output_dir='./generated_code'
|
||||
)
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Optional, Any
|
||||
from .base_generator import BaseGenerator
|
||||
from .model_generator import ModelGenerator
|
||||
from .serializer_generator import SerializerGenerator
|
||||
from .view_generator import ViewGenerator
|
||||
from .url_generator import URLGenerator
|
||||
from .yaml_parser import YAMLParser
|
||||
|
||||
|
||||
class DjangoCodeGenerator(BaseGenerator):
|
||||
"""Django代码生成器主入口类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化Django代码生成器"""
|
||||
super().__init__()
|
||||
self.model_generator = ModelGenerator()
|
||||
self.serializer_generator = SerializerGenerator()
|
||||
self.view_generator = ViewGenerator()
|
||||
self.url_generator = URLGenerator()
|
||||
self.yaml_parser = YAMLParser()
|
||||
|
||||
def generate(self, **kwargs) -> str:
|
||||
"""
|
||||
实现抽象方法generate
|
||||
|
||||
Returns:
|
||||
str: 生成的代码字符串
|
||||
"""
|
||||
return "Django代码生成器"
|
||||
|
||||
def generate_full_module(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[Dict[str, Any]],
|
||||
output_dir: str = './generated_code',
|
||||
app_name: str = None,
|
||||
operations: List[str] = None,
|
||||
permissions: List[str] = None,
|
||||
validators: Optional[Dict[str, str]] = None,
|
||||
table_name: str = None,
|
||||
verbose_name: str = None,
|
||||
ordering: List[str] = None
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
生成完整的Django模块代码(模型、序列化器、视图、URL)
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段配置列表
|
||||
output_dir: 输出目录
|
||||
app_name: 应用名称
|
||||
operations: 支持的操作列表
|
||||
permissions: 权限装饰器列表
|
||||
validators: 字段验证器映射
|
||||
table_name: 数据库表名
|
||||
verbose_name: 模型显示名称
|
||||
ordering: 默认排序字段
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 生成的代码文件映射
|
||||
"""
|
||||
if operations is None:
|
||||
operations = ['create', 'read', 'update', 'delete', 'list']
|
||||
|
||||
if not app_name:
|
||||
app_name = self.to_snake_case(model_name)
|
||||
|
||||
generated_files = {}
|
||||
|
||||
# 创建输出目录
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 生成模型代码
|
||||
model_code = self.generate_model(
|
||||
model_name=model_name,
|
||||
fields=fields,
|
||||
table_name=table_name,
|
||||
verbose_name=verbose_name,
|
||||
ordering=ordering
|
||||
)
|
||||
generated_files['models.py'] = model_code
|
||||
|
||||
# 生成序列化器代码
|
||||
serializer_code = self.generate_serializers(
|
||||
model_name=model_name,
|
||||
fields=[field['name'] for field in fields],
|
||||
validators=validators
|
||||
)
|
||||
generated_files['serializers.py'] = serializer_code
|
||||
|
||||
# 生成视图代码
|
||||
view_code = self.generate_views(
|
||||
model_name=model_name,
|
||||
operations=operations,
|
||||
permissions=permissions
|
||||
)
|
||||
generated_files['views.py'] = view_code
|
||||
|
||||
# 生成URL路由代码
|
||||
url_code = self.generate_urls(
|
||||
model_name=model_name,
|
||||
operations=operations,
|
||||
app_name=app_name
|
||||
)
|
||||
generated_files['urls.py'] = url_code
|
||||
|
||||
# 写入文件
|
||||
for filename, code in generated_files.items():
|
||||
file_path = os.path.join(output_dir, filename)
|
||||
self.write_to_file(file_path, code)
|
||||
|
||||
return generated_files
|
||||
|
||||
def generate_model(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[Dict[str, Any]],
|
||||
table_name: str = None,
|
||||
verbose_name: str = None,
|
||||
ordering: List[str] = None,
|
||||
status_choices: List[tuple] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成模型代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段配置列表
|
||||
table_name: 数据库表名
|
||||
verbose_name: 模型显示名称
|
||||
ordering: 默认排序字段
|
||||
status_choices: 状态选择项
|
||||
|
||||
Returns:
|
||||
str: 生成的模型代码
|
||||
"""
|
||||
return self.model_generator.generate(
|
||||
model_name=model_name,
|
||||
fields=fields,
|
||||
table_name=table_name,
|
||||
verbose_name=verbose_name,
|
||||
ordering=ordering,
|
||||
status_choices=status_choices
|
||||
)
|
||||
|
||||
def generate_serializers(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[str],
|
||||
validators: Optional[Dict[str, str]] = None,
|
||||
create_fields: Optional[List[str]] = None,
|
||||
update_fields: Optional[List[str]] = None,
|
||||
list_fields: Optional[List[str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成序列化器代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段列表
|
||||
validators: 字段验证器映射
|
||||
create_fields: 创建时使用的字段
|
||||
update_fields: 更新时使用的字段
|
||||
list_fields: 列表时显示的字段
|
||||
|
||||
Returns:
|
||||
str: 生成的序列化器代码
|
||||
"""
|
||||
return self.serializer_generator.generate(
|
||||
model_name=model_name,
|
||||
fields=fields,
|
||||
create_fields=create_fields,
|
||||
update_fields=update_fields,
|
||||
list_fields=list_fields,
|
||||
validators=validators
|
||||
)
|
||||
|
||||
def generate_views(
|
||||
self,
|
||||
model_name: str,
|
||||
operations: List[str] = None,
|
||||
permissions: List[str] = None,
|
||||
filters: Optional[List[str]] = None,
|
||||
ordering: Optional[List[str]] = None,
|
||||
search_fields: Optional[List[str]] = None,
|
||||
pagination: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
生成视图代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
operations: 支持的操作列表
|
||||
permissions: 权限装饰器列表
|
||||
filters: 过滤字段列表
|
||||
ordering: 排序字段列表
|
||||
search_fields: 搜索字段列表
|
||||
pagination: 是否启用分页
|
||||
|
||||
Returns:
|
||||
str: 生成的视图代码
|
||||
"""
|
||||
if operations is None:
|
||||
operations = ['create', 'read', 'update', 'delete', 'list']
|
||||
|
||||
return self.view_generator.generate(
|
||||
model_name=model_name,
|
||||
operations=operations,
|
||||
permissions=permissions,
|
||||
filters=filters,
|
||||
ordering=ordering,
|
||||
search_fields=search_fields,
|
||||
pagination=pagination
|
||||
)
|
||||
|
||||
def generate_urls(
|
||||
self,
|
||||
model_name: str,
|
||||
operations: List[str] = None,
|
||||
prefix: str = '',
|
||||
app_name: str = '',
|
||||
namespace: str = ''
|
||||
) -> str:
|
||||
"""
|
||||
生成URL路由代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
operations: 支持的操作列表
|
||||
prefix: URL前缀
|
||||
app_name: 应用名称
|
||||
namespace: 命名空间
|
||||
|
||||
Returns:
|
||||
str: 生成的URL路由代码
|
||||
"""
|
||||
if operations is None:
|
||||
operations = ['create', 'read', 'update', 'delete', 'list']
|
||||
|
||||
return self.url_generator.generate(
|
||||
model_name=model_name,
|
||||
operations=operations,
|
||||
prefix=prefix,
|
||||
app_name=app_name,
|
||||
namespace=namespace
|
||||
)
|
||||
|
||||
def generate_api_module(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[Dict[str, Any]],
|
||||
output_dir: str = './generated_api',
|
||||
version: str = 'v1',
|
||||
permissions: List[str] = None
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
生成RESTful API模块代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段配置列表
|
||||
output_dir: 输出目录
|
||||
version: API版本
|
||||
permissions: 权限装饰器列表
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 生成的代码文件映射
|
||||
"""
|
||||
generated_files = {}
|
||||
|
||||
# 创建输出目录
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 生成模型代码
|
||||
model_code = self.generate_model(
|
||||
model_name=model_name,
|
||||
fields=fields
|
||||
)
|
||||
generated_files['models.py'] = model_code
|
||||
|
||||
# 生成序列化器代码
|
||||
serializer_code = self.generate_serializers(
|
||||
model_name=model_name,
|
||||
fields=[field['name'] for field in fields]
|
||||
)
|
||||
generated_files['serializers.py'] = serializer_code
|
||||
|
||||
# 生成API视图代码
|
||||
view_code = self.generate_views(
|
||||
model_name=model_name,
|
||||
permissions=permissions
|
||||
)
|
||||
generated_files['views.py'] = view_code
|
||||
|
||||
# 生成API URL路由代码
|
||||
url_code = self.url_generator.generate_api_urls(
|
||||
model_name=model_name,
|
||||
version=version
|
||||
)
|
||||
generated_files['urls.py'] = url_code
|
||||
|
||||
# 写入文件
|
||||
for filename, code in generated_files.items():
|
||||
file_path = os.path.join(output_dir, filename)
|
||||
self.write_to_file(file_path, code)
|
||||
|
||||
return generated_files
|
||||
|
||||
def generate_nested_module(
|
||||
self,
|
||||
parent_model: str,
|
||||
child_model: str,
|
||||
parent_fields: List[Dict[str, Any]],
|
||||
child_fields: List[Dict[str, Any]],
|
||||
output_dir: str = './generated_nested',
|
||||
relationship_type: str = 'foreign_key'
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
生成嵌套资源模块代码
|
||||
|
||||
Args:
|
||||
parent_model: 父模型名称
|
||||
child_model: 子模型名称
|
||||
parent_fields: 父模型字段配置
|
||||
child_fields: 子模型字段配置
|
||||
output_dir: 输出目录
|
||||
relationship_type: 关系类型 ('foreign_key', 'one_to_one', 'many_to_many')
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 生成的代码文件映射
|
||||
"""
|
||||
generated_files = {}
|
||||
|
||||
# 创建输出目录
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 在子模型字段中添加父模型关系
|
||||
if relationship_type == 'foreign_key':
|
||||
child_fields.append({
|
||||
'name': self.to_snake_case(parent_model),
|
||||
'type': 'ForeignKey',
|
||||
'to': parent_model,
|
||||
'on_delete': 'models.CASCADE',
|
||||
'verbose_name': f'{parent_model}',
|
||||
'help_text': f'关联的{parent_model}'
|
||||
})
|
||||
|
||||
# 生成父模型代码
|
||||
parent_model_code = self.generate_model(
|
||||
model_name=parent_model,
|
||||
fields=parent_fields
|
||||
)
|
||||
generated_files[f'{self.to_snake_case(parent_model)}_models.py'] = parent_model_code
|
||||
|
||||
# 生成子模型代码
|
||||
child_model_code = self.generate_model(
|
||||
model_name=child_model,
|
||||
fields=child_fields
|
||||
)
|
||||
generated_files[f'{self.to_snake_case(child_model)}_models.py'] = child_model_code
|
||||
|
||||
# 生成嵌套URL路由
|
||||
nested_url_code = self.url_generator.generate_nested_urls(
|
||||
parent_model=parent_model,
|
||||
child_model=child_model
|
||||
)
|
||||
generated_files['nested_urls.py'] = nested_url_code
|
||||
|
||||
# 写入文件
|
||||
for filename, code in generated_files.items():
|
||||
file_path = os.path.join(output_dir, filename)
|
||||
self.write_to_file(file_path, code)
|
||||
|
||||
return generated_files
|
||||
|
||||
def generate_app_structure(
|
||||
self,
|
||||
app_name: str,
|
||||
models: List[Dict[str, Any]],
|
||||
output_dir: str = None
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
生成完整的Django应用结构
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
models: 模型配置列表
|
||||
output_dir: 输出目录
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 生成的代码文件映射
|
||||
"""
|
||||
if not output_dir:
|
||||
output_dir = f'./generated_{app_name}'
|
||||
|
||||
generated_files = {}
|
||||
|
||||
# 创建应用目录结构
|
||||
app_dir = os.path.join(output_dir, app_name)
|
||||
os.makedirs(app_dir, exist_ok=True)
|
||||
os.makedirs(os.path.join(app_dir, 'serializers'), exist_ok=True)
|
||||
os.makedirs(os.path.join(app_dir, 'views'), exist_ok=True)
|
||||
os.makedirs(os.path.join(app_dir, 'urls'), exist_ok=True)
|
||||
|
||||
# 生成应用配置文件
|
||||
apps_code = self._generate_apps_config(app_name)
|
||||
generated_files['apps.py'] = apps_code
|
||||
|
||||
# 生成__init__.py文件
|
||||
generated_files['__init__.py'] = ''
|
||||
generated_files['serializers/__init__.py'] = ''
|
||||
generated_files['views/__init__.py'] = ''
|
||||
generated_files['urls/__init__.py'] = ''
|
||||
|
||||
# 为每个模型生成代码
|
||||
all_models_code = []
|
||||
all_serializers_code = []
|
||||
all_views_code = []
|
||||
all_urls_code = []
|
||||
|
||||
for model_config in models:
|
||||
model_name = model_config['name']
|
||||
fields = model_config['fields']
|
||||
|
||||
# 生成模型代码
|
||||
model_code = self.generate_model(
|
||||
model_name=model_name,
|
||||
fields=fields,
|
||||
verbose_name=model_config.get('verbose_name', model_name),
|
||||
table_name=model_config.get('table_name'),
|
||||
ordering=model_config.get('ordering', ['-created_at'])
|
||||
)
|
||||
all_models_code.append(model_code)
|
||||
|
||||
# 生成序列化器代码
|
||||
serializer_code = self.generate_serializers(
|
||||
model_name=model_name,
|
||||
fields=[field['name'] for field in fields],
|
||||
validators=model_config.get('validators', {})
|
||||
)
|
||||
all_serializers_code.append(serializer_code)
|
||||
|
||||
# 生成视图代码
|
||||
view_code = self.generate_views(
|
||||
model_name=model_name,
|
||||
operations=model_config.get('operations', ['create', 'read', 'update', 'delete', 'list']),
|
||||
permissions=model_config.get('permissions', [])
|
||||
)
|
||||
all_views_code.append(view_code)
|
||||
|
||||
# 生成URL代码
|
||||
url_code = self.generate_urls(
|
||||
model_name=model_name,
|
||||
app_name=app_name,
|
||||
operations=model_config.get('operations', ['create', 'read', 'update', 'delete', 'list'])
|
||||
)
|
||||
all_urls_code.append(url_code)
|
||||
|
||||
# 合并所有代码
|
||||
generated_files['models.py'] = '\n\n'.join(all_models_code)
|
||||
generated_files['serializers.py'] = '\n\n'.join(all_serializers_code)
|
||||
generated_files['views.py'] = '\n\n'.join(all_views_code)
|
||||
generated_files['urls.py'] = '\n\n'.join(all_urls_code)
|
||||
|
||||
# 写入文件
|
||||
for filename, code in generated_files.items():
|
||||
file_path = os.path.join(app_dir, filename)
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
self.write_to_file(file_path, code)
|
||||
|
||||
return generated_files
|
||||
|
||||
def _generate_apps_config(self, app_name: str) -> str:
|
||||
"""生成Django应用配置代码"""
|
||||
class_name = self.snake_to_camel(app_name) + 'Config'
|
||||
|
||||
code_parts = [
|
||||
'from django.apps import AppConfig',
|
||||
'',
|
||||
f'class {class_name}(AppConfig):',
|
||||
' """',
|
||||
f' {app_name}应用配置',
|
||||
' """',
|
||||
" default_auto_field = 'django.db.models.BigAutoField'",
|
||||
f" name = '{app_name}'",
|
||||
f" verbose_name = '{app_name.title()}'"
|
||||
]
|
||||
|
||||
return '\n'.join(code_parts)
|
||||
|
||||
def generate_from_yaml_file(self, yaml_file_path: str) -> Dict[str, str]:
|
||||
"""
|
||||
从YAML配置文件生成Django应用代码
|
||||
|
||||
Args:
|
||||
yaml_file_path: YAML配置文件路径
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 生成的文件路径和内容映射
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: 当YAML文件不存在时
|
||||
ValueError: 当配置验证失败时
|
||||
"""
|
||||
# 解析YAML配置
|
||||
config = self.yaml_parser.parse_yaml_file(yaml_file_path)
|
||||
|
||||
# 从配置生成代码
|
||||
return self.generate_from_yaml_config(config)
|
||||
|
||||
def generate_from_yaml_config(self, config: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""
|
||||
从YAML配置字典生成Django应用代码
|
||||
|
||||
Args:
|
||||
config: YAML配置字典
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 生成的文件路径和内容映射
|
||||
"""
|
||||
app_name = config['app_name']
|
||||
models = config['models']
|
||||
output_dir = config.get('output_dir', '.') # 默认输出到当前目录
|
||||
|
||||
all_generated_files = {}
|
||||
|
||||
# 为每个模型生成代码
|
||||
if isinstance(models, dict):
|
||||
# 如果models是字典格式
|
||||
models_items = models.items()
|
||||
else:
|
||||
# 如果models是列表格式,转换为字典格式
|
||||
models_items = [(model['name'], model) for model in models]
|
||||
|
||||
for model_name, model_config in models_items:
|
||||
# 获取模型字段配置
|
||||
fields = model_config.get('fields', [])
|
||||
operations = model_config.get('operations', ['create', 'get', 'update', 'delete', 'list'])
|
||||
permissions = model_config.get('permissions', {})
|
||||
api_config = model_config.get('api', {})
|
||||
|
||||
# 转换字段格式
|
||||
processed_fields = {}
|
||||
for field in fields:
|
||||
field_name = field['name']
|
||||
processed_fields[field_name] = {
|
||||
'type': field['type'],
|
||||
'verbose_name': field.get('verbose_name', field_name),
|
||||
'help_text': field.get('help_text', f'{field_name}字段'),
|
||||
}
|
||||
|
||||
# 添加其他字段属性
|
||||
for attr in ['max_length', 'null', 'blank', 'default', 'unique', 'choices']:
|
||||
if attr in field:
|
||||
processed_fields[field_name][attr] = field[attr]
|
||||
|
||||
# 创建应用目录
|
||||
app_dir = os.path.join(output_dir, app_name)
|
||||
print(f"📁 创建应用目录: {app_dir}")
|
||||
os.makedirs(app_dir, exist_ok=True)
|
||||
|
||||
# 创建migrations子目录
|
||||
migrations_dir = os.path.join(app_dir, 'migrations')
|
||||
print(f"📁 创建子目录: {migrations_dir}")
|
||||
os.makedirs(migrations_dir, exist_ok=True)
|
||||
# 创建migrations/__init__.py文件
|
||||
migrations_init_file = os.path.join(migrations_dir, '__init__.py')
|
||||
with open(migrations_init_file, 'w', encoding='utf-8') as f:
|
||||
f.write('')
|
||||
all_generated_files[migrations_init_file] = ''
|
||||
|
||||
# 生成模型代码
|
||||
print(f"📝 生成模型代码...")
|
||||
model_code = self.model_generator.generate(model_name, processed_fields)
|
||||
model_file = os.path.join(app_dir, 'models.py')
|
||||
with open(model_file, 'w', encoding='utf-8') as f:
|
||||
f.write(model_code)
|
||||
all_generated_files[model_file] = model_code
|
||||
print(f"✅ 已生成: {model_file}")
|
||||
|
||||
# 生成序列化器代码
|
||||
print(f"📝 生成序列化器代码...")
|
||||
serializer_code = self.serializer_generator.generate(model_name, processed_fields)
|
||||
serializer_file = os.path.join(app_dir, 'serializers.py')
|
||||
with open(serializer_file, 'w', encoding='utf-8') as f:
|
||||
f.write(serializer_code)
|
||||
all_generated_files[serializer_file] = serializer_code
|
||||
print(f"✅ 已生成: {serializer_file}")
|
||||
|
||||
# 生成视图代码
|
||||
print(f"📝 生成视图代码...")
|
||||
view_code = self.view_generator.generate(model_name, operations)
|
||||
view_file = os.path.join(app_dir, 'views.py')
|
||||
with open(view_file, 'w', encoding='utf-8') as f:
|
||||
f.write(view_code)
|
||||
all_generated_files[view_file] = view_code
|
||||
print(f"✅ 已生成: {view_file}")
|
||||
|
||||
# 生成URL代码
|
||||
print(f"📝 生成URL代码...")
|
||||
url_code = self.url_generator.generate(model_name, operations)
|
||||
url_file = os.path.join(app_dir, 'urls.py')
|
||||
with open(url_file, 'w', encoding='utf-8') as f:
|
||||
f.write(url_code)
|
||||
all_generated_files[url_file] = url_code
|
||||
print(f"✅ 已生成: {url_file}")
|
||||
|
||||
# 生成应用配置文件
|
||||
print(f"📝 生成应用配置文件...")
|
||||
apps_code = self._generate_apps_config(app_name)
|
||||
apps_file = os.path.join(app_dir, 'apps.py')
|
||||
with open(apps_file, 'w', encoding='utf-8') as f:
|
||||
f.write(apps_code)
|
||||
all_generated_files[apps_file] = apps_code
|
||||
print(f"✅ 已生成: {apps_file}")
|
||||
|
||||
# 生成__init__.py文件
|
||||
init_file = os.path.join(app_dir, '__init__.py')
|
||||
with open(init_file, 'w', encoding='utf-8') as f:
|
||||
f.write('')
|
||||
all_generated_files[init_file] = ''
|
||||
print(f"✅ 已生成: {init_file}")
|
||||
|
||||
# 生成migrations的__init__.py
|
||||
migrations_init = os.path.join(app_dir, 'migrations', '__init__.py')
|
||||
with open(migrations_init, 'w', encoding='utf-8') as f:
|
||||
f.write('')
|
||||
all_generated_files[migrations_init] = ''
|
||||
print(f"✅ 已生成: {migrations_init}")
|
||||
|
||||
print(f"\n🎉 Django应用 '{app_name}' 生成完成!")
|
||||
print(f"📁 输出目录: {os.path.abspath(output_dir)}")
|
||||
print(f"📄 生成文件数量: {len(all_generated_files)}")
|
||||
|
||||
return all_generated_files
|
||||
|
||||
def generate_yaml_template(self, output_path: str = 'app_template.yaml') -> str:
|
||||
"""
|
||||
生成YAML配置文件模板
|
||||
|
||||
Args:
|
||||
output_path: 输出文件路径
|
||||
|
||||
Returns:
|
||||
str: 生成的模板内容
|
||||
"""
|
||||
return self.yaml_parser.generate_yaml_template(output_path)
|
||||
|
||||
def validate_yaml_config(self, yaml_file_path: str) -> bool:
|
||||
"""
|
||||
验证YAML配置文件格式
|
||||
|
||||
Args:
|
||||
yaml_file_path: YAML配置文件路径
|
||||
|
||||
Returns:
|
||||
bool: 验证是否通过
|
||||
"""
|
||||
try:
|
||||
self.yaml_parser.parse_yaml_file(yaml_file_path)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"YAML配置验证失败: {e}")
|
||||
return False
|
||||
434
hertz_studio_django_utils/code_generator/menu_generator.py
Normal file
434
hertz_studio_django_utils/code_generator/menu_generator.py
Normal file
@@ -0,0 +1,434 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
菜单权限生成器
|
||||
用于自动生成菜单配置和权限同步功能
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class MenuGenerator:
|
||||
"""
|
||||
菜单权限生成器
|
||||
|
||||
用于根据模型和操作自动生成菜单配置,包括:
|
||||
1. 菜单项配置
|
||||
2. 权限配置
|
||||
3. 菜单层级结构
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化菜单生成器"""
|
||||
self.menu_types = {
|
||||
'directory': 1, # 目录
|
||||
'menu': 2, # 菜单
|
||||
'button': 3 # 按钮
|
||||
}
|
||||
|
||||
# 标准操作映射 - 与视图生成器保持一致
|
||||
self.operation_mapping = {
|
||||
'list': {'name': '查询', 'permission': 'list'},
|
||||
'create': {'name': '新增', 'permission': 'add'},
|
||||
'retrieve': {'name': '详情', 'permission': 'list'}, # 统一使用list权限
|
||||
'update': {'name': '修改', 'permission': 'edit'},
|
||||
'delete': {'name': '删除', 'permission': 'remove'},
|
||||
'export': {'name': '导出', 'permission': 'export'},
|
||||
'import': {'name': '导入', 'permission': 'import'},
|
||||
}
|
||||
|
||||
def generate_menu_config(
|
||||
self,
|
||||
module_name: str,
|
||||
model_name: str,
|
||||
operations: List[str],
|
||||
parent_code: Optional[str] = None,
|
||||
menu_prefix: str = 'system',
|
||||
sort_order: int = 1,
|
||||
icon: Optional[str] = None,
|
||||
component_path: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成菜单配置
|
||||
|
||||
Args:
|
||||
module_name: 模块名称(中文)
|
||||
model_name: 模型名称(英文,snake_case)
|
||||
operations: 操作列表
|
||||
parent_code: 父级菜单代码
|
||||
menu_prefix: 菜单前缀
|
||||
sort_order: 排序
|
||||
icon: 图标
|
||||
component_path: 组件路径
|
||||
|
||||
Returns:
|
||||
List[Dict]: 菜单配置列表
|
||||
"""
|
||||
menus = []
|
||||
|
||||
# 生成主菜单
|
||||
main_menu_code = f"{menu_prefix}:{model_name}"
|
||||
main_menu = {
|
||||
'menu_name': module_name,
|
||||
'menu_code': main_menu_code,
|
||||
'menu_type': self.menu_types['menu'],
|
||||
'path': f"/{menu_prefix}/{model_name}",
|
||||
'component': component_path or f"{menu_prefix}/{model_name}/index",
|
||||
'icon': icon or model_name,
|
||||
'permission': f"{main_menu_code}:list",
|
||||
'sort_order': sort_order,
|
||||
'parent_code': parent_code
|
||||
}
|
||||
menus.append(main_menu)
|
||||
|
||||
# 生成操作按钮
|
||||
for i, operation in enumerate(operations, 1):
|
||||
if operation in self.operation_mapping:
|
||||
op_config = self.operation_mapping[operation]
|
||||
button_menu = {
|
||||
'menu_name': f"{module_name}{op_config['name']}",
|
||||
'menu_code': f"{main_menu_code}:{op_config['permission']}",
|
||||
'menu_type': self.menu_types['button'],
|
||||
'permission': f"{main_menu_code}:{op_config['permission']}",
|
||||
'sort_order': i,
|
||||
'parent_code': main_menu_code
|
||||
}
|
||||
menus.append(button_menu)
|
||||
|
||||
return menus
|
||||
|
||||
def generate_directory_config(
|
||||
self,
|
||||
directory_name: str,
|
||||
directory_code: str,
|
||||
path: str,
|
||||
icon: str = 'folder',
|
||||
sort_order: int = 1
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
生成目录配置
|
||||
|
||||
Args:
|
||||
directory_name: 目录名称
|
||||
directory_code: 目录代码
|
||||
path: 路径
|
||||
icon: 图标
|
||||
sort_order: 排序
|
||||
|
||||
Returns:
|
||||
Dict: 目录配置
|
||||
"""
|
||||
return {
|
||||
'menu_name': directory_name,
|
||||
'menu_code': directory_code,
|
||||
'menu_type': self.menu_types['directory'],
|
||||
'path': path,
|
||||
'icon': icon,
|
||||
'sort_order': sort_order,
|
||||
'parent_code': None
|
||||
}
|
||||
|
||||
def generate_custom_menu_config(
|
||||
self,
|
||||
menu_name: str,
|
||||
menu_code: str,
|
||||
operations: List[Dict[str, str]],
|
||||
parent_code: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
component: Optional[str] = None,
|
||||
icon: Optional[str] = None,
|
||||
sort_order: int = 1
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
生成自定义菜单配置
|
||||
|
||||
Args:
|
||||
menu_name: 菜单名称
|
||||
menu_code: 菜单代码
|
||||
operations: 自定义操作列表 [{'name': '操作名', 'permission': '权限码'}]
|
||||
parent_code: 父级菜单代码
|
||||
path: 路径
|
||||
component: 组件
|
||||
icon: 图标
|
||||
sort_order: 排序
|
||||
|
||||
Returns:
|
||||
List[Dict]: 菜单配置列表
|
||||
"""
|
||||
menus = []
|
||||
|
||||
# 生成主菜单
|
||||
main_menu = {
|
||||
'menu_name': menu_name,
|
||||
'menu_code': menu_code,
|
||||
'menu_type': self.menu_types['menu'],
|
||||
'sort_order': sort_order,
|
||||
'parent_code': parent_code
|
||||
}
|
||||
|
||||
if path:
|
||||
main_menu['path'] = path
|
||||
if component:
|
||||
main_menu['component'] = component
|
||||
if icon:
|
||||
main_menu['icon'] = icon
|
||||
if operations:
|
||||
main_menu['permission'] = f"{menu_code}:{operations[0]['permission']}"
|
||||
|
||||
menus.append(main_menu)
|
||||
|
||||
# 生成操作按钮
|
||||
for i, operation in enumerate(operations, 1):
|
||||
button_menu = {
|
||||
'menu_name': f"{menu_name}{operation['name']}",
|
||||
'menu_code': f"{menu_code}:{operation['permission']}",
|
||||
'menu_type': self.menu_types['button'],
|
||||
'permission': f"{menu_code}:{operation['permission']}",
|
||||
'sort_order': i,
|
||||
'parent_code': menu_code
|
||||
}
|
||||
menus.append(button_menu)
|
||||
|
||||
return menus
|
||||
|
||||
def update_menus_config_file(
|
||||
self,
|
||||
new_menus: List[Dict[str, Any]],
|
||||
config_file_path: str = None
|
||||
) -> bool:
|
||||
"""
|
||||
更新菜单配置文件
|
||||
|
||||
Args:
|
||||
new_menus: 新的菜单配置列表
|
||||
config_file_path: 配置文件路径
|
||||
|
||||
Returns:
|
||||
bool: 更新是否成功
|
||||
"""
|
||||
if not config_file_path:
|
||||
config_file_path = os.path.join(
|
||||
Path(__file__).parent.parent,
|
||||
'config',
|
||||
'menus_config.py'
|
||||
)
|
||||
|
||||
try:
|
||||
# 读取现有配置
|
||||
with open(config_file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 查找现有菜单列表的结束位置
|
||||
import_end = content.find(']')
|
||||
if import_end == -1:
|
||||
return False
|
||||
|
||||
# 生成新菜单配置代码
|
||||
new_menu_code = self._generate_menu_code(new_menus)
|
||||
|
||||
# 在现有菜单列表末尾添加新菜单(在最后一个 ] 之前)
|
||||
updated_content = (
|
||||
content[:import_end] +
|
||||
',\n\n # 新增菜单配置\n' +
|
||||
new_menu_code +
|
||||
content[import_end:]
|
||||
)
|
||||
|
||||
# 写入更新后的配置
|
||||
with open(config_file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(updated_content)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"更新菜单配置文件失败: {e}")
|
||||
return False
|
||||
|
||||
def _generate_menu_code(self, menus: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
生成菜单配置代码
|
||||
|
||||
Args:
|
||||
menus: 菜单配置列表
|
||||
|
||||
Returns:
|
||||
str: 菜单配置代码
|
||||
"""
|
||||
code_lines = []
|
||||
|
||||
for menu in menus:
|
||||
code_lines.append(" {")
|
||||
for key, value in menu.items():
|
||||
if isinstance(value, str):
|
||||
code_lines.append(f" '{key}': '{value}',")
|
||||
elif value is None:
|
||||
code_lines.append(f" '{key}': None,")
|
||||
else:
|
||||
code_lines.append(f" '{key}': {value},")
|
||||
code_lines.append(" }")
|
||||
|
||||
return '\n'.join(code_lines)
|
||||
|
||||
def generate_permission_sync_code(
|
||||
self,
|
||||
menu_configs: List[Dict[str, Any]]
|
||||
) -> str:
|
||||
"""
|
||||
生成权限同步代码
|
||||
|
||||
Args:
|
||||
menu_configs: 菜单配置列表
|
||||
|
||||
Returns:
|
||||
str: 权限同步代码
|
||||
"""
|
||||
sync_code = '''
|
||||
def sync_new_menus():
|
||||
"""
|
||||
同步新增菜单到数据库
|
||||
"""
|
||||
from hertz_studio_django_auth.models import HertzMenu
|
||||
from django.db import transaction
|
||||
|
||||
new_menus = [
|
||||
'''
|
||||
|
||||
# 添加菜单配置
|
||||
sync_code += self._generate_menu_code(menu_configs)
|
||||
|
||||
sync_code += '''
|
||||
]
|
||||
|
||||
print("正在同步新增菜单...")
|
||||
|
||||
with transaction.atomic():
|
||||
created_menus = {}
|
||||
|
||||
# 按层级创建菜单
|
||||
for menu_data in new_menus:
|
||||
parent_code = menu_data.pop('parent_code', None)
|
||||
parent_id = None
|
||||
|
||||
if parent_code and parent_code in created_menus:
|
||||
parent_id = created_menus[parent_code]
|
||||
|
||||
menu, created = HertzMenu.objects.get_or_create(
|
||||
menu_code=menu_data['menu_code'],
|
||||
defaults={
|
||||
**menu_data,
|
||||
'parent_id': parent_id
|
||||
}
|
||||
)
|
||||
|
||||
created_menus[menu.menu_code] = menu
|
||||
|
||||
if created:
|
||||
print(f"菜单创建成功: {menu.menu_name}")
|
||||
else:
|
||||
print(f"菜单已存在: {menu.menu_name}")
|
||||
|
||||
print("菜单同步完成")
|
||||
'''
|
||||
|
||||
return sync_code
|
||||
|
||||
def create_menu_sync_script(
|
||||
self,
|
||||
menu_configs: List[Dict[str, Any]],
|
||||
script_path: str = None
|
||||
) -> bool:
|
||||
"""
|
||||
创建菜单同步脚本
|
||||
|
||||
Args:
|
||||
menu_configs: 菜单配置列表
|
||||
script_path: 脚本路径
|
||||
|
||||
Returns:
|
||||
bool: 创建是否成功
|
||||
"""
|
||||
if not script_path:
|
||||
script_path = os.path.join(
|
||||
Path(__file__).parent.parent.parent,
|
||||
'sync_menus.py'
|
||||
)
|
||||
|
||||
try:
|
||||
script_content = f'''#!/usr/bin/env python
|
||||
"""
|
||||
菜单同步脚本
|
||||
自动生成的菜单权限同步脚本
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# 添加项目路径
|
||||
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# 设置Django环境
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
|
||||
django.setup()
|
||||
|
||||
{self.generate_permission_sync_code(menu_configs)}
|
||||
|
||||
if __name__ == "__main__":
|
||||
sync_new_menus()
|
||||
'''
|
||||
|
||||
with open(script_path, 'w', encoding='utf-8') as f:
|
||||
f.write(script_content)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建菜单同步脚本失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# 使用示例
|
||||
def example_usage():
|
||||
"""使用示例"""
|
||||
generator = MenuGenerator()
|
||||
|
||||
# 生成标准CRUD菜单
|
||||
crud_menus = generator.generate_menu_config(
|
||||
module_name="产品管理",
|
||||
model_name="product",
|
||||
operations=['list', 'create', 'update', 'delete'],
|
||||
parent_code="system",
|
||||
sort_order=5,
|
||||
icon="product"
|
||||
)
|
||||
|
||||
# 生成自定义菜单
|
||||
custom_menus = generator.generate_custom_menu_config(
|
||||
menu_name="报表管理",
|
||||
menu_code="system:report",
|
||||
operations=[
|
||||
{'name': '查看', 'permission': 'view'},
|
||||
{'name': '导出', 'permission': 'export'},
|
||||
{'name': '打印', 'permission': 'print'}
|
||||
],
|
||||
parent_code="system",
|
||||
path="/system/report",
|
||||
component="system/report/index",
|
||||
icon="report",
|
||||
sort_order=8
|
||||
)
|
||||
|
||||
# 更新配置文件
|
||||
all_menus = crud_menus + custom_menus
|
||||
generator.update_menus_config_file(all_menus)
|
||||
|
||||
# 创建同步脚本
|
||||
generator.create_menu_sync_script(all_menus)
|
||||
|
||||
print("菜单配置生成完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
example_usage()
|
||||
262
hertz_studio_django_utils/code_generator/model_generator.py
Normal file
262
hertz_studio_django_utils/code_generator/model_generator.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
Django模型代码生成器
|
||||
|
||||
该模块负责根据配置生成Django模型代码
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
from .base_generator import BaseGenerator
|
||||
|
||||
|
||||
class ModelGenerator(BaseGenerator):
|
||||
"""Django模型代码生成器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化模型生成器"""
|
||||
super().__init__()
|
||||
|
||||
def generate(self, model_name: str = None, fields: List[Dict[str, Any]] = None, **kwargs) -> str:
|
||||
"""
|
||||
生成Django模型代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段列表
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
str: 生成的模型代码
|
||||
"""
|
||||
# 处理参数,支持位置参数和关键字参数
|
||||
if model_name is None:
|
||||
model_name = kwargs.get('model_name', 'DefaultModel')
|
||||
if fields is None:
|
||||
fields = kwargs.get('fields', [])
|
||||
|
||||
table_name = kwargs.get('table_name')
|
||||
verbose_name = kwargs.get('verbose_name')
|
||||
verbose_name_plural = kwargs.get('verbose_name_plural')
|
||||
ordering = kwargs.get('ordering', ['-created_at'])
|
||||
status_choices = kwargs.get('status_choices', [
|
||||
('active', '激活'),
|
||||
('inactive', '未激活'),
|
||||
('deleted', '已删除')
|
||||
])
|
||||
|
||||
# 确保verbose_name不为None
|
||||
if verbose_name is None:
|
||||
verbose_name = model_name
|
||||
|
||||
# 确保verbose_name_plural不为None
|
||||
if verbose_name_plural is None:
|
||||
verbose_name_plural = verbose_name + '列表'
|
||||
|
||||
# 处理字段列表,确保每个字段都有必要的属性
|
||||
processed_fields = []
|
||||
for field in fields:
|
||||
if isinstance(field, dict):
|
||||
# 字段已经在YAML解析器中处理过类型转换,这里不再重复处理
|
||||
processed_fields.append(field)
|
||||
elif isinstance(field, str):
|
||||
# 如果字段是字符串,转换为字典格式
|
||||
processed_fields.append({
|
||||
'name': field,
|
||||
'type': 'CharField', # 直接使用Django字段类型
|
||||
'django_field_type': 'CharField',
|
||||
'max_length': 100,
|
||||
'verbose_name': field,
|
||||
'help_text': f'{field}字段'
|
||||
})
|
||||
|
||||
# 添加默认的时间戳字段(如果不存在)
|
||||
has_created_at = any(field.get('name') == 'created_at' for field in processed_fields)
|
||||
has_updated_at = any(field.get('name') == 'updated_at' for field in processed_fields)
|
||||
|
||||
if not has_created_at:
|
||||
processed_fields.append({
|
||||
'name': 'created_at',
|
||||
'type': 'DateTimeField',
|
||||
'auto_now_add': True,
|
||||
'verbose_name': '创建时间',
|
||||
'help_text': '记录创建时间'
|
||||
})
|
||||
|
||||
if not has_updated_at:
|
||||
processed_fields.append({
|
||||
'name': 'updated_at',
|
||||
'type': 'DateTimeField',
|
||||
'auto_now': True,
|
||||
'verbose_name': '更新时间',
|
||||
'help_text': '记录最后更新时间'
|
||||
})
|
||||
|
||||
# 准备模板上下文
|
||||
context = {
|
||||
'model_name': model_name,
|
||||
'fields': processed_fields,
|
||||
'table_name': table_name or self.camel_to_snake(model_name),
|
||||
'verbose_name': verbose_name,
|
||||
'verbose_name_plural': verbose_name_plural,
|
||||
'ordering': ordering,
|
||||
'status_choices': status_choices,
|
||||
'has_status_field': any(field.get('name') == 'status' for field in processed_fields)
|
||||
}
|
||||
|
||||
# 渲染模板
|
||||
return self.render_template('django/models.mako', context)
|
||||
|
||||
def generate_model_with_relationships(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[Dict[str, Any]],
|
||||
relationships: List[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""
|
||||
生成包含关系字段的模型代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段配置列表
|
||||
relationships: 关系字段配置列表
|
||||
|
||||
Returns:
|
||||
str: 生成的模型代码
|
||||
"""
|
||||
if relationships:
|
||||
# 将关系字段添加到字段列表中
|
||||
for rel in relationships:
|
||||
fields.append(rel)
|
||||
|
||||
return self.generate(
|
||||
model_name=model_name,
|
||||
fields=fields,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def generate_abstract_model(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[Dict[str, Any]],
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""
|
||||
生成抽象模型代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段配置列表
|
||||
|
||||
Returns:
|
||||
str: 生成的抽象模型代码
|
||||
"""
|
||||
kwargs['abstract'] = True
|
||||
return self.generate(
|
||||
model_name=model_name,
|
||||
fields=fields,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def generate_proxy_model(
|
||||
self,
|
||||
model_name: str,
|
||||
base_model: str,
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""
|
||||
生成代理模型代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
base_model: 基础模型名称
|
||||
|
||||
Returns:
|
||||
str: 生成的代理模型代码
|
||||
"""
|
||||
kwargs['proxy'] = True
|
||||
kwargs['base_model'] = base_model
|
||||
return self.generate(
|
||||
model_name=model_name,
|
||||
fields=[],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def validate_field_config(self, field_config: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
验证字段配置
|
||||
|
||||
Args:
|
||||
field_config: 字段配置字典
|
||||
|
||||
Returns:
|
||||
bool: 验证是否通过
|
||||
"""
|
||||
required_keys = ['name', 'type']
|
||||
for key in required_keys:
|
||||
if key not in field_config:
|
||||
return False
|
||||
|
||||
# 验证字段类型
|
||||
valid_types = {
|
||||
'CharField', 'TextField', 'IntegerField', 'FloatField',
|
||||
'DecimalField', 'BooleanField', 'DateField', 'DateTimeField',
|
||||
'EmailField', 'URLField', 'ImageField', 'FileField',
|
||||
'ForeignKey', 'ManyToManyField', 'OneToOneField', 'JSONField',
|
||||
'SlugField', 'PositiveIntegerField', 'BigIntegerField',
|
||||
'SmallIntegerField', 'UUIDField', 'TimeField'
|
||||
}
|
||||
|
||||
if field_config['type'] not in valid_types:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def add_timestamp_fields(self, fields: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
添加时间戳字段
|
||||
|
||||
Args:
|
||||
fields: 原字段列表
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 包含时间戳字段的字段列表
|
||||
"""
|
||||
timestamp_fields = [
|
||||
{
|
||||
'name': 'created_at',
|
||||
'type': 'DateTimeField',
|
||||
'auto_now_add': True,
|
||||
'verbose_name': '创建时间',
|
||||
'help_text': '记录创建时间'
|
||||
},
|
||||
{
|
||||
'name': 'updated_at',
|
||||
'type': 'DateTimeField',
|
||||
'auto_now': True,
|
||||
'verbose_name': '更新时间',
|
||||
'help_text': '记录最后更新时间'
|
||||
}
|
||||
]
|
||||
|
||||
return fields + timestamp_fields
|
||||
|
||||
def add_status_field(self, fields: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
添加状态字段
|
||||
|
||||
Args:
|
||||
fields: 原字段列表
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 包含状态字段的字段列表
|
||||
"""
|
||||
status_field = {
|
||||
'name': 'status',
|
||||
'type': 'IntegerField',
|
||||
'choices': 'StatusChoices.choices',
|
||||
'default': 'StatusChoices.ENABLED',
|
||||
'verbose_name': '状态'
|
||||
}
|
||||
|
||||
return fields + [status_field]
|
||||
336
hertz_studio_django_utils/code_generator/serializer_generator.py
Normal file
336
hertz_studio_django_utils/code_generator/serializer_generator.py
Normal file
@@ -0,0 +1,336 @@
|
||||
"""
|
||||
Django序列化器代码生成器
|
||||
|
||||
该模块负责根据配置生成Django REST Framework序列化器代码
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
from .base_generator import BaseGenerator
|
||||
|
||||
|
||||
class SerializerGenerator(BaseGenerator):
|
||||
"""Django序列化器代码生成器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化序列化器生成器"""
|
||||
super().__init__()
|
||||
|
||||
def generate(self, model_name: str = None, fields: List[Any] = None, **kwargs) -> str:
|
||||
"""
|
||||
生成Django序列化器代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段列表(可以是字符串列表或字典列表)
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
str: 生成的序列化器代码
|
||||
"""
|
||||
# 处理参数,支持位置参数和关键字参数
|
||||
if model_name is None:
|
||||
model_name = kwargs.get('model_name', 'DefaultModel')
|
||||
if fields is None:
|
||||
fields = kwargs.get('fields', [])
|
||||
|
||||
# 处理字段列表,统一转换为字典格式
|
||||
processed_fields = []
|
||||
|
||||
for field in fields:
|
||||
if isinstance(field, str):
|
||||
# 字符串字段转换为字典格式
|
||||
field_config = {
|
||||
'name': field,
|
||||
'type': 'CharField',
|
||||
'create': True,
|
||||
'update': True,
|
||||
'list': True,
|
||||
'required': True,
|
||||
'read_only': False
|
||||
}
|
||||
processed_fields.append(field_config)
|
||||
elif isinstance(field, dict):
|
||||
# 字典字段,确保有必要的属性
|
||||
field_config = {
|
||||
'name': field.get('name', 'unknown_field'),
|
||||
'type': field.get('type', 'CharField'),
|
||||
'create': field.get('create', True),
|
||||
'update': field.get('update', True),
|
||||
'list': field.get('list', True),
|
||||
'required': field.get('required', True),
|
||||
'read_only': field.get('read_only', False)
|
||||
}
|
||||
processed_fields.append(field_config)
|
||||
|
||||
# 添加默认字段(如果不存在)
|
||||
field_names = [f['name'] for f in processed_fields]
|
||||
|
||||
if 'id' not in field_names:
|
||||
processed_fields.insert(0, {
|
||||
'name': 'id',
|
||||
'type': 'IntegerField',
|
||||
'create': False,
|
||||
'update': False,
|
||||
'list': True,
|
||||
'required': False,
|
||||
'read_only': True
|
||||
})
|
||||
|
||||
if 'created_at' not in field_names:
|
||||
processed_fields.append({
|
||||
'name': 'created_at',
|
||||
'type': 'DateTimeField',
|
||||
'create': False,
|
||||
'update': False,
|
||||
'list': True,
|
||||
'required': False,
|
||||
'read_only': True
|
||||
})
|
||||
|
||||
if 'updated_at' not in field_names:
|
||||
processed_fields.append({
|
||||
'name': 'updated_at',
|
||||
'type': 'DateTimeField',
|
||||
'create': False,
|
||||
'update': False,
|
||||
'list': True,
|
||||
'required': False,
|
||||
'read_only': True
|
||||
})
|
||||
|
||||
# 生成字段列表
|
||||
create_fields_list = [f['name'] for f in processed_fields if f.get('create', False)]
|
||||
update_fields_list = [f['name'] for f in processed_fields if f.get('update', False)]
|
||||
list_fields_list = [f['name'] for f in processed_fields if f.get('list', False)]
|
||||
|
||||
# 准备模板上下文
|
||||
context = {
|
||||
'model_name': model_name,
|
||||
'model_name_lower': model_name.lower(),
|
||||
'fields': processed_fields,
|
||||
'create_fields_list': create_fields_list,
|
||||
'update_fields_list': update_fields_list,
|
||||
'list_fields_list': list_fields_list,
|
||||
'has_create_serializer': bool(create_fields_list),
|
||||
'has_update_serializer': bool(update_fields_list),
|
||||
'has_list_serializer': bool(list_fields_list)
|
||||
}
|
||||
|
||||
# 渲染模板
|
||||
return self.render_template('django/serializers.mako', context)
|
||||
|
||||
def generate_crud_serializers(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[str],
|
||||
create_fields: Optional[List[str]] = None,
|
||||
update_fields: Optional[List[str]] = None,
|
||||
list_fields: Optional[List[str]] = None,
|
||||
validators: Optional[Dict[str, str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成CRUD序列化器代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段列表
|
||||
create_fields: 创建时使用的字段
|
||||
update_fields: 更新时使用的字段
|
||||
list_fields: 列表时显示的字段
|
||||
validators: 字段验证器映射
|
||||
|
||||
Returns:
|
||||
str: 生成的序列化器代码
|
||||
"""
|
||||
# 默认字段配置
|
||||
if create_fields is None:
|
||||
create_fields = [f for f in fields if f not in ['id', 'created_at', 'updated_at']]
|
||||
if update_fields is None:
|
||||
update_fields = [f for f in fields if f not in ['id', 'created_at', 'updated_at']]
|
||||
if list_fields is None:
|
||||
list_fields = fields
|
||||
|
||||
# 构建字段配置
|
||||
field_configs = []
|
||||
for field_name in fields:
|
||||
field_config = {
|
||||
'name': field_name,
|
||||
'create': field_name in create_fields,
|
||||
'update': field_name in update_fields,
|
||||
'list': field_name in list_fields,
|
||||
'validators': validators.get(field_name, {}) if validators else {}
|
||||
}
|
||||
field_configs.append(field_config)
|
||||
|
||||
return self.generate(
|
||||
model_name=model_name,
|
||||
fields=field_configs
|
||||
)
|
||||
|
||||
def generate_nested_serializer(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[str],
|
||||
nested_fields: Dict[str, Dict[str, Any]]
|
||||
) -> str:
|
||||
"""
|
||||
生成嵌套序列化器代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段列表
|
||||
nested_fields: 嵌套字段配置
|
||||
|
||||
Returns:
|
||||
str: 生成的嵌套序列化器代码
|
||||
"""
|
||||
# 构建字段配置,包含嵌套字段信息
|
||||
field_configs = []
|
||||
for field_name in fields:
|
||||
field_config = {
|
||||
'name': field_name,
|
||||
'nested': field_name in nested_fields,
|
||||
'nested_config': nested_fields.get(field_name, {})
|
||||
}
|
||||
field_configs.append(field_config)
|
||||
|
||||
return self.generate(
|
||||
model_name=model_name,
|
||||
fields=field_configs,
|
||||
has_nested=True
|
||||
)
|
||||
|
||||
def generate_read_only_serializer(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[str]
|
||||
) -> str:
|
||||
"""
|
||||
生成只读序列化器代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段列表
|
||||
|
||||
Returns:
|
||||
str: 生成的只读序列化器代码
|
||||
"""
|
||||
field_configs = []
|
||||
for field_name in fields:
|
||||
field_config = {
|
||||
'name': field_name,
|
||||
'read_only': True
|
||||
}
|
||||
field_configs.append(field_config)
|
||||
|
||||
return self.generate(
|
||||
model_name=model_name,
|
||||
fields=field_configs,
|
||||
read_only=True
|
||||
)
|
||||
|
||||
def generate_create_serializer(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[str],
|
||||
validators: Optional[Dict[str, str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成创建序列化器代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段列表
|
||||
validators: 字段验证器映射
|
||||
|
||||
Returns:
|
||||
str: 生成的创建序列化器代码
|
||||
"""
|
||||
field_configs = []
|
||||
for field_name in fields:
|
||||
field_config = {
|
||||
'name': field_name,
|
||||
'validators': validators.get(field_name, {}) if validators else {}
|
||||
}
|
||||
field_configs.append(field_config)
|
||||
|
||||
return self.generate(
|
||||
model_name=model_name,
|
||||
fields=field_configs,
|
||||
serializer_type='create'
|
||||
)
|
||||
|
||||
def generate_update_serializer(
|
||||
self,
|
||||
model_name: str,
|
||||
fields: List[str],
|
||||
validators: Optional[Dict[str, str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成更新序列化器代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
fields: 字段列表
|
||||
validators: 字段验证器映射
|
||||
|
||||
Returns:
|
||||
str: 生成的更新序列化器代码
|
||||
"""
|
||||
field_configs = []
|
||||
for field_name in fields:
|
||||
field_config = {
|
||||
'name': field_name,
|
||||
'validators': validators.get(field_name, {}) if validators else {}
|
||||
}
|
||||
field_configs.append(field_config)
|
||||
|
||||
return self.generate(
|
||||
model_name=model_name,
|
||||
fields=field_configs,
|
||||
serializer_type='update'
|
||||
)
|
||||
|
||||
def add_custom_validation(
|
||||
self,
|
||||
serializer_code: str,
|
||||
field_name: str,
|
||||
validation_code: str
|
||||
) -> str:
|
||||
"""
|
||||
添加自定义验证方法
|
||||
|
||||
Args:
|
||||
serializer_code: 原序列化器代码
|
||||
field_name: 字段名称
|
||||
validation_code: 验证代码
|
||||
|
||||
Returns:
|
||||
str: 包含自定义验证的序列化器代码
|
||||
"""
|
||||
validation_method = f"""
|
||||
def validate_{field_name}(self, value):
|
||||
\"\"\"
|
||||
验证{field_name}字段
|
||||
\"\"\"
|
||||
{validation_code}
|
||||
return value
|
||||
"""
|
||||
|
||||
# 在类定义结束前插入验证方法
|
||||
lines = serializer_code.split('\n')
|
||||
insert_index = -1
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith('class ') and 'Serializer' in line:
|
||||
# 找到类定义结束的位置
|
||||
for j in range(i + 1, len(lines)):
|
||||
if lines[j].strip() and not lines[j].startswith(' '):
|
||||
insert_index = j
|
||||
break
|
||||
break
|
||||
|
||||
if insert_index > 0:
|
||||
lines.insert(insert_index, validation_method)
|
||||
|
||||
return '\n'.join(lines)
|
||||
248
hertz_studio_django_utils/code_generator/template_engine.py
Normal file
248
hertz_studio_django_utils/code_generator/template_engine.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
模板引擎类
|
||||
|
||||
基于Mako模板引擎的代码生成模板管理器
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
from mako.template import Template
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako.exceptions import TemplateLookupException, CompileException
|
||||
|
||||
|
||||
class TemplateEngine:
|
||||
"""
|
||||
模板引擎类
|
||||
|
||||
负责管理和渲染Mako模板
|
||||
"""
|
||||
|
||||
def __init__(self, template_dir: str = None):
|
||||
"""
|
||||
初始化模板引擎
|
||||
|
||||
Args:
|
||||
template_dir: 模板目录路径
|
||||
"""
|
||||
if template_dir is None:
|
||||
# 默认模板目录
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
template_dir = os.path.join(current_dir, 'templates')
|
||||
|
||||
self.template_dir = template_dir
|
||||
self.lookup = TemplateLookup(
|
||||
directories=[template_dir],
|
||||
module_directory=os.path.join(template_dir, '.mako_modules'),
|
||||
input_encoding='utf-8',
|
||||
output_encoding='utf-8',
|
||||
encoding_errors='replace'
|
||||
)
|
||||
|
||||
def render_template(self, template_name: str, context: Dict[str, Any]) -> str:
|
||||
"""
|
||||
渲染模板
|
||||
|
||||
Args:
|
||||
template_name: 模板文件名
|
||||
context: 模板上下文变量
|
||||
|
||||
Returns:
|
||||
str: 渲染后的代码字符串
|
||||
|
||||
Raises:
|
||||
TemplateLookupException: 模板不存在
|
||||
CompileException: 模板编译错误
|
||||
"""
|
||||
try:
|
||||
template = self.lookup.get_template(template_name)
|
||||
rendered = template.render(**context)
|
||||
# 确保返回字符串而不是bytes
|
||||
if isinstance(rendered, bytes):
|
||||
return rendered.decode('utf-8')
|
||||
return rendered
|
||||
except TemplateLookupException as e:
|
||||
raise TemplateLookupException(f"模板 '{template_name}' 不存在: {str(e)}")
|
||||
except CompileException as e:
|
||||
raise CompileException(f"模板 '{template_name}' 编译错误: {str(e)}")
|
||||
except Exception as e:
|
||||
raise Exception(f"渲染模板 '{template_name}' 时发生错误: {str(e)}")
|
||||
|
||||
def render_django_model(self, context: Dict[str, Any]) -> str:
|
||||
"""
|
||||
渲染Django模型模板
|
||||
|
||||
Args:
|
||||
context: 模板上下文
|
||||
|
||||
Returns:
|
||||
str: 生成的模型代码
|
||||
"""
|
||||
return self.render_template('django/models.mako', context)
|
||||
|
||||
def render_django_serializer(self, context: Dict[str, Any]) -> str:
|
||||
"""
|
||||
渲染Django序列化器模板
|
||||
|
||||
Args:
|
||||
context: 模板上下文
|
||||
|
||||
Returns:
|
||||
str: 生成的序列化器代码
|
||||
"""
|
||||
return self.render_template('django/serializers.mako', context)
|
||||
|
||||
def render_django_view(self, context: Dict[str, Any]) -> str:
|
||||
"""
|
||||
渲染Django视图模板
|
||||
|
||||
Args:
|
||||
context: 模板上下文
|
||||
|
||||
Returns:
|
||||
str: 生成的视图代码
|
||||
"""
|
||||
return self.render_template('django/views.mako', context)
|
||||
|
||||
def render_django_url(self, context: Dict[str, Any]) -> str:
|
||||
"""
|
||||
渲染Django URL配置模板
|
||||
|
||||
Args:
|
||||
context: 模板上下文
|
||||
|
||||
Returns:
|
||||
str: 生成的URL配置代码
|
||||
"""
|
||||
return self.render_template('django/urls.mako', context)
|
||||
|
||||
def render_django_app(self, context: Dict[str, Any]) -> str:
|
||||
"""
|
||||
渲染Django应用配置模板
|
||||
|
||||
Args:
|
||||
context: 模板上下文
|
||||
|
||||
Returns:
|
||||
str: 生成的应用配置代码
|
||||
"""
|
||||
return self.render_template('django/apps.mako', context)
|
||||
|
||||
def get_available_templates(self) -> list:
|
||||
"""
|
||||
获取可用的模板列表
|
||||
|
||||
Returns:
|
||||
list: 模板文件列表
|
||||
"""
|
||||
templates = []
|
||||
for root, dirs, files in os.walk(self.template_dir):
|
||||
for file in files:
|
||||
if file.endswith('.mako'):
|
||||
rel_path = os.path.relpath(os.path.join(root, file), self.template_dir)
|
||||
templates.append(rel_path.replace('\\', '/'))
|
||||
return templates
|
||||
|
||||
def template_exists(self, template_name: str) -> bool:
|
||||
"""
|
||||
检查模板是否存在
|
||||
|
||||
Args:
|
||||
template_name: 模板名称
|
||||
|
||||
Returns:
|
||||
bool: 模板是否存在
|
||||
"""
|
||||
try:
|
||||
self.lookup.get_template(template_name)
|
||||
return True
|
||||
except TemplateLookupException:
|
||||
return False
|
||||
|
||||
def validate_template(self, template_name: str) -> tuple:
|
||||
"""
|
||||
验证模板语法
|
||||
|
||||
Args:
|
||||
template_name: 模板名称
|
||||
|
||||
Returns:
|
||||
tuple: (是否有效, 错误信息)
|
||||
"""
|
||||
try:
|
||||
template = self.lookup.get_template(template_name)
|
||||
# 尝试编译模板
|
||||
template.code
|
||||
return True, None
|
||||
except CompileException as e:
|
||||
return False, f"模板编译错误: {str(e)}"
|
||||
except TemplateLookupException as e:
|
||||
return False, f"模板不存在: {str(e)}"
|
||||
except Exception as e:
|
||||
return False, f"未知错误: {str(e)}"
|
||||
|
||||
def create_context(self, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
创建模板上下文
|
||||
|
||||
Args:
|
||||
**kwargs: 上下文变量
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 模板上下文
|
||||
"""
|
||||
return kwargs
|
||||
|
||||
def add_template_directory(self, directory: str) -> None:
|
||||
"""
|
||||
添加模板目录
|
||||
|
||||
Args:
|
||||
directory: 模板目录路径
|
||||
"""
|
||||
if os.path.exists(directory):
|
||||
self.lookup.directories.append(directory)
|
||||
else:
|
||||
raise FileNotFoundError(f"模板目录不存在: {directory}")
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""
|
||||
清除模板缓存
|
||||
"""
|
||||
self.lookup._collection.clear()
|
||||
|
||||
def get_template_info(self, template_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取模板信息
|
||||
|
||||
Args:
|
||||
template_name: 模板名称
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 模板信息
|
||||
"""
|
||||
try:
|
||||
template = self.lookup.get_template(template_name)
|
||||
template_path = template.filename
|
||||
|
||||
info = {
|
||||
'name': template_name,
|
||||
'path': template_path,
|
||||
'exists': True,
|
||||
'size': os.path.getsize(template_path) if template_path else 0,
|
||||
'modified_time': os.path.getmtime(template_path) if template_path else None
|
||||
}
|
||||
|
||||
# 验证模板
|
||||
is_valid, error = self.validate_template(template_name)
|
||||
info['valid'] = is_valid
|
||||
info['error'] = error
|
||||
|
||||
return info
|
||||
except TemplateLookupException:
|
||||
return {
|
||||
'name': template_name,
|
||||
'exists': False,
|
||||
'valid': False,
|
||||
'error': '模板不存在'
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from mako import runtime, filters, cache
|
||||
UNDEFINED = runtime.UNDEFINED
|
||||
STOP_RENDERING = runtime.STOP_RENDERING
|
||||
__M_dict_builtin = dict
|
||||
__M_locals_builtin = locals
|
||||
_magic_number = 10
|
||||
_modified_time = 1760426258.0375054
|
||||
_enable_loop = True
|
||||
_template_filename = 'C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/models.mako'
|
||||
_template_uri = 'django/models.mako'
|
||||
_source_encoding = 'utf-8'
|
||||
_exports = []
|
||||
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def render_body(context,**pageargs):
|
||||
__M_caller = context.caller_stack._push_frame()
|
||||
try:
|
||||
__M_locals = __M_dict_builtin(pageargs=pageargs)
|
||||
any = context.get('any', UNDEFINED)
|
||||
chr = context.get('chr', UNDEFINED)
|
||||
ordering = context.get('ordering', UNDEFINED)
|
||||
fields = context.get('fields', UNDEFINED)
|
||||
verbose_name = context.get('verbose_name', UNDEFINED)
|
||||
model_name = context.get('model_name', UNDEFINED)
|
||||
str = context.get('str', UNDEFINED)
|
||||
table_name = context.get('table_name', UNDEFINED)
|
||||
status_choices = context.get('status_choices', UNDEFINED)
|
||||
tuple = context.get('tuple', UNDEFINED)
|
||||
isinstance = context.get('isinstance', UNDEFINED)
|
||||
len = context.get('len', UNDEFINED)
|
||||
__M_writer = context.writer()
|
||||
__M_writer('"""\r\nDjango模型模板\r\n"""\r\n')
|
||||
__M_writer('\r\n')
|
||||
|
||||
# 导入必要的模块
|
||||
imports = []
|
||||
if any(field.get('type') == 'ForeignKey' for field in fields):
|
||||
imports.append('from django.contrib.auth import get_user_model')
|
||||
if any(field.get('type') in ['DateTimeField', 'DateField', 'TimeField'] for field in fields):
|
||||
imports.append('from django.utils import timezone')
|
||||
if any(field.get('choices') for field in fields):
|
||||
imports.append('from django.core.validators import validate_email')
|
||||
|
||||
# 生成状态选择项
|
||||
status_choices_code = ""
|
||||
if status_choices:
|
||||
choices_list = []
|
||||
for choice in status_choices:
|
||||
if isinstance(choice, tuple) and len(choice) == 2:
|
||||
choices_list.append(f" ('{choice[0]}', '{choice[1]}')")
|
||||
else:
|
||||
choices_list.append(f" ('{choice}', '{choice}')")
|
||||
status_choices_code = f""" STATUS_CHOICES = [
|
||||
{',{}'.format(chr(10)).join(choices_list)},
|
||||
]"""
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['field','choice','choices_list','imports','status_choices_code'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer('from django.db import models\r\n')
|
||||
for import_line in imports:
|
||||
__M_writer(str(import_line))
|
||||
__M_writer('\r\n')
|
||||
__M_writer('\r\nclass ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('(models.Model):\r\n """\r\n ')
|
||||
__M_writer(str(verbose_name or model_name))
|
||||
__M_writer('模型\r\n')
|
||||
if table_name:
|
||||
__M_writer(' \r\n 数据表名: ')
|
||||
__M_writer(str(table_name))
|
||||
__M_writer('\r\n')
|
||||
__M_writer(' 创建时间: ')
|
||||
__M_writer(str(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
||||
__M_writer('\r\n """\r\n')
|
||||
if status_choices_code:
|
||||
__M_writer(str(status_choices_code))
|
||||
__M_writer('\r\n\r\n')
|
||||
for field in fields:
|
||||
|
||||
field_name = field['name']
|
||||
# 使用转换后的Django字段类型
|
||||
field_type = field.get('type', 'CharField')
|
||||
field_options = []
|
||||
|
||||
# 处理字段选项
|
||||
if field.get('verbose_name'):
|
||||
field_options.append(f"verbose_name='{field['verbose_name']}'")
|
||||
if field.get('help_text'):
|
||||
field_options.append(f"help_text='{field['help_text']}'")
|
||||
if field.get('max_length'):
|
||||
field_options.append(f"max_length={field['max_length']}")
|
||||
if field.get('null'):
|
||||
field_options.append(f"null={field['null']}")
|
||||
if field.get('blank'):
|
||||
field_options.append(f"blank={field['blank']}")
|
||||
if field.get('default') is not None:
|
||||
if isinstance(field['default'], str):
|
||||
field_options.append(f"default='{field['default']}'")
|
||||
else:
|
||||
field_options.append(f"default={field['default']}")
|
||||
if field.get('unique'):
|
||||
field_options.append(f"unique={field['unique']}")
|
||||
if field.get('db_index'):
|
||||
field_options.append(f"db_index={field['db_index']}")
|
||||
|
||||
# 处理特殊字段类型
|
||||
if field_type == 'ForeignKey':
|
||||
if field.get('to'):
|
||||
field_options.insert(0, f"'{field['to']}'")
|
||||
else:
|
||||
field_options.insert(0, "get_user_model()")
|
||||
if field.get('on_delete'):
|
||||
field_options.append(f"on_delete=models.{field['on_delete']}")
|
||||
else:
|
||||
field_options.append("on_delete=models.CASCADE")
|
||||
elif field_type == 'ManyToManyField':
|
||||
if field.get('to'):
|
||||
field_options.insert(0, f"'{field['to']}'")
|
||||
elif field_type == 'OneToOneField':
|
||||
if field.get('to'):
|
||||
field_options.insert(0, f"'{field['to']}'")
|
||||
if field.get('on_delete'):
|
||||
field_options.append(f"on_delete=models.{field['on_delete']}")
|
||||
else:
|
||||
field_options.append("on_delete=models.CASCADE")
|
||||
|
||||
# 处理选择项
|
||||
if field.get('choices'):
|
||||
if field_name == 'status' and status_choices:
|
||||
field_options.append("choices=STATUS_CHOICES")
|
||||
else:
|
||||
choices_list = []
|
||||
for choice in field['choices']:
|
||||
if isinstance(choice, tuple) and len(choice) == 2:
|
||||
choices_list.append(f"('{choice[0]}', '{choice[1]}')")
|
||||
else:
|
||||
choices_list.append(f"('{choice}', '{choice}')")
|
||||
field_options.append(f"choices=[{', '.join(choices_list)}]")
|
||||
|
||||
# 处理特殊字段类型的额外参数
|
||||
if field_type == 'DecimalField':
|
||||
if not any('max_digits' in opt for opt in field_options):
|
||||
field_options.append('max_digits=10')
|
||||
if not any('decimal_places' in opt for opt in field_options):
|
||||
field_options.append('decimal_places=2')
|
||||
elif field_type == 'DateTimeField':
|
||||
if field_name == 'created_at' and not any('auto_now_add' in opt for opt in field_options):
|
||||
field_options.append('auto_now_add=True')
|
||||
elif field_name == 'updated_at' and not any('auto_now' in opt for opt in field_options):
|
||||
field_options.append('auto_now=True')
|
||||
|
||||
options_str = ', '.join(field_options) if field_options else ''
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['field_options','options_str','choice','choices_list','opt','field_name','field_type'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer('\r\n ')
|
||||
__M_writer(str(field_name))
|
||||
__M_writer(' = models.')
|
||||
__M_writer(str(field_type))
|
||||
__M_writer('(')
|
||||
__M_writer(str(options_str))
|
||||
__M_writer(')\r\n')
|
||||
__M_writer('\r\n class Meta:\r\n')
|
||||
if table_name:
|
||||
__M_writer(" db_table = '")
|
||||
__M_writer(str(table_name))
|
||||
__M_writer("'\r\n")
|
||||
__M_writer(" verbose_name = '")
|
||||
__M_writer(str(verbose_name or model_name))
|
||||
__M_writer("'\r\n verbose_name_plural = '")
|
||||
__M_writer(str(verbose_name or model_name))
|
||||
__M_writer("'\r\n")
|
||||
if ordering:
|
||||
__M_writer(' ordering = [')
|
||||
__M_writer(str(', '.join([f"'{field}'" for field in ordering])))
|
||||
__M_writer(']\r\n')
|
||||
else:
|
||||
__M_writer(" ordering = ['-created_at']\r\n")
|
||||
__M_writer('\r\n def __str__(self):\r\n """字符串表示"""\r\n')
|
||||
if fields:
|
||||
__M_writer(' ')
|
||||
|
||||
# 寻找合适的字段作为字符串表示
|
||||
str_field = None
|
||||
for field in fields:
|
||||
if field['name'] in ['name', 'title', 'username', 'email']:
|
||||
str_field = field['name']
|
||||
break
|
||||
if not str_field:
|
||||
# 如果没有找到合适的字段,使用第一个字符串字段
|
||||
for field in fields:
|
||||
if field['type'] in ['CharField', 'TextField', 'EmailField']:
|
||||
str_field = field['name']
|
||||
break
|
||||
if not str_field:
|
||||
str_field = 'id'
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['field','str_field'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer('\r\n return str(self.')
|
||||
__M_writer(str(str_field))
|
||||
__M_writer(')\r\n')
|
||||
else:
|
||||
__M_writer(' return f"')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('({self.id})"\r\n')
|
||||
__M_writer('\r\n def save(self, *args, **kwargs):\r\n """保存方法"""\r\n super().save(*args, **kwargs)\r\n\r\n @classmethod\r\n def get_by_id(cls, obj_id):\r\n """根据ID获取对象"""\r\n try:\r\n return cls.objects.get(id=obj_id)\r\n except cls.DoesNotExist:\r\n return None')
|
||||
return ''
|
||||
finally:
|
||||
context.caller_stack._pop_frame()
|
||||
|
||||
|
||||
"""
|
||||
__M_BEGIN_METADATA
|
||||
{"filename": "C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/models.mako", "uri": "django/models.mako", "source_encoding": "utf-8", "line_map": {"16": 4, "17": 5, "18": 6, "19": 7, "20": 0, "37": 1, "38": 6, "39": 7, "40": 8, "41": 9, "42": 10, "43": 11, "44": 12, "45": 13, "46": 14, "47": 15, "48": 16, "49": 17, "50": 18, "51": 19, "52": 20, "53": 21, "54": 22, "55": 23, "56": 24, "57": 25, "58": 26, "59": 27, "60": 28, "61": 29, "62": 30, "65": 30, "66": 31, "67": 32, "68": 32, "69": 34, "70": 35, "71": 35, "72": 37, "73": 37, "74": 38, "75": 39, "76": 40, "77": 40, "78": 42, "79": 42, "80": 42, "81": 44, "82": 45, "83": 45, "84": 48, "85": 49, "86": 50, "87": 51, "88": 52, "89": 53, "90": 54, "91": 55, "92": 56, "93": 57, "94": 58, "95": 59, "96": 60, "97": 61, "98": 62, "99": 63, "100": 64, "101": 65, "102": 66, "103": 67, "104": 68, "105": 69, "106": 70, "107": 71, "108": 72, "109": 73, "110": 74, "111": 75, "112": 76, "113": 77, "114": 78, "115": 79, "116": 80, "117": 81, "118": 82, "119": 83, "120": 84, "121": 85, "122": 86, "123": 87, "124": 88, "125": 89, "126": 90, "127": 91, "128": 92, "129": 93, "130": 94, "131": 95, "132": 96, "133": 97, "134": 98, "135": 99, "136": 100, "137": 101, "138": 102, "139": 103, "140": 104, "141": 105, "142": 106, "143": 107, "144": 108, "145": 109, "146": 110, "147": 111, "148": 112, "149": 113, "150": 114, "151": 115, "152": 116, "153": 117, "154": 118, "155": 119, "156": 120, "157": 121, "158": 122, "159": 123, "160": 124, "163": 123, "164": 124, "165": 124, "166": 124, "167": 124, "168": 124, "169": 124, "170": 126, "171": 128, "172": 129, "173": 129, "174": 129, "175": 131, "176": 131, "177": 131, "178": 132, "179": 132, "180": 133, "181": 134, "182": 134, "183": 134, "184": 135, "185": 136, "186": 138, "187": 141, "188": 142, "189": 142, "190": 143, "191": 144, "192": 145, "193": 146, "194": 147, "195": 148, "196": 149, "197": 150, "198": 151, "199": 152, "200": 153, "201": 154, "202": 155, "203": 156, "204": 157, "205": 158, "208": 157, "209": 158, "210": 158, "211": 159, "212": 160, "213": 160, "214": 160, "215": 162, "221": 215}}
|
||||
__M_END_METADATA
|
||||
"""
|
||||
@@ -0,0 +1,173 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from mako import runtime, filters, cache
|
||||
UNDEFINED = runtime.UNDEFINED
|
||||
STOP_RENDERING = runtime.STOP_RENDERING
|
||||
__M_dict_builtin = dict
|
||||
__M_locals_builtin = locals
|
||||
_magic_number = 10
|
||||
_modified_time = 1760424185.601884
|
||||
_enable_loop = True
|
||||
_template_filename = 'C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/serializers.mako'
|
||||
_template_uri = 'django/serializers.mako'
|
||||
_source_encoding = 'utf-8'
|
||||
_exports = []
|
||||
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def render_body(context,**pageargs):
|
||||
__M_caller = context.caller_stack._push_frame()
|
||||
try:
|
||||
__M_locals = __M_dict_builtin(pageargs=pageargs)
|
||||
model_name = context.get('model_name', UNDEFINED)
|
||||
fields = context.get('fields', UNDEFINED)
|
||||
__M_writer = context.writer()
|
||||
__M_writer('"""\nDjango序列化器模板\n"""\n')
|
||||
__M_writer('\n')
|
||||
|
||||
# 生成字段列表
|
||||
all_fields = []
|
||||
create_fields_list = []
|
||||
update_fields_list = []
|
||||
list_fields_list = []
|
||||
|
||||
# 处理字段列表
|
||||
for field in fields:
|
||||
field_name = field['name']
|
||||
all_fields.append(field_name)
|
||||
|
||||
# 排除自动生成的字段
|
||||
if field_name not in ['id', 'created_at', 'updated_at']:
|
||||
create_fields_list.append(field_name)
|
||||
update_fields_list.append(field_name)
|
||||
|
||||
list_fields_list.append(field_name)
|
||||
|
||||
# 添加默认字段
|
||||
if 'id' not in all_fields:
|
||||
all_fields.insert(0, 'id')
|
||||
if 'created_at' not in all_fields:
|
||||
all_fields.append('created_at')
|
||||
if 'updated_at' not in all_fields:
|
||||
all_fields.append('updated_at')
|
||||
|
||||
# 如果没有指定列表字段,使用所有字段
|
||||
if not list_fields_list:
|
||||
list_fields_list = all_fields
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['all_fields','field_name','update_fields_list','field','create_fields_list','list_fields_list'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer('\nfrom rest_framework import serializers\nfrom .models import ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('\n\n\nclass ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('Serializer(serializers.ModelSerializer):\n """\n ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('序列化器\n \n 用于')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('模型的序列化和反序列化\n 创建时间: ')
|
||||
__M_writer(str(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
||||
__M_writer('\n """\n \n class Meta:\n model = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('\n fields = [')
|
||||
__M_writer(str(', '.join([f"'{field}'" for field in all_fields])))
|
||||
__M_writer("]\n read_only_fields = ['id', 'created_at', 'updated_at']\n \n")
|
||||
for field in fields:
|
||||
pass
|
||||
if field.get('validators'):
|
||||
__M_writer(' def validate_')
|
||||
__M_writer(str(field['name']))
|
||||
__M_writer('(self, value):\n """\n 验证')
|
||||
__M_writer(str(field['name']))
|
||||
__M_writer('字段\n """\n')
|
||||
for validator_name, validator_rule in field['validators'].items():
|
||||
__M_writer(' # ')
|
||||
__M_writer(str(validator_name))
|
||||
__M_writer('验证: ')
|
||||
__M_writer(str(validator_rule))
|
||||
__M_writer('\n')
|
||||
__M_writer(' return value\n \n')
|
||||
__M_writer(' def validate(self, attrs):\n """\n 对象级别的验证\n """\n # 在这里添加跨字段验证逻辑\n return attrs\n \n def create(self, validated_data):\n """\n 创建')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('实例\n """\n return ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('.objects.create(**validated_data)\n \n def update(self, instance, validated_data):\n """\n 更新')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('实例\n """\n for attr, value in validated_data.items():\n setattr(instance, attr, value)\n instance.save()\n return instance\n\n\nclass ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('CreateSerializer(serializers.ModelSerializer):\n """\n ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('创建序列化器\n \n 用于创建')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('实例\n """\n \n class Meta:\n model = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('\n fields = [')
|
||||
__M_writer(str(', '.join([f"'{field}'" for field in create_fields_list])))
|
||||
__M_writer(']\n \n')
|
||||
for field in fields:
|
||||
pass
|
||||
if field['name'] in create_fields_list and field.get('validators'):
|
||||
__M_writer(' def validate_')
|
||||
__M_writer(str(field['name']))
|
||||
__M_writer('(self, value):\n """\n 验证')
|
||||
__M_writer(str(field['name']))
|
||||
__M_writer('字段\n """\n')
|
||||
for validator_name, validator_rule in field['validators'].items():
|
||||
__M_writer(' # ')
|
||||
__M_writer(str(validator_name))
|
||||
__M_writer('验证: ')
|
||||
__M_writer(str(validator_rule))
|
||||
__M_writer('\n')
|
||||
__M_writer(' return value\n \n')
|
||||
__M_writer('\n\nclass ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('UpdateSerializer(serializers.ModelSerializer):\n """\n ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('更新序列化器\n \n 用于更新')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('实例\n """\n \n class Meta:\n model = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('\n fields = [')
|
||||
__M_writer(str(', '.join([f"'{field}'" for field in update_fields_list])))
|
||||
__M_writer(']\n \n')
|
||||
for field in fields:
|
||||
pass
|
||||
if field['name'] in update_fields_list and field.get('validators'):
|
||||
__M_writer(' def validate_')
|
||||
__M_writer(str(field['name']))
|
||||
__M_writer('(self, value):\n """\n 验证')
|
||||
__M_writer(str(field['name']))
|
||||
__M_writer('字段\n """\n')
|
||||
for validator_name, validator_rule in field['validators'].items():
|
||||
__M_writer(' # ')
|
||||
__M_writer(str(validator_name))
|
||||
__M_writer('验证: ')
|
||||
__M_writer(str(validator_rule))
|
||||
__M_writer('\n')
|
||||
__M_writer(' return value\n \n')
|
||||
__M_writer('\n\nclass ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('ListSerializer(serializers.ModelSerializer):\n """\n ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('列表序列化器\n \n 用于列表显示')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('实例\n """\n \n class Meta:\n model = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('\n fields = [')
|
||||
__M_writer(str(', '.join([f"'{field}'" for field in list_fields_list])))
|
||||
__M_writer(']\n read_only_fields = [')
|
||||
__M_writer(str(', '.join([f"'{field}'" for field in list_fields_list])))
|
||||
__M_writer(']')
|
||||
return ''
|
||||
finally:
|
||||
context.caller_stack._pop_frame()
|
||||
|
||||
|
||||
"""
|
||||
__M_BEGIN_METADATA
|
||||
{"filename": "C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/serializers.mako", "uri": "django/serializers.mako", "source_encoding": "utf-8", "line_map": {"16": 4, "17": 5, "18": 6, "19": 7, "20": 0, "27": 1, "28": 6, "29": 7, "30": 8, "31": 9, "32": 10, "33": 11, "34": 12, "35": 13, "36": 14, "37": 15, "38": 16, "39": 17, "40": 18, "41": 19, "42": 20, "43": 21, "44": 22, "45": 23, "46": 24, "47": 25, "48": 26, "49": 27, "50": 28, "51": 29, "52": 30, "53": 31, "54": 32, "55": 33, "56": 34, "57": 35, "58": 36, "59": 37, "60": 38, "63": 37, "64": 39, "65": 39, "66": 42, "67": 42, "68": 44, "69": 44, "70": 46, "71": 46, "72": 47, "73": 47, "74": 51, "75": 51, "76": 52, "77": 52, "78": 55, "80": 56, "81": 57, "82": 57, "83": 57, "84": 59, "85": 59, "86": 61, "87": 62, "88": 62, "89": 62, "90": 62, "91": 62, "92": 64, "93": 68, "94": 77, "95": 77, "96": 79, "97": 79, "98": 83, "99": 83, "100": 91, "101": 91, "102": 93, "103": 93, "104": 95, "105": 95, "106": 99, "107": 99, "108": 100, "109": 100, "110": 102, "112": 103, "113": 104, "114": 104, "115": 104, "116": 106, "117": 106, "118": 108, "119": 109, "120": 109, "121": 109, "122": 109, "123": 109, "124": 111, "125": 115, "126": 117, "127": 117, "128": 119, "129": 119, "130": 121, "131": 121, "132": 125, "133": 125, "134": 126, "135": 126, "136": 128, "138": 129, "139": 130, "140": 130, "141": 130, "142": 132, "143": 132, "144": 134, "145": 135, "146": 135, "147": 135, "148": 135, "149": 135, "150": 137, "151": 141, "152": 143, "153": 143, "154": 145, "155": 145, "156": 147, "157": 147, "158": 151, "159": 151, "160": 152, "161": 152, "162": 153, "163": 153, "169": 163}}
|
||||
__M_END_METADATA
|
||||
"""
|
||||
@@ -0,0 +1,129 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from mako import runtime, filters, cache
|
||||
UNDEFINED = runtime.UNDEFINED
|
||||
STOP_RENDERING = runtime.STOP_RENDERING
|
||||
__M_dict_builtin = dict
|
||||
__M_locals_builtin = locals
|
||||
_magic_number = 10
|
||||
_modified_time = 1760498624.445131
|
||||
_enable_loop = True
|
||||
_template_filename = 'C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/urls.mako'
|
||||
_template_uri = 'django/urls.mako'
|
||||
_source_encoding = 'utf-8'
|
||||
_exports = []
|
||||
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def render_body(context,**pageargs):
|
||||
__M_caller = context.caller_stack._push_frame()
|
||||
try:
|
||||
__M_locals = __M_dict_builtin(pageargs=pageargs)
|
||||
app_name = context.get('app_name', UNDEFINED)
|
||||
operations = context.get('operations', UNDEFINED)
|
||||
model_name = context.get('model_name', UNDEFINED)
|
||||
prefix = context.get('prefix', UNDEFINED)
|
||||
__M_writer = context.writer()
|
||||
__M_writer('"""\nDjango URL配置模板\n"""\n')
|
||||
__M_writer('\n')
|
||||
|
||||
# 生成操作列表
|
||||
operations_list = operations or ['create', 'get', 'update', 'delete', 'list']
|
||||
snake_model_name = model_name.lower()
|
||||
resource_name = snake_model_name
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['operations_list','resource_name','snake_model_name'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer("\nfrom django.urls import path\nfrom . import views\n\napp_name = '")
|
||||
__M_writer(str(app_name or snake_model_name))
|
||||
__M_writer("'\n\nurlpatterns = [\n")
|
||||
if 'create' in operations_list:
|
||||
__M_writer(' # 创建')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("\n path('', views.create_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer(", name='create_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("'),\n")
|
||||
__M_writer(' \n')
|
||||
if 'list' in operations_list:
|
||||
__M_writer(' # 获取')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("列表\n path('list/', views.list_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer(", name='list_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("'),\n")
|
||||
__M_writer(' \n')
|
||||
if 'get' in operations_list:
|
||||
__M_writer(' # 获取')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("详情\n path('<int:")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("_id>/', views.get_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer(", name='get_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("'),\n")
|
||||
__M_writer(' \n')
|
||||
if 'update' in operations_list:
|
||||
__M_writer(' # 更新')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("\n path('<int:")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("_id>/update/', views.update_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer(", name='update_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("'),\n")
|
||||
__M_writer(' \n')
|
||||
if 'delete' in operations_list:
|
||||
__M_writer(' # 删除')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("\n path('<int:")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("_id>/delete/', views.delete_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer(", name='delete_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("'),\n")
|
||||
__M_writer(']\n\n# RESTful风格的URL配置(可选)\nrestful_urlpatterns = [\n')
|
||||
if 'create' in operations_list or 'list' in operations_list:
|
||||
__M_writer(' # POST: 创建')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer(', GET: 获取')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("列表\n path('")
|
||||
__M_writer(str(prefix))
|
||||
__M_writer(str(resource_name))
|
||||
__M_writer("/', views.")
|
||||
__M_writer(str('create_' + snake_model_name if 'create' in operations_list else 'list_' + snake_model_name))
|
||||
__M_writer(", name='")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("_collection'),\n")
|
||||
__M_writer(' \n')
|
||||
if 'get' in operations_list or 'update' in operations_list or 'delete' in operations_list:
|
||||
__M_writer(" # GET: 获取详情, PUT/PATCH: 更新, DELETE: 删除\n path('")
|
||||
__M_writer(str(prefix))
|
||||
__M_writer(str(resource_name))
|
||||
__M_writer('/<int:')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("_id>/', views.update_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer(", name='")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("_detail'),\n")
|
||||
__M_writer(']')
|
||||
return ''
|
||||
finally:
|
||||
context.caller_stack._pop_frame()
|
||||
|
||||
|
||||
"""
|
||||
__M_BEGIN_METADATA
|
||||
{"filename": "C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/urls.mako", "uri": "django/urls.mako", "source_encoding": "utf-8", "line_map": {"16": 4, "17": 5, "18": 6, "19": 7, "20": 0, "29": 1, "30": 6, "31": 7, "32": 8, "33": 9, "34": 10, "35": 11, "36": 12, "37": 13, "40": 12, "41": 16, "42": 16, "43": 19, "44": 20, "45": 20, "46": 20, "47": 21, "48": 21, "49": 21, "50": 21, "51": 23, "52": 24, "53": 25, "54": 25, "55": 25, "56": 26, "57": 26, "58": 26, "59": 26, "60": 28, "61": 29, "62": 30, "63": 30, "64": 30, "65": 31, "66": 31, "67": 31, "68": 31, "69": 31, "70": 31, "71": 33, "72": 34, "73": 35, "74": 35, "75": 35, "76": 36, "77": 36, "78": 36, "79": 36, "80": 36, "81": 36, "82": 38, "83": 39, "84": 40, "85": 40, "86": 40, "87": 41, "88": 41, "89": 41, "90": 41, "91": 41, "92": 41, "93": 43, "94": 47, "95": 48, "96": 48, "97": 48, "98": 48, "99": 48, "100": 49, "101": 49, "102": 49, "103": 49, "104": 49, "105": 49, "106": 49, "107": 51, "108": 52, "109": 53, "110": 54, "111": 54, "112": 54, "113": 54, "114": 54, "115": 54, "116": 54, "117": 54, "118": 54, "119": 56, "125": 119}}
|
||||
__M_END_METADATA
|
||||
"""
|
||||
@@ -0,0 +1,328 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from mako import runtime, filters, cache
|
||||
UNDEFINED = runtime.UNDEFINED
|
||||
STOP_RENDERING = runtime.STOP_RENDERING
|
||||
__M_dict_builtin = dict
|
||||
__M_locals_builtin = locals
|
||||
_magic_number = 10
|
||||
_modified_time = 1760498624.4341366
|
||||
_enable_loop = True
|
||||
_template_filename = 'C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/views.mako'
|
||||
_template_uri = 'django/views.mako'
|
||||
_source_encoding = 'utf-8'
|
||||
_exports = []
|
||||
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def render_body(context,**pageargs):
|
||||
__M_caller = context.caller_stack._push_frame()
|
||||
try:
|
||||
__M_locals = __M_dict_builtin(pageargs=pageargs)
|
||||
operations = context.get('operations', UNDEFINED)
|
||||
pagination = context.get('pagination', UNDEFINED)
|
||||
isinstance = context.get('isinstance', UNDEFINED)
|
||||
permissions = context.get('permissions', UNDEFINED)
|
||||
dict = context.get('dict', UNDEFINED)
|
||||
ordering = context.get('ordering', UNDEFINED)
|
||||
model_name = context.get('model_name', UNDEFINED)
|
||||
search_fields = context.get('search_fields', UNDEFINED)
|
||||
__M_writer = context.writer()
|
||||
__M_writer('"""\nDjango视图模板\n"""\n')
|
||||
__M_writer('\n')
|
||||
|
||||
# 生成操作列表
|
||||
operations_list = operations or ['create', 'get', 'update', 'delete', 'list']
|
||||
permissions_list = permissions or []
|
||||
snake_model_name = model_name.lower()
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['operations_list','snake_model_name','permissions_list'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer('\nfrom rest_framework.decorators import api_view\nfrom rest_framework.response import Response\nfrom rest_framework import status\nfrom drf_spectacular.utils import extend_schema, OpenApiResponse\nfrom django.core.paginator import Paginator\nfrom django.db.models import Q\n\nfrom .models import ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('\nfrom .serializers import (\n ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('Serializer,\n ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('CreateSerializer,\n ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('UpdateSerializer,\n ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('ListSerializer\n)\nfrom hertz_studio_django_utils.responses import HertzResponse\n')
|
||||
if permissions_list:
|
||||
__M_writer('from hertz_studio_django_auth.utils.decorators import login_required, permission_required\n')
|
||||
__M_writer('\n\n')
|
||||
if 'create' in operations_list:
|
||||
__M_writer("@extend_schema(\n operation_id='create_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("',\n summary='创建")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("',\n description='创建新的")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("实例',\n request=")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('CreateSerializer,\n responses={\n 201: OpenApiResponse(response=')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("Serializer, description='创建成功'),\n 400: OpenApiResponse(description='参数错误'),\n },\n tags=['")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("']\n)\n@api_view(['POST'])\n")
|
||||
if 'create' in permissions_list:
|
||||
|
||||
# 获取权限代码,如果permissions_list是字典,则获取对应的权限代码
|
||||
if isinstance(permissions_list, dict) and 'create' in permissions_list:
|
||||
permission_code = permissions_list['create']
|
||||
else:
|
||||
permission_code = f'{snake_model_name}:create'
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['permission_code'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer("\n@permission_required('")
|
||||
__M_writer(str(permission_code))
|
||||
__M_writer("')\n")
|
||||
__M_writer('def create_')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('(request):\n """\n 创建')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('\n \n 创建时间: ')
|
||||
__M_writer(str(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
||||
__M_writer('\n """\n try:\n serializer = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('CreateSerializer(data=request.data)\n if serializer.is_valid():\n instance = serializer.save()\n response_serializer = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("Serializer(instance)\n return HertzResponse.success(\n data=response_serializer.data,\n message='")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("创建成功'\n )\n return HertzResponse.validation_error(\n message='参数验证失败',\n errors=serializer.errors\n )\n except Exception as e:\n return HertzResponse.error(\n message='创建")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("失败',\n error=str(e)\n )\n\n\n")
|
||||
if 'get' in operations_list or 'retrieve' in operations_list:
|
||||
__M_writer("@extend_schema(\n operation_id='get_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("',\n summary='获取")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("详情',\n description='根据ID获取")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("详情',\n responses={\n 200: OpenApiResponse(response=")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("Serializer, description='获取成功'),\n 404: OpenApiResponse(description='")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("不存在'),\n },\n tags=['")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("']\n)\n@api_view(['GET'])\n")
|
||||
if 'get' in permissions_list or 'retrieve' in permissions_list:
|
||||
|
||||
# 获取权限代码
|
||||
if isinstance(permissions_list, dict):
|
||||
permission_code = permissions_list.get('get') or permissions_list.get('retrieve') or f'{snake_model_name}:query'
|
||||
else:
|
||||
permission_code = f'{snake_model_name}:query'
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['permission_code'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer("\n@permission_required('")
|
||||
__M_writer(str(permission_code))
|
||||
__M_writer("')\n")
|
||||
__M_writer('def get_')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('(request, ')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('_id):\n """\n 获取')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('详情\n \n Args:\n ')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('_id: ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('ID\n """\n try:\n instance = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('.get_by_id(')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("_id)\n if not instance:\n return HertzResponse.not_found(message='")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("不存在')\n \n serializer = ")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("Serializer(instance)\n return HertzResponse.success(\n data=serializer.data,\n message='获取")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("详情成功'\n )\n except Exception as e:\n return HertzResponse.error(\n message='获取")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("详情失败',\n error=str(e)\n )\n\n\n")
|
||||
if 'update' in operations_list:
|
||||
__M_writer("@extend_schema(\n operation_id='update_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("',\n summary='更新")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("',\n description='根据ID更新")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("信息',\n request=")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('UpdateSerializer,\n responses={\n 200: OpenApiResponse(response=')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("Serializer, description='更新成功'),\n 404: OpenApiResponse(description='")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("不存在'),\n 400: OpenApiResponse(description='参数错误'),\n },\n tags=['")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("']\n)\n@api_view(['PUT', 'PATCH'])\n")
|
||||
if 'update' in permissions_list:
|
||||
|
||||
# 获取权限代码
|
||||
if isinstance(permissions_list, dict) and 'update' in permissions_list:
|
||||
permission_code = permissions_list['update']
|
||||
else:
|
||||
permission_code = f'{snake_model_name}:update'
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['permission_code'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer("\n@permission_required('")
|
||||
__M_writer(str(permission_code))
|
||||
__M_writer("')\n")
|
||||
__M_writer('def update_')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('(request, ')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('_id):\n """\n 更新')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('\n \n Args:\n ')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('_id: ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('ID\n """\n try:\n instance = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('.get_by_id(')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("_id)\n if not instance:\n return HertzResponse.not_found(message='")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("不存在')\n \n partial = request.method == 'PATCH'\n serializer = ")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('UpdateSerializer(\n instance, \n data=request.data, \n partial=partial\n )\n \n if serializer.is_valid():\n updated_instance = serializer.save()\n response_serializer = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("Serializer(updated_instance)\n return HertzResponse.success(\n data=response_serializer.data,\n message='")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("更新成功'\n )\n return HertzResponse.validation_error(\n message='参数验证失败',\n errors=serializer.errors\n )\n except Exception as e:\n return HertzResponse.error(\n message='更新")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("失败',\n error=str(e)\n )\n\n\n")
|
||||
if 'delete' in operations_list:
|
||||
__M_writer("@extend_schema(\n operation_id='delete_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("',\n summary='删除")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("',\n description='根据ID删除")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("',\n responses={\n 200: OpenApiResponse(description='删除成功'),\n 404: OpenApiResponse(description='")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("不存在'),\n },\n tags=['")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("']\n)\n@api_view(['DELETE'])\n")
|
||||
if 'delete' in permissions_list:
|
||||
|
||||
# 获取权限代码
|
||||
if isinstance(permissions_list, dict) and 'delete' in permissions_list:
|
||||
permission_code = permissions_list['delete']
|
||||
else:
|
||||
permission_code = f'{snake_model_name}:delete'
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['permission_code'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer("\n@permission_required('")
|
||||
__M_writer(str(permission_code))
|
||||
__M_writer("')\n")
|
||||
__M_writer('def delete_')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('(request, ')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('_id):\n """\n 删除')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('\n \n Args:\n ')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('_id: ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('ID\n """\n try:\n instance = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('.get_by_id(')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("_id)\n if not instance:\n return HertzResponse.not_found(message='")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("不存在')\n \n instance.delete()\n return HertzResponse.success(message='")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("删除成功')\n except Exception as e:\n return HertzResponse.error(\n message='删除")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("失败',\n error=str(e)\n )\n\n\n")
|
||||
if 'list' in operations_list:
|
||||
__M_writer("@extend_schema(\n operation_id='list_")
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer("',\n summary='获取")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("列表',\n description='分页获取")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("列表',\n responses={\n 200: OpenApiResponse(response=")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("ListSerializer, description='获取成功'),\n },\n tags=['")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("']\n)\n@api_view(['GET'])\n")
|
||||
if 'list' in permissions_list:
|
||||
|
||||
# 获取权限代码
|
||||
if isinstance(permissions_list, dict) and 'list' in permissions_list:
|
||||
permission_code = permissions_list['list']
|
||||
else:
|
||||
permission_code = f'{snake_model_name}:list'
|
||||
|
||||
|
||||
__M_locals_builtin_stored = __M_locals_builtin()
|
||||
__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in ['permission_code'] if __M_key in __M_locals_builtin_stored]))
|
||||
__M_writer("\n@permission_required('")
|
||||
__M_writer(str(permission_code))
|
||||
__M_writer("')\n")
|
||||
__M_writer('def list_')
|
||||
__M_writer(str(snake_model_name))
|
||||
__M_writer('(request):\n """\n 获取')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer('列表\n \n 支持分页、搜索和排序\n """\n try:\n queryset = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer(".objects.all()\n \n # 搜索功能\n search = request.GET.get('search', '')\n if search:\n")
|
||||
if search_fields:
|
||||
__M_writer(' search_q = Q()\n')
|
||||
for field in search_fields:
|
||||
__M_writer(' search_q |= Q(')
|
||||
__M_writer(str(field))
|
||||
__M_writer('__icontains=search)\n')
|
||||
__M_writer(' queryset = queryset.filter(search_q)\n')
|
||||
else:
|
||||
__M_writer(' # 默认搜索字段,可根据需要调整\n queryset = queryset.filter(\n Q(id__icontains=search)\n )\n')
|
||||
__M_writer(" \n # 排序功能\n ordering = request.GET.get('ordering', '-created_at')\n")
|
||||
if ordering:
|
||||
__M_writer(' valid_orderings = [')
|
||||
__M_writer(str(', '.join([f"'{field}'" for field in ordering] + [f"'-{field}'" for field in ordering])))
|
||||
__M_writer(']\n')
|
||||
else:
|
||||
__M_writer(" valid_orderings = ['created_at', '-created_at', 'updated_at', '-updated_at']\n")
|
||||
__M_writer(' if ordering in valid_orderings:\n queryset = queryset.order_by(ordering)\n \n # 分页功能\n')
|
||||
if pagination:
|
||||
__M_writer(" page = int(request.GET.get('page', 1))\n page_size = int(request.GET.get('page_size', 20))\n paginator = Paginator(queryset, page_size)\n page_obj = paginator.get_page(page)\n \n serializer = ")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("ListSerializer(page_obj.object_list, many=True)\n \n return HertzResponse.success(\n data={\n 'results': serializer.data,\n 'pagination': {\n 'page': page,\n 'page_size': page_size,\n 'total_pages': paginator.num_pages,\n 'total_count': paginator.count,\n 'has_next': page_obj.has_next(),\n 'has_previous': page_obj.has_previous(),\n }\n },\n message='获取")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("列表成功'\n )\n")
|
||||
else:
|
||||
__M_writer(' serializer = ')
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("ListSerializer(queryset, many=True)\n return HertzResponse.success(\n data=serializer.data,\n message='获取")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("列表成功'\n )\n")
|
||||
__M_writer(" except Exception as e:\n return HertzResponse.error(\n message='获取")
|
||||
__M_writer(str(model_name))
|
||||
__M_writer("列表失败',\n error=str(e)\n )\n\n\n")
|
||||
return ''
|
||||
finally:
|
||||
context.caller_stack._pop_frame()
|
||||
|
||||
|
||||
"""
|
||||
__M_BEGIN_METADATA
|
||||
{"filename": "C:/2025.8/project/9/moban/hertz_server_django/hertz_studio_django_utils/code_generator/templates/django/views.mako", "uri": "django/views.mako", "source_encoding": "utf-8", "line_map": {"16": 4, "17": 5, "18": 6, "19": 7, "20": 0, "33": 1, "34": 6, "35": 7, "36": 8, "37": 9, "38": 10, "39": 11, "40": 12, "41": 13, "44": 12, "45": 20, "46": 20, "47": 22, "48": 22, "49": 23, "50": 23, "51": 24, "52": 24, "53": 25, "54": 25, "55": 28, "56": 29, "57": 31, "58": 33, "59": 34, "60": 35, "61": 35, "62": 36, "63": 36, "64": 37, "65": 37, "66": 38, "67": 38, "68": 40, "69": 40, "70": 43, "71": 43, "72": 46, "73": 47, "74": 48, "75": 49, "76": 50, "77": 51, "78": 52, "79": 53, "80": 54, "83": 53, "84": 54, "85": 54, "86": 56, "87": 56, "88": 56, "89": 58, "90": 58, "91": 60, "92": 60, "93": 63, "94": 63, "95": 66, "96": 66, "97": 69, "98": 69, "99": 77, "100": 77, "101": 83, "102": 84, "103": 85, "104": 85, "105": 86, "106": 86, "107": 87, "108": 87, "109": 89, "110": 89, "111": 90, "112": 90, "113": 92, "114": 92, "115": 95, "116": 96, "117": 97, "118": 98, "119": 99, "120": 100, "121": 101, "122": 102, "123": 103, "126": 102, "127": 103, "128": 103, "129": 105, "130": 105, "131": 105, "132": 105, "133": 105, "134": 107, "135": 107, "136": 110, "137": 110, "138": 110, "139": 110, "140": 113, "141": 113, "142": 113, "143": 113, "144": 115, "145": 115, "146": 117, "147": 117, "148": 120, "149": 120, "150": 124, "151": 124, "152": 130, "153": 131, "154": 132, "155": 132, "156": 133, "157": 133, "158": 134, "159": 134, "160": 135, "161": 135, "162": 137, "163": 137, "164": 138, "165": 138, "166": 141, "167": 141, "168": 144, "169": 145, "170": 146, "171": 147, "172": 148, "173": 149, "174": 150, "175": 151, "176": 152, "179": 151, "180": 152, "181": 152, "182": 154, "183": 154, "184": 154, "185": 154, "186": 154, "187": 156, "188": 156, "189": 159, "190": 159, "191": 159, "192": 159, "193": 162, "194": 162, "195": 162, "196": 162, "197": 164, "198": 164, "199": 167, "200": 167, "201": 175, "202": 175, "203": 178, "204": 178, "205": 186, "206": 186, "207": 192, "208": 193, "209": 194, "210": 194, "211": 195, "212": 195, "213": 196, "214": 196, "215": 199, "216": 199, "217": 201, "218": 201, "219": 204, "220": 205, "221": 206, "222": 207, "223": 208, "224": 209, "225": 210, "226": 211, "227": 212, "230": 211, "231": 212, "232": 212, "233": 214, "234": 214, "235": 214, "236": 214, "237": 214, "238": 216, "239": 216, "240": 219, "241": 219, "242": 219, "243": 219, "244": 222, "245": 222, "246": 222, "247": 222, "248": 224, "249": 224, "250": 227, "251": 227, "252": 230, "253": 230, "254": 236, "255": 237, "256": 238, "257": 238, "258": 239, "259": 239, "260": 240, "261": 240, "262": 242, "263": 242, "264": 244, "265": 244, "266": 247, "267": 248, "268": 249, "269": 250, "270": 251, "271": 252, "272": 253, "273": 254, "274": 255, "277": 254, "278": 255, "279": 255, "280": 257, "281": 257, "282": 257, "283": 259, "284": 259, "285": 264, "286": 264, "287": 269, "288": 270, "289": 271, "290": 272, "291": 272, "292": 272, "293": 274, "294": 275, "295": 276, "296": 281, "297": 284, "298": 285, "299": 285, "300": 285, "301": 286, "302": 287, "303": 289, "304": 293, "305": 294, "306": 299, "307": 299, "308": 313, "309": 313, "310": 315, "311": 316, "312": 316, "313": 316, "314": 319, "315": 319, "316": 322, "317": 324, "318": 324, "324": 318}}
|
||||
__M_END_METADATA
|
||||
"""
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Django应用配置模板
|
||||
"""
|
||||
<%!
|
||||
from datetime import datetime
|
||||
%>
|
||||
<%
|
||||
# 生成应用配置类名
|
||||
app_class_name = ''.join(word.capitalize() for word in app_name.split('_')) + 'Config'
|
||||
%>
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ${app_class_name}(AppConfig):
|
||||
"""
|
||||
${app_name}应用配置
|
||||
|
||||
创建时间: ${datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
"""
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = '${app_name}'
|
||||
verbose_name = '${verbose_name or app_name}'
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Django模型模板
|
||||
"""
|
||||
<%!
|
||||
from datetime import datetime
|
||||
%>
|
||||
<%
|
||||
# 导入必要的模块
|
||||
imports = []
|
||||
if any(field.get('type') == 'ForeignKey' for field in fields):
|
||||
imports.append('from django.contrib.auth import get_user_model')
|
||||
if any(field.get('type') in ['DateTimeField', 'DateField', 'TimeField'] for field in fields):
|
||||
imports.append('from django.utils import timezone')
|
||||
if any(field.get('choices') for field in fields):
|
||||
imports.append('from django.core.validators import validate_email')
|
||||
|
||||
# 生成状态选择项
|
||||
status_choices_code = ""
|
||||
if status_choices:
|
||||
choices_list = []
|
||||
for choice in status_choices:
|
||||
if isinstance(choice, tuple) and len(choice) == 2:
|
||||
choices_list.append(f" ('{choice[0]}', '{choice[1]}')")
|
||||
else:
|
||||
choices_list.append(f" ('{choice}', '{choice}')")
|
||||
status_choices_code = f""" STATUS_CHOICES = [
|
||||
{',{}'.format(chr(10)).join(choices_list)},
|
||||
]"""
|
||||
%>\
|
||||
from django.db import models
|
||||
% for import_line in imports:
|
||||
${import_line}
|
||||
% endfor
|
||||
|
||||
class ${model_name}(models.Model):
|
||||
"""
|
||||
${verbose_name or model_name}模型
|
||||
% if table_name:
|
||||
|
||||
数据表名: ${table_name}
|
||||
% endif
|
||||
创建时间: ${datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
"""
|
||||
% if status_choices_code:
|
||||
${status_choices_code}
|
||||
|
||||
% endif
|
||||
% for field in fields:
|
||||
<%
|
||||
field_name = field['name']
|
||||
# 使用转换后的Django字段类型
|
||||
field_type = field.get('type', 'CharField')
|
||||
field_options = []
|
||||
|
||||
# 处理字段选项
|
||||
if field.get('verbose_name'):
|
||||
field_options.append(f"verbose_name='{field['verbose_name']}'")
|
||||
if field.get('help_text'):
|
||||
field_options.append(f"help_text='{field['help_text']}'")
|
||||
if field.get('max_length'):
|
||||
field_options.append(f"max_length={field['max_length']}")
|
||||
if field.get('null'):
|
||||
field_options.append(f"null={field['null']}")
|
||||
if field.get('blank'):
|
||||
field_options.append(f"blank={field['blank']}")
|
||||
if field.get('default') is not None:
|
||||
if isinstance(field['default'], str):
|
||||
field_options.append(f"default='{field['default']}'")
|
||||
else:
|
||||
field_options.append(f"default={field['default']}")
|
||||
if field.get('unique'):
|
||||
field_options.append(f"unique={field['unique']}")
|
||||
if field.get('db_index'):
|
||||
field_options.append(f"db_index={field['db_index']}")
|
||||
|
||||
# 处理特殊字段类型
|
||||
if field_type == 'ForeignKey':
|
||||
if field.get('to'):
|
||||
field_options.insert(0, f"'{field['to']}'")
|
||||
else:
|
||||
field_options.insert(0, "get_user_model()")
|
||||
if field.get('on_delete'):
|
||||
field_options.append(f"on_delete=models.{field['on_delete']}")
|
||||
else:
|
||||
field_options.append("on_delete=models.CASCADE")
|
||||
elif field_type == 'ManyToManyField':
|
||||
if field.get('to'):
|
||||
field_options.insert(0, f"'{field['to']}'")
|
||||
elif field_type == 'OneToOneField':
|
||||
if field.get('to'):
|
||||
field_options.insert(0, f"'{field['to']}'")
|
||||
if field.get('on_delete'):
|
||||
field_options.append(f"on_delete=models.{field['on_delete']}")
|
||||
else:
|
||||
field_options.append("on_delete=models.CASCADE")
|
||||
|
||||
# 处理选择项
|
||||
if field.get('choices'):
|
||||
if field_name == 'status' and status_choices:
|
||||
field_options.append("choices=STATUS_CHOICES")
|
||||
else:
|
||||
choices_list = []
|
||||
for choice in field['choices']:
|
||||
if isinstance(choice, tuple) and len(choice) == 2:
|
||||
choices_list.append(f"('{choice[0]}', '{choice[1]}')")
|
||||
else:
|
||||
choices_list.append(f"('{choice}', '{choice}')")
|
||||
field_options.append(f"choices=[{', '.join(choices_list)}]")
|
||||
|
||||
# 处理特殊字段类型的额外参数
|
||||
if field_type == 'DecimalField':
|
||||
if not any('max_digits' in opt for opt in field_options):
|
||||
field_options.append('max_digits=10')
|
||||
if not any('decimal_places' in opt for opt in field_options):
|
||||
field_options.append('decimal_places=2')
|
||||
elif field_type == 'DateTimeField':
|
||||
if field_name == 'created_at' and not any('auto_now_add' in opt for opt in field_options):
|
||||
field_options.append('auto_now_add=True')
|
||||
elif field_name == 'updated_at' and not any('auto_now' in opt for opt in field_options):
|
||||
field_options.append('auto_now=True')
|
||||
|
||||
options_str = ', '.join(field_options) if field_options else ''
|
||||
%>
|
||||
${field_name} = models.${field_type}(${options_str})
|
||||
% endfor
|
||||
|
||||
class Meta:
|
||||
% if table_name:
|
||||
db_table = '${table_name}'
|
||||
% endif
|
||||
verbose_name = '${verbose_name or model_name}'
|
||||
verbose_name_plural = '${verbose_name or model_name}'
|
||||
% if ordering:
|
||||
ordering = [${', '.join([f"'{field}'" for field in ordering])}]
|
||||
% else:
|
||||
ordering = ['-created_at']
|
||||
% endif
|
||||
|
||||
def __str__(self):
|
||||
"""字符串表示"""
|
||||
% if fields:
|
||||
<%
|
||||
# 寻找合适的字段作为字符串表示
|
||||
str_field = None
|
||||
for field in fields:
|
||||
if field['name'] in ['name', 'title', 'username', 'email']:
|
||||
str_field = field['name']
|
||||
break
|
||||
if not str_field:
|
||||
# 如果没有找到合适的字段,使用第一个字符串字段
|
||||
for field in fields:
|
||||
if field['type'] in ['CharField', 'TextField', 'EmailField']:
|
||||
str_field = field['name']
|
||||
break
|
||||
if not str_field:
|
||||
str_field = 'id'
|
||||
%>
|
||||
return str(self.${str_field})
|
||||
% else:
|
||||
return f"${model_name}({self.id})"
|
||||
% endif
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""保存方法"""
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_by_id(cls, obj_id):
|
||||
"""根据ID获取对象"""
|
||||
try:
|
||||
return cls.objects.get(id=obj_id)
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Django主项目URL配置模板
|
||||
用于自动添加新app的URL路由
|
||||
"""
|
||||
<%!
|
||||
from datetime import datetime
|
||||
%>
|
||||
<%
|
||||
# 获取现有的URL配置
|
||||
existing_urls = url_patterns or []
|
||||
new_app_config = {
|
||||
'app_name': app_name,
|
||||
'url_path': url_path or f'api/{app_name.replace("hertz_studio_django_", "").replace("_", "/")}/',
|
||||
'comment': comment or f'{verbose_name or app_name} routes'
|
||||
}
|
||||
|
||||
# 检查URL是否已存在
|
||||
url_exists = any(config.get('app_name') == new_app_config['app_name'] for config in existing_urls)
|
||||
if not url_exists:
|
||||
existing_urls.append(new_app_config)
|
||||
%>
|
||||
"""
|
||||
URL configuration for hertz_server_django project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
# 首页路由
|
||||
path('', views.index, name='index'),
|
||||
|
||||
% for url_config in existing_urls:
|
||||
# ${url_config['comment']}
|
||||
path('${url_config['url_path']}', include('${url_config['app_name']}.urls')),
|
||||
|
||||
% endfor
|
||||
# API documentation routes
|
||||
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
]
|
||||
|
||||
# 在开发环境下提供媒体文件服务
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])
|
||||
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
Django序列化器模板
|
||||
"""
|
||||
<%!
|
||||
from datetime import datetime
|
||||
%>
|
||||
<%
|
||||
# 生成字段列表
|
||||
all_fields = []
|
||||
create_fields_list = []
|
||||
update_fields_list = []
|
||||
list_fields_list = []
|
||||
|
||||
# 处理字段列表
|
||||
for field in fields:
|
||||
field_name = field['name']
|
||||
all_fields.append(field_name)
|
||||
|
||||
# 排除自动生成的字段
|
||||
if field_name not in ['id', 'created_at', 'updated_at']:
|
||||
create_fields_list.append(field_name)
|
||||
update_fields_list.append(field_name)
|
||||
|
||||
list_fields_list.append(field_name)
|
||||
|
||||
# 添加默认字段
|
||||
if 'id' not in all_fields:
|
||||
all_fields.insert(0, 'id')
|
||||
if 'created_at' not in all_fields:
|
||||
all_fields.append('created_at')
|
||||
if 'updated_at' not in all_fields:
|
||||
all_fields.append('updated_at')
|
||||
|
||||
# 如果没有指定列表字段,使用所有字段
|
||||
if not list_fields_list:
|
||||
list_fields_list = all_fields
|
||||
%>
|
||||
from rest_framework import serializers
|
||||
from .models import ${model_name}
|
||||
|
||||
|
||||
class ${model_name}Serializer(serializers.ModelSerializer):
|
||||
"""
|
||||
${model_name}序列化器
|
||||
|
||||
用于${model_name}模型的序列化和反序列化
|
||||
创建时间: ${datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ${model_name}
|
||||
fields = [${', '.join([f"'{field}'" for field in all_fields])}]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
% for field in fields:
|
||||
% if field.get('validators'):
|
||||
def validate_${field['name']}(self, value):
|
||||
"""
|
||||
验证${field['name']}字段
|
||||
"""
|
||||
% for validator_name, validator_rule in field['validators'].items():
|
||||
# ${validator_name}验证: ${validator_rule}
|
||||
% endfor
|
||||
return value
|
||||
|
||||
% endif
|
||||
% endfor
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
对象级别的验证
|
||||
"""
|
||||
# 在这里添加跨字段验证逻辑
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
创建${model_name}实例
|
||||
"""
|
||||
return ${model_name}.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
更新${model_name}实例
|
||||
"""
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class ${model_name}CreateSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
${model_name}创建序列化器
|
||||
|
||||
用于创建${model_name}实例
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ${model_name}
|
||||
fields = [${', '.join([f"'{field}'" for field in create_fields_list])}]
|
||||
|
||||
% for field in fields:
|
||||
% if field['name'] in create_fields_list and field.get('validators'):
|
||||
def validate_${field['name']}(self, value):
|
||||
"""
|
||||
验证${field['name']}字段
|
||||
"""
|
||||
% for validator_name, validator_rule in field['validators'].items():
|
||||
# ${validator_name}验证: ${validator_rule}
|
||||
% endfor
|
||||
return value
|
||||
|
||||
% endif
|
||||
% endfor
|
||||
|
||||
|
||||
class ${model_name}UpdateSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
${model_name}更新序列化器
|
||||
|
||||
用于更新${model_name}实例
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ${model_name}
|
||||
fields = [${', '.join([f"'{field}'" for field in update_fields_list])}]
|
||||
|
||||
% for field in fields:
|
||||
% if field['name'] in update_fields_list and field.get('validators'):
|
||||
def validate_${field['name']}(self, value):
|
||||
"""
|
||||
验证${field['name']}字段
|
||||
"""
|
||||
% for validator_name, validator_rule in field['validators'].items():
|
||||
# ${validator_name}验证: ${validator_rule}
|
||||
% endfor
|
||||
return value
|
||||
|
||||
% endif
|
||||
% endfor
|
||||
|
||||
|
||||
class ${model_name}ListSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
${model_name}列表序列化器
|
||||
|
||||
用于列表显示${model_name}实例
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ${model_name}
|
||||
fields = [${', '.join([f"'{field}'" for field in list_fields_list])}]
|
||||
read_only_fields = [${', '.join([f"'{field}'" for field in list_fields_list])}]
|
||||
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
Django settings.py 配置模板
|
||||
用于自动添加新app到INSTALLED_APPS
|
||||
"""
|
||||
<%!
|
||||
from datetime import datetime
|
||||
%>
|
||||
<%
|
||||
# 获取现有的INSTALLED_APPS列表
|
||||
existing_apps = installed_apps or []
|
||||
new_app = app_name
|
||||
|
||||
# 检查app是否已存在
|
||||
if new_app not in existing_apps:
|
||||
existing_apps.append(new_app)
|
||||
%>
|
||||
"""
|
||||
Django settings for hertz_server_django project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.2.6.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from decouple import config
|
||||
|
||||
# 修复DRF的ip_address_validators函数
|
||||
def fix_drf_ip_validators():
|
||||
"""
|
||||
修复DRF的ip_address_validators函数返回值问题
|
||||
"""
|
||||
try:
|
||||
from rest_framework import fields
|
||||
|
||||
# 保存原始函数
|
||||
original_ip_address_validators = fields.ip_address_validators
|
||||
|
||||
def fixed_ip_address_validators(protocol, unpack_ipv4):
|
||||
"""
|
||||
修复后的ip_address_validators函数,确保返回两个值
|
||||
"""
|
||||
validators = original_ip_address_validators(protocol, unpack_ipv4)
|
||||
# 如果只返回了validators,添加默认的error_message
|
||||
if isinstance(validators, list):
|
||||
return validators, 'Enter a valid IP address.'
|
||||
else:
|
||||
# 如果已经返回了两个值,直接返回
|
||||
return validators
|
||||
|
||||
# 应用猴子补丁
|
||||
fields.ip_address_validators = fixed_ip_address_validators
|
||||
|
||||
except ImportError:
|
||||
# 如果DRF未安装,忽略错误
|
||||
pass
|
||||
|
||||
# 应用修复
|
||||
fix_drf_ip_validators()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = config('SECRET_KEY', default='django-insecure-0a1bx*8!97l^4z#ml#ufn_*9ut*)zlso$*k-g^h&(2=p@^51md')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = config('DEBUG', default=True, cast=bool)
|
||||
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||
|
||||
# Database switch configuration
|
||||
USE_REDIS_AS_DB = config('USE_REDIS_AS_DB', default=True, cast=bool)
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# Third party apps
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
'channels',
|
||||
'drf_spectacular',
|
||||
|
||||
# Local apps
|
||||
% for app in existing_apps:
|
||||
'${app}',
|
||||
% endfor
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'hertz_studio_django_auth.utils.middleware.AuthMiddleware', # 权限认证中间件
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'hertz_server_django.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / 'templates']
|
||||
,
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'hertz_server_django.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
if USE_REDIS_AS_DB:
|
||||
# Redis as primary database (for caching and session storage)
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'data/db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
# Use Redis for sessions
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
||||
SESSION_CACHE_ALIAS = 'default'
|
||||
|
||||
else:
|
||||
# MySQL database configuration
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': config('DB_NAME', default='hertz_server'),
|
||||
'USER': config('DB_USER', default='root'),
|
||||
'PASSWORD': config('DB_PASSWORD', default='root'),
|
||||
'HOST': config('DB_HOST', default='localhost'),
|
||||
'PORT': config('DB_PORT', default='3306'),
|
||||
'OPTIONS': {
|
||||
'charset': 'utf8mb4',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Redis
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django_redis.cache.RedisCache',
|
||||
'LOCATION': config('REDIS_URL', default='redis://127.0.0.1:6379/0'),
|
||||
'OPTIONS': {
|
||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / 'static',
|
||||
]
|
||||
|
||||
# Media files (User uploaded files)
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Django REST Framework configuration
|
||||
# 使用自定义AuthMiddleware进行认证,不使用DRF的认证和权限系统
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [], # 不使用DRF认证类
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.AllowAny', # 所有接口默认允许访问,由AuthMiddleware控制权限
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 20,
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
],
|
||||
}
|
||||
|
||||
# Spectacular (OpenAPI 3.0) configuration
|
||||
SPECTACULAR_SETTINGS = {
|
||||
'TITLE': 'Hertz Server API',
|
||||
'DESCRIPTION': 'API documentation for Hertz Server Django project',
|
||||
'VERSION': '1.0.0',
|
||||
'SERVE_INCLUDE_SCHEMA': False,
|
||||
'COMPONENT_SPLIT_REQUEST': True,
|
||||
'SCHEMA_PATH_PREFIX': '/api/',
|
||||
}
|
||||
|
||||
# CORS configuration
|
||||
CORS_ALLOWED_ORIGINS = config(
|
||||
'CORS_ALLOWED_ORIGINS',
|
||||
default='http://localhost:3000,http://127.0.0.1:3000',
|
||||
cast=lambda v: [s.strip() for s in v.split(',')]
|
||||
)
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = config('CORS_ALLOW_ALL_ORIGINS', default=False, cast=bool)
|
||||
|
||||
# Captcha settings
|
||||
CAPTCHA_IMAGE_SIZE = (
|
||||
config('CAPTCHA_IMAGE_SIZE_WIDTH', default=120, cast=int),
|
||||
config('CAPTCHA_IMAGE_SIZE_HEIGHT', default=50, cast=int)
|
||||
)
|
||||
CAPTCHA_LENGTH = config('CAPTCHA_LENGTH', default=4, cast=int)
|
||||
CAPTCHA_TIMEOUT = config('CAPTCHA_TIMEOUT', default=5, cast=int) # minutes
|
||||
CAPTCHA_FONT_SIZE = config('CAPTCHA_FONT_SIZE', default=40, cast=int)
|
||||
CAPTCHA_BACKGROUND_COLOR = config('CAPTCHA_BACKGROUND_COLOR', default='#ffffff')
|
||||
CAPTCHA_FOREGROUND_COLOR = config('CAPTCHA_FOREGROUND_COLOR', default='#000000')
|
||||
# 验证码词典文件路径
|
||||
CAPTCHA_WORDS_DICTIONARY = str(BASE_DIR / 'captcha_words.txt')
|
||||
# 验证码挑战函数配置
|
||||
CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' # 默认使用随机字符
|
||||
# 数学验证码配置
|
||||
CAPTCHA_MATH_CHALLENGE_OPERATOR = '+-*'
|
||||
# 验证码噪声和过滤器
|
||||
CAPTCHA_NOISE_FUNCTIONS = (
|
||||
'captcha.helpers.noise_arcs',
|
||||
'captcha.helpers.noise_dots',
|
||||
)
|
||||
CAPTCHA_FILTER_FUNCTIONS = (
|
||||
'captcha.helpers.post_smooth',
|
||||
)
|
||||
|
||||
# Email configuration
|
||||
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
|
||||
EMAIL_HOST = config('EMAIL_HOST', default='smtp.qq.com')
|
||||
EMAIL_PORT = config('EMAIL_PORT', default=465, cast=int)
|
||||
EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=True, cast=bool)
|
||||
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
|
||||
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='563161210@qq.com')
|
||||
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='563161210@qq.com')
|
||||
|
||||
# Channels configuration for WebSocket support
|
||||
ASGI_APPLICATION = 'hertz_server_django.asgi.application'
|
||||
|
||||
# Channel layers configuration
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
"hosts": [config('REDIS_URL', default='redis://127.0.0.1:6379/2')],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# 自定义用户模型
|
||||
AUTH_USER_MODEL = 'hertz_studio_django_auth.HertzUser'
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET_KEY = config('JWT_SECRET_KEY', default=SECRET_KEY)
|
||||
JWT_ALGORITHM = 'HS256'
|
||||
JWT_ACCESS_TOKEN_LIFETIME = config('JWT_ACCESS_TOKEN_LIFETIME', default=60 * 60 * 24, cast=int) # 24小时
|
||||
JWT_REFRESH_TOKEN_LIFETIME = config('JWT_REFRESH_TOKEN_LIFETIME', default=60 * 60 * 24 * 7, cast=int) # 7天
|
||||
|
||||
# 权限系统配置
|
||||
HERTZ_AUTH_SETTINGS = {
|
||||
'SUPER_ADMIN_PERMISSIONS': ['*'], # 超级管理员拥有所有权限
|
||||
'DEFAULT_PERMISSIONS': [], # 默认权限
|
||||
}
|
||||
|
||||
# AuthMiddleware配置 - 不需要登录验证的URL模式(支持正则表达式)
|
||||
NO_AUTH_PATTERNS = config(
|
||||
'NO_AUTH_PATTERNS',
|
||||
default=r'^/api/auth/login/?$,^/api/auth/register/?$,^/api/auth/email/code/?$,^/api/auth/send-email-code/?$,^/api/auth/password/reset/?$,^/api/captcha/.*$,^/api/docs/.*$,^/api/redoc/.*$,^/api/schema/.*$,^/admin/.*$,^/static/.*$,^/media/.*$,^/demo/.*$,^/websocket/.*$,^/api/system/.*$',
|
||||
cast=lambda v: [s.strip() for s in v.split(',')]
|
||||
)
|
||||
|
||||
# 密码加密配置
|
||||
PASSWORD_HASHERS = [
|
||||
'hertz_studio_django_utils.crypto.MD5PasswordHasher', # 使用MD5加密
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||
]
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Django URL配置模板
|
||||
"""
|
||||
<%!
|
||||
from datetime import datetime
|
||||
%>
|
||||
<%
|
||||
# 生成操作列表
|
||||
operations_list = operations or ['create', 'get', 'update', 'delete', 'list']
|
||||
snake_model_name = model_name.lower()
|
||||
resource_name = snake_model_name
|
||||
%>
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = '${app_name or snake_model_name}'
|
||||
|
||||
urlpatterns = [
|
||||
% if 'create' in operations_list:
|
||||
# 创建${model_name}
|
||||
path('', views.create_${snake_model_name}, name='create_${snake_model_name}'),
|
||||
% endif
|
||||
|
||||
% if 'list' in operations_list:
|
||||
# 获取${model_name}列表
|
||||
path('list/', views.list_${snake_model_name}, name='list_${snake_model_name}'),
|
||||
% endif
|
||||
|
||||
% if 'get' in operations_list:
|
||||
# 获取${model_name}详情
|
||||
path('<int:${snake_model_name}_id>/', views.get_${snake_model_name}, name='get_${snake_model_name}'),
|
||||
% endif
|
||||
|
||||
% if 'update' in operations_list:
|
||||
# 更新${model_name}
|
||||
path('<int:${snake_model_name}_id>/update/', views.update_${snake_model_name}, name='update_${snake_model_name}'),
|
||||
% endif
|
||||
|
||||
% if 'delete' in operations_list:
|
||||
# 删除${model_name}
|
||||
path('<int:${snake_model_name}_id>/delete/', views.delete_${snake_model_name}, name='delete_${snake_model_name}'),
|
||||
% endif
|
||||
]
|
||||
|
||||
# RESTful风格的URL配置(可选)
|
||||
restful_urlpatterns = [
|
||||
% if 'create' in operations_list or 'list' in operations_list:
|
||||
# POST: 创建${model_name}, GET: 获取${model_name}列表
|
||||
path('${prefix}${resource_name}/', views.${'create_' + snake_model_name if 'create' in operations_list else 'list_' + snake_model_name}, name='${snake_model_name}_collection'),
|
||||
% endif
|
||||
|
||||
% if 'get' in operations_list or 'update' in operations_list or 'delete' in operations_list:
|
||||
# GET: 获取详情, PUT/PATCH: 更新, DELETE: 删除
|
||||
path('${prefix}${resource_name}/<int:${snake_model_name}_id>/', views.update_${snake_model_name}, name='${snake_model_name}_detail'),
|
||||
% endif
|
||||
]
|
||||
@@ -0,0 +1,329 @@
|
||||
"""
|
||||
Django视图模板
|
||||
"""
|
||||
<%!
|
||||
from datetime import datetime
|
||||
%>
|
||||
<%
|
||||
# 生成操作列表
|
||||
operations_list = operations or ['create', 'get', 'update', 'delete', 'list']
|
||||
permissions_list = permissions or []
|
||||
snake_model_name = model_name.lower()
|
||||
%>
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
|
||||
from .models import ${model_name}
|
||||
from .serializers import (
|
||||
${model_name}Serializer,
|
||||
${model_name}CreateSerializer,
|
||||
${model_name}UpdateSerializer,
|
||||
${model_name}ListSerializer
|
||||
)
|
||||
from hertz_studio_django_utils.responses import HertzResponse
|
||||
% if permissions_list:
|
||||
from hertz_studio_django_auth.utils.decorators import login_required, permission_required
|
||||
% endif
|
||||
|
||||
|
||||
% if 'create' in operations_list:
|
||||
@extend_schema(
|
||||
operation_id='create_${snake_model_name}',
|
||||
summary='创建${model_name}',
|
||||
description='创建新的${model_name}实例',
|
||||
request=${model_name}CreateSerializer,
|
||||
responses={
|
||||
201: OpenApiResponse(response=${model_name}Serializer, description='创建成功'),
|
||||
400: OpenApiResponse(description='参数错误'),
|
||||
},
|
||||
tags=['${model_name}']
|
||||
)
|
||||
@api_view(['POST'])
|
||||
% if 'create' in permissions_list:
|
||||
<%
|
||||
# 获取权限代码,如果permissions_list是字典,则获取对应的权限代码
|
||||
if isinstance(permissions_list, dict) and 'create' in permissions_list:
|
||||
permission_code = permissions_list['create']
|
||||
else:
|
||||
permission_code = f'{snake_model_name}:create'
|
||||
%>
|
||||
@permission_required('${permission_code}')
|
||||
% endif
|
||||
def create_${snake_model_name}(request):
|
||||
"""
|
||||
创建${model_name}
|
||||
|
||||
创建时间: ${datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
"""
|
||||
try:
|
||||
serializer = ${model_name}CreateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
instance = serializer.save()
|
||||
response_serializer = ${model_name}Serializer(instance)
|
||||
return HertzResponse.success(
|
||||
data=response_serializer.data,
|
||||
message='${model_name}创建成功'
|
||||
)
|
||||
return HertzResponse.validation_error(
|
||||
message='参数验证失败',
|
||||
errors=serializer.errors
|
||||
)
|
||||
except Exception as e:
|
||||
return HertzResponse.error(
|
||||
message='创建${model_name}失败',
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
% endif
|
||||
% if 'get' in operations_list or 'retrieve' in operations_list:
|
||||
@extend_schema(
|
||||
operation_id='get_${snake_model_name}',
|
||||
summary='获取${model_name}详情',
|
||||
description='根据ID获取${model_name}详情',
|
||||
responses={
|
||||
200: OpenApiResponse(response=${model_name}Serializer, description='获取成功'),
|
||||
404: OpenApiResponse(description='${model_name}不存在'),
|
||||
},
|
||||
tags=['${model_name}']
|
||||
)
|
||||
@api_view(['GET'])
|
||||
% if 'get' in permissions_list or 'retrieve' in permissions_list:
|
||||
<%
|
||||
# 获取权限代码
|
||||
if isinstance(permissions_list, dict):
|
||||
permission_code = permissions_list.get('get') or permissions_list.get('retrieve') or f'{snake_model_name}:query'
|
||||
else:
|
||||
permission_code = f'{snake_model_name}:query'
|
||||
%>
|
||||
@permission_required('${permission_code}')
|
||||
% endif
|
||||
def get_${snake_model_name}(request, ${snake_model_name}_id):
|
||||
"""
|
||||
获取${model_name}详情
|
||||
|
||||
Args:
|
||||
${snake_model_name}_id: ${model_name}ID
|
||||
"""
|
||||
try:
|
||||
instance = ${model_name}.get_by_id(${snake_model_name}_id)
|
||||
if not instance:
|
||||
return HertzResponse.not_found(message='${model_name}不存在')
|
||||
|
||||
serializer = ${model_name}Serializer(instance)
|
||||
return HertzResponse.success(
|
||||
data=serializer.data,
|
||||
message='获取${model_name}详情成功'
|
||||
)
|
||||
except Exception as e:
|
||||
return HertzResponse.error(
|
||||
message='获取${model_name}详情失败',
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
% endif
|
||||
% if 'update' in operations_list:
|
||||
@extend_schema(
|
||||
operation_id='update_${snake_model_name}',
|
||||
summary='更新${model_name}',
|
||||
description='根据ID更新${model_name}信息',
|
||||
request=${model_name}UpdateSerializer,
|
||||
responses={
|
||||
200: OpenApiResponse(response=${model_name}Serializer, description='更新成功'),
|
||||
404: OpenApiResponse(description='${model_name}不存在'),
|
||||
400: OpenApiResponse(description='参数错误'),
|
||||
},
|
||||
tags=['${model_name}']
|
||||
)
|
||||
@api_view(['PUT', 'PATCH'])
|
||||
% if 'update' in permissions_list:
|
||||
<%
|
||||
# 获取权限代码
|
||||
if isinstance(permissions_list, dict) and 'update' in permissions_list:
|
||||
permission_code = permissions_list['update']
|
||||
else:
|
||||
permission_code = f'{snake_model_name}:update'
|
||||
%>
|
||||
@permission_required('${permission_code}')
|
||||
% endif
|
||||
def update_${snake_model_name}(request, ${snake_model_name}_id):
|
||||
"""
|
||||
更新${model_name}
|
||||
|
||||
Args:
|
||||
${snake_model_name}_id: ${model_name}ID
|
||||
"""
|
||||
try:
|
||||
instance = ${model_name}.get_by_id(${snake_model_name}_id)
|
||||
if not instance:
|
||||
return HertzResponse.not_found(message='${model_name}不存在')
|
||||
|
||||
partial = request.method == 'PATCH'
|
||||
serializer = ${model_name}UpdateSerializer(
|
||||
instance,
|
||||
data=request.data,
|
||||
partial=partial
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
updated_instance = serializer.save()
|
||||
response_serializer = ${model_name}Serializer(updated_instance)
|
||||
return HertzResponse.success(
|
||||
data=response_serializer.data,
|
||||
message='${model_name}更新成功'
|
||||
)
|
||||
return HertzResponse.validation_error(
|
||||
message='参数验证失败',
|
||||
errors=serializer.errors
|
||||
)
|
||||
except Exception as e:
|
||||
return HertzResponse.error(
|
||||
message='更新${model_name}失败',
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
% endif
|
||||
% if 'delete' in operations_list:
|
||||
@extend_schema(
|
||||
operation_id='delete_${snake_model_name}',
|
||||
summary='删除${model_name}',
|
||||
description='根据ID删除${model_name}',
|
||||
responses={
|
||||
200: OpenApiResponse(description='删除成功'),
|
||||
404: OpenApiResponse(description='${model_name}不存在'),
|
||||
},
|
||||
tags=['${model_name}']
|
||||
)
|
||||
@api_view(['DELETE'])
|
||||
% if 'delete' in permissions_list:
|
||||
<%
|
||||
# 获取权限代码
|
||||
if isinstance(permissions_list, dict) and 'delete' in permissions_list:
|
||||
permission_code = permissions_list['delete']
|
||||
else:
|
||||
permission_code = f'{snake_model_name}:delete'
|
||||
%>
|
||||
@permission_required('${permission_code}')
|
||||
% endif
|
||||
def delete_${snake_model_name}(request, ${snake_model_name}_id):
|
||||
"""
|
||||
删除${model_name}
|
||||
|
||||
Args:
|
||||
${snake_model_name}_id: ${model_name}ID
|
||||
"""
|
||||
try:
|
||||
instance = ${model_name}.get_by_id(${snake_model_name}_id)
|
||||
if not instance:
|
||||
return HertzResponse.not_found(message='${model_name}不存在')
|
||||
|
||||
instance.delete()
|
||||
return HertzResponse.success(message='${model_name}删除成功')
|
||||
except Exception as e:
|
||||
return HertzResponse.error(
|
||||
message='删除${model_name}失败',
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
% endif
|
||||
% if 'list' in operations_list:
|
||||
@extend_schema(
|
||||
operation_id='list_${snake_model_name}',
|
||||
summary='获取${model_name}列表',
|
||||
description='分页获取${model_name}列表',
|
||||
responses={
|
||||
200: OpenApiResponse(response=${model_name}ListSerializer, description='获取成功'),
|
||||
},
|
||||
tags=['${model_name}']
|
||||
)
|
||||
@api_view(['GET'])
|
||||
% if 'list' in permissions_list:
|
||||
<%
|
||||
# 获取权限代码
|
||||
if isinstance(permissions_list, dict) and 'list' in permissions_list:
|
||||
permission_code = permissions_list['list']
|
||||
else:
|
||||
permission_code = f'{snake_model_name}:list'
|
||||
%>
|
||||
@permission_required('${permission_code}')
|
||||
% endif
|
||||
def list_${snake_model_name}(request):
|
||||
"""
|
||||
获取${model_name}列表
|
||||
|
||||
支持分页、搜索和排序
|
||||
"""
|
||||
try:
|
||||
queryset = ${model_name}.objects.all()
|
||||
|
||||
# 搜索功能
|
||||
search = request.GET.get('search', '')
|
||||
if search:
|
||||
% if search_fields:
|
||||
search_q = Q()
|
||||
% for field in search_fields:
|
||||
search_q |= Q(${field}__icontains=search)
|
||||
% endfor
|
||||
queryset = queryset.filter(search_q)
|
||||
% else:
|
||||
# 默认搜索字段,可根据需要调整
|
||||
queryset = queryset.filter(
|
||||
Q(id__icontains=search)
|
||||
)
|
||||
% endif
|
||||
|
||||
# 排序功能
|
||||
ordering = request.GET.get('ordering', '-created_at')
|
||||
% if ordering:
|
||||
valid_orderings = [${', '.join([f"'{field}'" for field in ordering] + [f"'-{field}'" for field in ordering])}]
|
||||
% else:
|
||||
valid_orderings = ['created_at', '-created_at', 'updated_at', '-updated_at']
|
||||
% endif
|
||||
if ordering in valid_orderings:
|
||||
queryset = queryset.order_by(ordering)
|
||||
|
||||
# 分页功能
|
||||
% if pagination:
|
||||
page = int(request.GET.get('page', 1))
|
||||
page_size = int(request.GET.get('page_size', 20))
|
||||
paginator = Paginator(queryset, page_size)
|
||||
page_obj = paginator.get_page(page)
|
||||
|
||||
serializer = ${model_name}ListSerializer(page_obj.object_list, many=True)
|
||||
|
||||
return HertzResponse.success(
|
||||
data={
|
||||
'results': serializer.data,
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'total_pages': paginator.num_pages,
|
||||
'total_count': paginator.count,
|
||||
'has_next': page_obj.has_next(),
|
||||
'has_previous': page_obj.has_previous(),
|
||||
}
|
||||
},
|
||||
message='获取${model_name}列表成功'
|
||||
)
|
||||
% else:
|
||||
serializer = ${model_name}ListSerializer(queryset, many=True)
|
||||
return HertzResponse.success(
|
||||
data=serializer.data,
|
||||
message='获取${model_name}列表成功'
|
||||
)
|
||||
% endif
|
||||
except Exception as e:
|
||||
return HertzResponse.error(
|
||||
message='获取${model_name}列表失败',
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
|
||||
% endif
|
||||
6
hertz_studio_django_utils/code_generator/test/apps.py
Normal file
6
hertz_studio_django_utils/code_generator/test/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class hertz_studio_django_testConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'hertz_studio_django_test'
|
||||
verbose_name = '测试应用'
|
||||
30
hertz_studio_django_utils/code_generator/test/models.py
Normal file
30
hertz_studio_django_utils/code_generator/test/models.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.db import models
|
||||
from hertz_studio_django_auth.models import HertzUser
|
||||
import os
|
||||
|
||||
class Testmodels(models.Model):
|
||||
"""
|
||||
测试模型
|
||||
"""
|
||||
# 测试字段
|
||||
test_field = models.CharField(max_length=100, verbose_name='测试字段')
|
||||
name = models.CharField(max_length=50, unique=True, verbose_name='名称')
|
||||
test_path = models.CharField(max_length=100, verbose_name='测试路径')
|
||||
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='父项')
|
||||
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
update_at = models.DatetimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
|
||||
class Meta:
|
||||
db_table = test1
|
||||
verbose_name = '测试模型'
|
||||
verbose_name_plural = '测试模型'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_full_path(self):
|
||||
if self.parent:
|
||||
return f'{self.parent.get_full_path()} > {self.name}'
|
||||
return self.name
|
||||
24
hertz_studio_django_utils/code_generator/test/serializers.py
Normal file
24
hertz_studio_django_utils/code_generator/test/serializers.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils import timezone
|
||||
from models import *
|
||||
|
||||
class TestSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
测试序列化器
|
||||
"""
|
||||
parent_name = serializers.CharField(source='parent.name', read_only=True)
|
||||
children_count = serializers.SerializerMethodField()
|
||||
articales_count = serializers.SerializerMethodField()
|
||||
full_path = serializers.CharField(source='get_full_path',read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Testmodels
|
||||
fields=[
|
||||
'name','parent','is_active','full_path','created_at','updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
|
||||
def get_children_count(self, obj):
|
||||
"""获取子分类数量"""
|
||||
return obj.children.filter(is_active=True).count()
|
||||
6
hertz_studio_django_utils/code_generator/test/urls.py
Normal file
6
hertz_studio_django_utils/code_generator/test/urls.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.urls import path, include
|
||||
from views import *
|
||||
|
||||
urlpatterns = [
|
||||
path('test/', test, name='test'),
|
||||
]
|
||||
350
hertz_studio_django_utils/code_generator/url_generator.py
Normal file
350
hertz_studio_django_utils/code_generator/url_generator.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
Django URL配置代码生成器
|
||||
|
||||
该模块负责根据配置生成Django URL路由代码
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
from .base_generator import BaseGenerator
|
||||
|
||||
|
||||
class URLGenerator(BaseGenerator):
|
||||
"""Django URL配置代码生成器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化URL生成器"""
|
||||
super().__init__()
|
||||
|
||||
def generate(self, model_name: str = None, operations: List[str] = None, app_name: str = None, **kwargs) -> str:
|
||||
"""
|
||||
生成Django URL配置代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
operations: 支持的操作列表
|
||||
app_name: 应用名称
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
str: 生成的URL配置代码
|
||||
"""
|
||||
# 处理参数,支持位置参数和关键字参数
|
||||
if model_name is None:
|
||||
model_name = kwargs.get('model_name', 'DefaultModel')
|
||||
if operations is None:
|
||||
operations = kwargs.get('operations', ['list', 'create', 'retrieve', 'update', 'delete'])
|
||||
if app_name is None:
|
||||
app_name = kwargs.get('app_name', 'default_app')
|
||||
|
||||
api_version = kwargs.get('api_version', 'v1')
|
||||
prefix = kwargs.get('prefix', '')
|
||||
namespace = kwargs.get('namespace', app_name)
|
||||
|
||||
# 确保operations是列表
|
||||
if isinstance(operations, str):
|
||||
operations = [operations]
|
||||
|
||||
# 准备模板上下文
|
||||
context = {
|
||||
'app_name': app_name,
|
||||
'model_name': model_name,
|
||||
'model_name_lower': model_name.lower(),
|
||||
'snake_model_name': model_name.lower(), # 添加snake_model_name变量
|
||||
'operations': operations,
|
||||
'api_version': api_version,
|
||||
'prefix': prefix,
|
||||
'namespace': namespace,
|
||||
'has_create': 'create' in operations,
|
||||
'has_list': 'list' in operations,
|
||||
'has_retrieve': 'retrieve' in operations,
|
||||
'has_update': 'update' in operations,
|
||||
'has_delete': 'delete' in operations,
|
||||
'url_patterns': []
|
||||
}
|
||||
|
||||
# 生成URL模式
|
||||
for operation in operations:
|
||||
if operation == 'list':
|
||||
context['url_patterns'].append({
|
||||
'pattern': f'{model_name.lower()}/',
|
||||
'view': f'list_{model_name.lower()}',
|
||||
'name': f'{model_name.lower()}_list'
|
||||
})
|
||||
elif operation == 'create':
|
||||
context['url_patterns'].append({
|
||||
'pattern': f'{model_name.lower()}/create/',
|
||||
'view': f'create_{model_name.lower()}',
|
||||
'name': f'{model_name.lower()}_create'
|
||||
})
|
||||
elif operation == 'retrieve' or operation == 'get':
|
||||
context['url_patterns'].append({
|
||||
'pattern': f'{model_name.lower()}/<int:pk>/',
|
||||
'view': f'get_{model_name.lower()}',
|
||||
'name': f'{model_name.lower()}_detail'
|
||||
})
|
||||
elif operation == 'update':
|
||||
context['url_patterns'].append({
|
||||
'pattern': f'{model_name.lower()}/<int:pk>/update/',
|
||||
'view': f'update_{model_name.lower()}',
|
||||
'name': f'{model_name.lower()}_update'
|
||||
})
|
||||
elif operation == 'delete':
|
||||
context['url_patterns'].append({
|
||||
'pattern': f'{model_name.lower()}/<int:pk>/delete/',
|
||||
'view': f'delete_{model_name.lower()}',
|
||||
'name': f'{model_name.lower()}_delete'
|
||||
})
|
||||
|
||||
# 渲染模板
|
||||
return self.render_template('django/urls.mako', context)
|
||||
|
||||
def generate_rest_urls(
|
||||
self,
|
||||
app_name: str,
|
||||
model_name: str,
|
||||
viewset_name: Optional[str] = None,
|
||||
api_version: str = 'v1',
|
||||
namespace: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成REST风格的URL配置
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
model_name: 模型名称
|
||||
viewset_name: ViewSet名称
|
||||
api_version: API版本
|
||||
namespace: 命名空间
|
||||
|
||||
Returns:
|
||||
str: 生成的REST URL配置代码
|
||||
"""
|
||||
if viewset_name is None:
|
||||
viewset_name = f'{model_name}ViewSet'
|
||||
|
||||
context = {
|
||||
'app_name': app_name,
|
||||
'model_name': model_name,
|
||||
'viewset_name': viewset_name,
|
||||
'api_version': api_version,
|
||||
'namespace': namespace,
|
||||
'url_type': 'rest'
|
||||
}
|
||||
|
||||
return self.render_template('django/urls.mako', context)
|
||||
|
||||
def generate_router_urls(
|
||||
self,
|
||||
app_name: str,
|
||||
viewsets: List[Dict[str, str]],
|
||||
api_version: str = 'v1',
|
||||
router_type: str = 'DefaultRouter'
|
||||
) -> str:
|
||||
"""
|
||||
生成使用Router的URL配置
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
viewsets: ViewSet配置列表
|
||||
api_version: API版本
|
||||
router_type: Router类型
|
||||
|
||||
Returns:
|
||||
str: 生成的Router URL配置代码
|
||||
"""
|
||||
context = {
|
||||
'app_name': app_name,
|
||||
'viewsets': viewsets,
|
||||
'api_version': api_version,
|
||||
'router_type': router_type,
|
||||
'url_type': 'router'
|
||||
}
|
||||
|
||||
return self.render_template('django/urls.mako', context)
|
||||
|
||||
def generate_function_based_urls(
|
||||
self,
|
||||
app_name: str,
|
||||
views: List[Dict[str, Any]],
|
||||
api_version: str = 'v1'
|
||||
) -> str:
|
||||
"""
|
||||
生成基于函数视图的URL配置
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
views: 视图配置列表
|
||||
api_version: API版本
|
||||
|
||||
Returns:
|
||||
str: 生成的函数视图URL配置代码
|
||||
"""
|
||||
context = {
|
||||
'app_name': app_name,
|
||||
'views': views,
|
||||
'api_version': api_version,
|
||||
'url_type': 'function_based'
|
||||
}
|
||||
|
||||
return self.render_template('django/urls.mako', context)
|
||||
|
||||
def generate_nested_urls(
|
||||
self,
|
||||
app_name: str,
|
||||
parent_model: str,
|
||||
child_model: str,
|
||||
operations: List[str],
|
||||
api_version: str = 'v1'
|
||||
) -> str:
|
||||
"""
|
||||
生成嵌套资源的URL配置
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
parent_model: 父模型名称
|
||||
child_model: 子模型名称
|
||||
operations: 支持的操作列表
|
||||
api_version: API版本
|
||||
|
||||
Returns:
|
||||
str: 生成的嵌套URL配置代码
|
||||
"""
|
||||
context = {
|
||||
'app_name': app_name,
|
||||
'parent_model': parent_model,
|
||||
'child_model': child_model,
|
||||
'operations': operations,
|
||||
'api_version': api_version,
|
||||
'url_type': 'nested'
|
||||
}
|
||||
|
||||
return self.render_template('django/urls.mako', context)
|
||||
|
||||
def generate_custom_action_urls(
|
||||
self,
|
||||
app_name: str,
|
||||
model_name: str,
|
||||
actions: List[Dict[str, Any]],
|
||||
api_version: str = 'v1'
|
||||
) -> str:
|
||||
"""
|
||||
生成自定义动作的URL配置
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
model_name: 模型名称
|
||||
actions: 自定义动作配置列表
|
||||
api_version: API版本
|
||||
|
||||
Returns:
|
||||
str: 生成的自定义动作URL配置代码
|
||||
"""
|
||||
context = {
|
||||
'app_name': app_name,
|
||||
'model_name': model_name,
|
||||
'actions': actions,
|
||||
'api_version': api_version,
|
||||
'url_type': 'custom_actions'
|
||||
}
|
||||
|
||||
return self.render_template('django/urls.mako', context)
|
||||
|
||||
def generate_app_urls(
|
||||
self,
|
||||
app_name: str,
|
||||
models: List[str],
|
||||
api_version: str = 'v1',
|
||||
include_admin: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
生成应用级别的URL配置
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
models: 模型名称列表
|
||||
api_version: API版本
|
||||
include_admin: 是否包含管理后台URL
|
||||
|
||||
Returns:
|
||||
str: 生成的应用URL配置代码
|
||||
"""
|
||||
context = {
|
||||
'app_name': app_name,
|
||||
'models': models,
|
||||
'api_version': api_version,
|
||||
'include_admin': include_admin,
|
||||
'url_type': 'app_level'
|
||||
}
|
||||
|
||||
return self.render_template('django/urls.mako', context)
|
||||
|
||||
def add_url_pattern(
|
||||
self,
|
||||
url_code: str,
|
||||
pattern: str,
|
||||
view: str,
|
||||
name: str,
|
||||
methods: Optional[List[str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
添加URL模式
|
||||
|
||||
Args:
|
||||
url_code: 原URL配置代码
|
||||
pattern: URL模式
|
||||
view: 视图名称
|
||||
name: URL名称
|
||||
methods: HTTP方法列表
|
||||
|
||||
Returns:
|
||||
str: 包含新URL模式的配置代码
|
||||
"""
|
||||
if methods:
|
||||
method_str = f", methods={methods}"
|
||||
else:
|
||||
method_str = ""
|
||||
|
||||
new_pattern = f" path('{pattern}', {view}, name='{name}'{method_str}),"
|
||||
|
||||
# 在urlpatterns列表中添加新模式
|
||||
lines = url_code.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
if 'urlpatterns = [' in line:
|
||||
# 找到列表结束位置
|
||||
for j in range(i + 1, len(lines)):
|
||||
if lines[j].strip() == ']':
|
||||
lines.insert(j, new_pattern)
|
||||
break
|
||||
break
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def generate_api_documentation_urls(
|
||||
self,
|
||||
app_name: str,
|
||||
api_version: str = 'v1',
|
||||
include_swagger: bool = True,
|
||||
include_redoc: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
生成API文档的URL配置
|
||||
|
||||
Args:
|
||||
app_name: 应用名称
|
||||
api_version: API版本
|
||||
include_swagger: 是否包含Swagger UI
|
||||
include_redoc: 是否包含ReDoc
|
||||
|
||||
Returns:
|
||||
str: 生成的API文档URL配置代码
|
||||
"""
|
||||
context = {
|
||||
'app_name': app_name,
|
||||
'api_version': api_version,
|
||||
'include_swagger': include_swagger,
|
||||
'include_redoc': include_redoc,
|
||||
'url_type': 'api_docs'
|
||||
}
|
||||
|
||||
return self.render_template('django/urls.mako', context)
|
||||
416
hertz_studio_django_utils/code_generator/view_generator.py
Normal file
416
hertz_studio_django_utils/code_generator/view_generator.py
Normal file
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
Django视图代码生成器
|
||||
|
||||
该模块负责根据配置生成Django REST Framework视图代码
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
from .base_generator import BaseGenerator
|
||||
|
||||
|
||||
class ViewGenerator(BaseGenerator):
|
||||
"""Django视图代码生成器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化视图生成器"""
|
||||
super().__init__()
|
||||
|
||||
def generate(self, model_name: str = None, operations: List[str] = None, **kwargs) -> str:
|
||||
"""
|
||||
生成Django视图代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
operations: 支持的操作列表
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
str: 生成的视图代码
|
||||
"""
|
||||
# 处理参数,支持位置参数和关键字参数
|
||||
if model_name is None:
|
||||
model_name = kwargs.get('model_name', 'DefaultModel')
|
||||
if operations is None:
|
||||
operations = kwargs.get('operations', ['list', 'create', 'retrieve', 'update', 'delete'])
|
||||
|
||||
permissions = kwargs.get('permissions', {})
|
||||
authentication = kwargs.get('authentication', ['IsAuthenticated'])
|
||||
pagination = kwargs.get('pagination', True)
|
||||
filters = kwargs.get('filters', {})
|
||||
permission_type = kwargs.get('permission_type', 'standard') # 'standard' 或 'system'
|
||||
|
||||
# 确保operations是列表
|
||||
if isinstance(operations, str):
|
||||
operations = [operations]
|
||||
|
||||
# 自动生成权限配置
|
||||
if not permissions and permission_type:
|
||||
if permission_type == 'system':
|
||||
permissions = self.generate_system_permission_config(model_name, operations)
|
||||
else:
|
||||
permissions = self.generate_permission_config(model_name, operations)
|
||||
|
||||
# 准备模板上下文
|
||||
context = {
|
||||
'model_name': model_name,
|
||||
'model_name_lower': model_name.lower(),
|
||||
'operations': operations,
|
||||
'permissions': permissions,
|
||||
'permissions_list': permissions, # 为了兼容模板
|
||||
'authentication': authentication,
|
||||
'pagination': pagination,
|
||||
'filters': filters,
|
||||
'has_create': 'create' in operations,
|
||||
'has_list': 'list' in operations,
|
||||
'has_retrieve': 'retrieve' in operations,
|
||||
'has_update': 'update' in operations,
|
||||
'has_delete': 'delete' in operations,
|
||||
'viewset_name': f'{model_name}ViewSet',
|
||||
'serializer_name': f'{model_name}Serializer',
|
||||
'queryset_name': f'{model_name}.objects.all()',
|
||||
'view_classes': []
|
||||
}
|
||||
|
||||
# 生成视图类列表
|
||||
for operation in operations:
|
||||
if operation == 'list':
|
||||
context['view_classes'].append({
|
||||
'name': f'{model_name}ListView',
|
||||
'base_class': 'ListAPIView',
|
||||
'operation': 'list'
|
||||
})
|
||||
elif operation == 'create':
|
||||
context['view_classes'].append({
|
||||
'name': f'{model_name}CreateView',
|
||||
'base_class': 'CreateAPIView',
|
||||
'operation': 'create'
|
||||
})
|
||||
elif operation == 'retrieve' or operation == 'get':
|
||||
context['view_classes'].append({
|
||||
'name': f'{model_name}DetailView',
|
||||
'base_class': 'RetrieveAPIView',
|
||||
'operation': 'retrieve'
|
||||
})
|
||||
elif operation == 'update':
|
||||
context['view_classes'].append({
|
||||
'name': f'{model_name}UpdateView',
|
||||
'base_class': 'UpdateAPIView',
|
||||
'operation': 'update'
|
||||
})
|
||||
elif operation == 'delete':
|
||||
context['view_classes'].append({
|
||||
'name': f'{model_name}DeleteView',
|
||||
'base_class': 'DestroyAPIView',
|
||||
'operation': 'delete'
|
||||
})
|
||||
|
||||
# 渲染模板
|
||||
return self.render_template('django/views.mako', context)
|
||||
|
||||
def generate_api_view(
|
||||
self,
|
||||
model_name: str,
|
||||
view_name: str,
|
||||
http_methods: List[str],
|
||||
permissions: Optional[List[str]] = None,
|
||||
authentication: Optional[List[str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成API视图代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
view_name: 视图名称
|
||||
http_methods: HTTP方法列表
|
||||
permissions: 权限列表
|
||||
authentication: 认证方式列表
|
||||
|
||||
Returns:
|
||||
str: 生成的API视图代码
|
||||
"""
|
||||
context = {
|
||||
'model_name': model_name,
|
||||
'view_name': view_name,
|
||||
'http_methods': http_methods,
|
||||
'permissions': permissions or [],
|
||||
'authentication': authentication or [],
|
||||
'view_type': 'api_view'
|
||||
}
|
||||
|
||||
return self.render_template('django/views.mako', context)
|
||||
|
||||
def generate_viewset(
|
||||
self,
|
||||
model_name: str,
|
||||
viewset_type: str = 'ModelViewSet',
|
||||
operations: Optional[List[str]] = None,
|
||||
permissions: Optional[List[str]] = None,
|
||||
filters: Optional[Dict[str, Any]] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成ViewSet代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
viewset_type: ViewSet类型
|
||||
operations: 支持的操作列表
|
||||
permissions: 权限列表
|
||||
filters: 过滤器配置
|
||||
|
||||
Returns:
|
||||
str: 生成的ViewSet代码
|
||||
"""
|
||||
if operations is None:
|
||||
operations = ['list', 'create', 'retrieve', 'update', 'destroy']
|
||||
|
||||
context = {
|
||||
'model_name': model_name,
|
||||
'viewset_type': viewset_type,
|
||||
'operations': operations,
|
||||
'permissions': permissions or [],
|
||||
'filters': filters or {},
|
||||
'view_type': 'viewset'
|
||||
}
|
||||
|
||||
return self.render_template('django/views.mako', context)
|
||||
|
||||
def generate_generic_view(
|
||||
self,
|
||||
model_name: str,
|
||||
view_type: str,
|
||||
permissions: Optional[List[str]] = None,
|
||||
filters: Optional[Dict[str, Any]] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成通用视图代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
view_type: 视图类型 (ListAPIView, CreateAPIView等)
|
||||
permissions: 权限列表
|
||||
filters: 过滤器配置
|
||||
|
||||
Returns:
|
||||
str: 生成的通用视图代码
|
||||
"""
|
||||
context = {
|
||||
'model_name': model_name,
|
||||
'view_type': view_type,
|
||||
'permissions': permissions or [],
|
||||
'filters': filters or {},
|
||||
'generic_view': True
|
||||
}
|
||||
|
||||
return self.render_template('django/views.mako', context)
|
||||
|
||||
def generate_crud_views(
|
||||
self,
|
||||
model_name: str,
|
||||
operations: List[str],
|
||||
permissions: Optional[Dict[str, List[str]]] = None,
|
||||
pagination: bool = True,
|
||||
filters: Optional[Dict[str, Any]] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成CRUD视图代码
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
operations: 支持的操作列表
|
||||
permissions: 每个操作的权限配置
|
||||
pagination: 是否启用分页
|
||||
filters: 过滤器配置
|
||||
|
||||
Returns:
|
||||
str: 生成的CRUD视图代码
|
||||
"""
|
||||
context = {
|
||||
'model_name': model_name,
|
||||
'operations': operations,
|
||||
'permissions': permissions or {},
|
||||
'pagination': pagination,
|
||||
'filters': filters or {},
|
||||
'crud_views': True
|
||||
}
|
||||
|
||||
return self.render_template('django/views.mako', context)
|
||||
|
||||
def generate_custom_action(
|
||||
self,
|
||||
action_name: str,
|
||||
http_methods: List[str],
|
||||
detail: bool = False,
|
||||
permissions: Optional[List[str]] = None,
|
||||
serializer_class: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
生成自定义动作代码
|
||||
|
||||
Args:
|
||||
action_name: 动作名称
|
||||
http_methods: HTTP方法列表
|
||||
detail: 是否为详情动作
|
||||
permissions: 权限列表
|
||||
serializer_class: 序列化器类名
|
||||
|
||||
Returns:
|
||||
str: 生成的自定义动作代码
|
||||
"""
|
||||
context = {
|
||||
'action_name': action_name,
|
||||
'http_methods': http_methods,
|
||||
'detail': detail,
|
||||
'permissions': permissions or [],
|
||||
'serializer_class': serializer_class,
|
||||
'custom_action': True
|
||||
}
|
||||
|
||||
return self.render_template('django/views.mako', context)
|
||||
|
||||
def add_permission_decorator(
|
||||
self,
|
||||
view_code: str,
|
||||
permissions: List[str]
|
||||
) -> str:
|
||||
"""
|
||||
添加权限装饰器
|
||||
|
||||
Args:
|
||||
view_code: 原视图代码
|
||||
permissions: 权限列表
|
||||
|
||||
Returns:
|
||||
str: 包含权限装饰器的视图代码
|
||||
"""
|
||||
decorators = []
|
||||
for permission in permissions:
|
||||
if permission == 'login_required':
|
||||
decorators.append('@login_required')
|
||||
elif permission == 'no_login_required':
|
||||
decorators.append('@no_login_required')
|
||||
else:
|
||||
decorators.append(f'@permission_required("{permission}")')
|
||||
|
||||
# 在函数定义前添加装饰器
|
||||
lines = view_code.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith('def ') or line.strip().startswith('class '):
|
||||
# 在函数或类定义前插入装饰器
|
||||
for j, decorator in enumerate(decorators):
|
||||
lines.insert(i + j, decorator)
|
||||
break
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def generate_permission_config(
|
||||
self,
|
||||
model_name: str,
|
||||
operations: List[str],
|
||||
permission_prefix: str = None
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
生成权限配置字典
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
operations: 操作列表
|
||||
permission_prefix: 权限前缀,默认使用模型名称小写
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 权限配置字典
|
||||
"""
|
||||
if permission_prefix is None:
|
||||
permission_prefix = model_name.lower()
|
||||
|
||||
# 统一权限映射规则,与菜单生成器保持一致
|
||||
permission_mapping = {
|
||||
'list': f'{permission_prefix}:list',
|
||||
'create': f'{permission_prefix}:add', # 改为add,与菜单一致
|
||||
'retrieve': f'{permission_prefix}:list', # 改为list,与菜单一致
|
||||
'get': f'{permission_prefix}:list', # 改为list,与菜单一致
|
||||
'update': f'{permission_prefix}:edit', # 改为edit,与菜单一致
|
||||
'delete': f'{permission_prefix}:remove', # 改为remove,与菜单一致
|
||||
'partial_update': f'{permission_prefix}:edit' # 改为edit,与菜单一致
|
||||
}
|
||||
|
||||
return {op: permission_mapping.get(op, f'{permission_prefix}:{op}') for op in operations}
|
||||
|
||||
def generate_system_permission_config(
|
||||
self,
|
||||
model_name: str,
|
||||
operations: List[str],
|
||||
module_prefix: str = 'system'
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
生成系统级权限配置字典
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
operations: 操作列表
|
||||
module_prefix: 模块前缀,默认为'system'
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 系统级权限配置字典
|
||||
"""
|
||||
model_lower = model_name.lower()
|
||||
|
||||
permission_mapping = {
|
||||
'list': f'{module_prefix}:{model_lower}:list',
|
||||
'create': f'{model_lower}:create',
|
||||
'retrieve': f'{module_prefix}:{model_lower}:query',
|
||||
'get': f'{module_prefix}:{model_lower}:query',
|
||||
'update': f'{model_lower}:update',
|
||||
'delete': f'{model_lower}:delete',
|
||||
'partial_update': f'{model_lower}:update'
|
||||
}
|
||||
|
||||
return {op: permission_mapping.get(op, f'{module_prefix}:{model_lower}:{op}') for op in operations}
|
||||
|
||||
def add_swagger_documentation(
|
||||
self,
|
||||
view_code: str,
|
||||
operation_id: str,
|
||||
summary: str,
|
||||
description: str,
|
||||
tags: List[str],
|
||||
request_schema: Optional[str] = None,
|
||||
response_schema: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
添加Swagger文档注解
|
||||
|
||||
Args:
|
||||
view_code: 原视图代码
|
||||
operation_id: 操作ID
|
||||
summary: 摘要
|
||||
description: 描述
|
||||
tags: 标签列表
|
||||
request_schema: 请求模式
|
||||
response_schema: 响应模式
|
||||
|
||||
Returns:
|
||||
str: 包含Swagger文档的视图代码
|
||||
"""
|
||||
swagger_decorator = f"""@extend_schema(
|
||||
operation_id='{operation_id}',
|
||||
summary='{summary}',
|
||||
description='{description}',
|
||||
tags={tags}"""
|
||||
|
||||
if request_schema:
|
||||
swagger_decorator += f",\n request={request_schema}"
|
||||
|
||||
if response_schema:
|
||||
swagger_decorator += f",\n responses={{200: {response_schema}}}"
|
||||
|
||||
swagger_decorator += "\n)"
|
||||
|
||||
# 在函数定义前添加装饰器
|
||||
lines = view_code.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith('def '):
|
||||
lines.insert(i, swagger_decorator)
|
||||
break
|
||||
|
||||
return '\n'.join(lines)
|
||||
184
hertz_studio_django_utils/code_generator/yaml_generator_cli.py
Normal file
184
hertz_studio_django_utils/code_generator/yaml_generator_cli.py
Normal file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
YAML Django代码生成器命令行工具
|
||||
|
||||
该脚本提供命令行接口,用于从YAML配置文件生成Django应用代码。
|
||||
|
||||
使用示例:
|
||||
python yaml_generator_cli.py generate app_config.yaml
|
||||
python yaml_generator_cli.py template --output my_app.yaml
|
||||
python yaml_generator_cli.py validate app_config.yaml
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from django_code_generator import DjangoCodeGenerator
|
||||
|
||||
|
||||
def generate_from_yaml(yaml_file_path: str, output_dir: str = None) -> bool:
|
||||
"""
|
||||
从YAML文件生成Django应用代码
|
||||
|
||||
Args:
|
||||
yaml_file_path: YAML配置文件路径
|
||||
output_dir: 输出目录(可选)
|
||||
|
||||
Returns:
|
||||
bool: 生成是否成功
|
||||
"""
|
||||
try:
|
||||
generator = DjangoCodeGenerator()
|
||||
|
||||
# 解析YAML配置
|
||||
config = generator.yaml_parser.parse_yaml_file(yaml_file_path)
|
||||
|
||||
# 如果指定了输出目录,覆盖配置中的设置
|
||||
if output_dir:
|
||||
config['output_dir'] = output_dir
|
||||
|
||||
print(f"正在从 {yaml_file_path} 生成Django应用代码...")
|
||||
print(f"应用名称: {config['app_name']}")
|
||||
print(f"输出目录: {config['output_dir']}")
|
||||
print(f"模型数量: {len(config['models'])}")
|
||||
|
||||
# 生成代码
|
||||
generated_files = generator.generate_from_yaml_config(config)
|
||||
|
||||
print(f"\n✅ 成功生成 {len(generated_files)} 个文件:")
|
||||
for file_path in sorted(generated_files.keys()):
|
||||
print(f" 📄 {file_path}")
|
||||
|
||||
print(f"\n🎉 Django应用 '{config['app_name']}' 生成完成!")
|
||||
print(f"📁 输出目录: {os.path.abspath(config['output_dir'])}")
|
||||
|
||||
return True
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"❌ 错误: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 生成失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def generate_template(output_path: str = 'app_template.yaml') -> bool:
|
||||
"""
|
||||
生成YAML配置文件模板
|
||||
|
||||
Args:
|
||||
output_path: 输出文件路径
|
||||
|
||||
Returns:
|
||||
bool: 生成是否成功
|
||||
"""
|
||||
try:
|
||||
generator = DjangoCodeGenerator()
|
||||
template_content = generator.generate_yaml_template(output_path)
|
||||
|
||||
print(f"✅ YAML配置模板已生成: {os.path.abspath(output_path)}")
|
||||
print("\n📝 模板内容预览:")
|
||||
print("-" * 50)
|
||||
# 显示前20行
|
||||
lines = template_content.split('\n')
|
||||
for i, line in enumerate(lines[:20]):
|
||||
print(f"{i+1:2d}: {line}")
|
||||
if len(lines) > 20:
|
||||
print(f"... (还有 {len(lines) - 20} 行)")
|
||||
print("-" * 50)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 模板生成失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def validate_yaml(yaml_file_path: str) -> bool:
|
||||
"""
|
||||
验证YAML配置文件
|
||||
|
||||
Args:
|
||||
yaml_file_path: YAML配置文件路径
|
||||
|
||||
Returns:
|
||||
bool: 验证是否通过
|
||||
"""
|
||||
try:
|
||||
generator = DjangoCodeGenerator()
|
||||
config = generator.yaml_parser.parse_yaml_file(yaml_file_path)
|
||||
|
||||
print(f"✅ YAML配置文件验证通过: {yaml_file_path}")
|
||||
print(f"📋 配置摘要:")
|
||||
print(f" 应用名称: {config['app_name']}")
|
||||
print(f" 版本: {config.get('version', 'N/A')}")
|
||||
print(f" 描述: {config.get('description', 'N/A')}")
|
||||
print(f" 作者: {config.get('author', 'N/A')}")
|
||||
print(f" 输出目录: {config['output_dir']}")
|
||||
print(f" 模型数量: {len(config['models'])}")
|
||||
|
||||
print(f"\n📊 模型详情:")
|
||||
for i, model in enumerate(config['models'], 1):
|
||||
print(f" {i}. {model['name']} ({len(model['fields'])} 个字段)")
|
||||
operations = model.get('operations', [])
|
||||
if operations:
|
||||
print(f" 操作: {', '.join(operations)}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ YAML配置验证失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='YAML Django代码生成器',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
使用示例:
|
||||
%(prog)s generate app_config.yaml # 从YAML生成代码
|
||||
%(prog)s generate app_config.yaml -o ./my_app # 指定输出目录
|
||||
%(prog)s template # 生成默认模板
|
||||
%(prog)s template -o my_template.yaml # 生成自定义模板
|
||||
%(prog)s validate app_config.yaml # 验证YAML配置
|
||||
"""
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='可用命令')
|
||||
|
||||
# generate 子命令
|
||||
generate_parser = subparsers.add_parser('generate', help='从YAML文件生成Django应用代码')
|
||||
generate_parser.add_argument('yaml_file', help='YAML配置文件路径')
|
||||
generate_parser.add_argument('-o', '--output', help='输出目录')
|
||||
|
||||
# template 子命令
|
||||
template_parser = subparsers.add_parser('template', help='生成YAML配置文件模板')
|
||||
template_parser.add_argument('-o', '--output', default='app_template.yaml', help='输出文件路径')
|
||||
|
||||
# validate 子命令
|
||||
validate_parser = subparsers.add_parser('validate', help='验证YAML配置文件')
|
||||
validate_parser.add_argument('yaml_file', help='YAML配置文件路径')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
success = False
|
||||
|
||||
if args.command == 'generate':
|
||||
success = generate_from_yaml(args.yaml_file, args.output)
|
||||
elif args.command == 'template':
|
||||
success = generate_template(args.output)
|
||||
elif args.command == 'validate':
|
||||
success = validate_yaml(args.yaml_file)
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
372
hertz_studio_django_utils/code_generator/yaml_parser.py
Normal file
372
hertz_studio_django_utils/code_generator/yaml_parser.py
Normal file
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
YAML配置文件解析器
|
||||
|
||||
该模块负责解析YAML配置文件,将其转换为Django代码生成器可以使用的数据结构。
|
||||
|
||||
使用示例:
|
||||
parser = YAMLParser()
|
||||
config = parser.parse_yaml_file('app_config.yaml')
|
||||
generator = DjangoCodeGenerator()
|
||||
generator.generate_from_yaml_config(config)
|
||||
"""
|
||||
|
||||
import yaml
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class YAMLParser:
|
||||
"""YAML配置文件解析器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化YAML解析器"""
|
||||
# 支持Django字段类型
|
||||
self.supported_field_types = {
|
||||
'CharField', 'TextField', 'IntegerField', 'FloatField',
|
||||
'DecimalField', 'BooleanField', 'DateField', 'DateTimeField',
|
||||
'EmailField', 'URLField', 'ImageField', 'FileField',
|
||||
'ForeignKey', 'ManyToManyField', 'OneToOneField', 'JSONField',
|
||||
'SlugField', 'PositiveIntegerField', 'BigIntegerField',
|
||||
'SmallIntegerField', 'UUIDField', 'TimeField',
|
||||
'GenericIPAddressField', 'BinaryField', 'DurationField',
|
||||
# 支持通用字段类型
|
||||
'string', 'text', 'integer', 'float', 'decimal', 'boolean',
|
||||
'date', 'datetime', 'email', 'url', 'image', 'file', 'json',
|
||||
'slug', 'uuid', 'time', 'ip', 'binary', 'duration'
|
||||
}
|
||||
|
||||
# 通用字段类型到Django字段类型的映射
|
||||
self.field_type_mapping = {
|
||||
'string': 'CharField',
|
||||
'text': 'TextField',
|
||||
'integer': 'IntegerField',
|
||||
'float': 'FloatField',
|
||||
'decimal': 'DecimalField',
|
||||
'boolean': 'BooleanField',
|
||||
'date': 'DateField',
|
||||
'datetime': 'DateTimeField',
|
||||
'email': 'EmailField',
|
||||
'url': 'URLField',
|
||||
'image': 'ImageField',
|
||||
'file': 'FileField',
|
||||
'json': 'JSONField',
|
||||
'slug': 'SlugField',
|
||||
'uuid': 'UUIDField',
|
||||
'time': 'TimeField',
|
||||
'ip': 'GenericIPAddressField',
|
||||
'binary': 'BinaryField',
|
||||
'duration': 'DurationField'
|
||||
}
|
||||
|
||||
self.supported_operations = {
|
||||
'create', 'read', 'update', 'delete', 'list', 'search', 'filter', 'get', 'retrieve'
|
||||
}
|
||||
|
||||
def parse_yaml_file(self, yaml_file_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
解析YAML配置文件
|
||||
|
||||
Args:
|
||||
yaml_file_path: YAML文件路径
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 解析后的配置字典
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: 当文件不存在时
|
||||
yaml.YAMLError: 当YAML格式错误时
|
||||
ValueError: 当配置验证失败时
|
||||
"""
|
||||
if not os.path.exists(yaml_file_path):
|
||||
raise FileNotFoundError(f"YAML配置文件不存在: {yaml_file_path}")
|
||||
|
||||
try:
|
||||
with open(yaml_file_path, 'r', encoding='utf-8') as file:
|
||||
config = yaml.safe_load(file)
|
||||
except yaml.YAMLError as e:
|
||||
raise yaml.YAMLError(f"YAML文件格式错误: {e}")
|
||||
|
||||
# 验证配置结构
|
||||
validated_config = self._validate_config(config)
|
||||
return validated_config
|
||||
|
||||
def _validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
验证YAML配置结构
|
||||
|
||||
Args:
|
||||
config: 原始配置字典
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 验证后的配置字典
|
||||
|
||||
Raises:
|
||||
ValueError: 当配置验证失败时
|
||||
"""
|
||||
if not isinstance(config, dict):
|
||||
raise ValueError("YAML配置必须是字典格式")
|
||||
|
||||
# 验证必需的顶级字段
|
||||
required_fields = ['app_name', 'models']
|
||||
for field in required_fields:
|
||||
if field not in config:
|
||||
raise ValueError(f"缺少必需字段: {field}")
|
||||
|
||||
# 验证应用名称
|
||||
app_name = config['app_name']
|
||||
if not isinstance(app_name, str) or not app_name.strip():
|
||||
raise ValueError("app_name必须是非空字符串")
|
||||
|
||||
# 验证模型配置
|
||||
models = config['models']
|
||||
if isinstance(models, dict):
|
||||
# 如果models是字典格式,转换为列表格式
|
||||
models_list = []
|
||||
for model_name, model_config in models.items():
|
||||
model_config['name'] = model_name
|
||||
models_list.append(model_config)
|
||||
models = models_list
|
||||
elif not isinstance(models, list) or len(models) == 0:
|
||||
raise ValueError("models必须是非空列表或字典")
|
||||
|
||||
validated_models = []
|
||||
for i, model in enumerate(models):
|
||||
validated_model = self._validate_model_config(model, i)
|
||||
validated_models.append(validated_model)
|
||||
|
||||
# 构建验证后的配置
|
||||
validated_config = {
|
||||
'app_name': app_name.strip(),
|
||||
'models': validated_models,
|
||||
'output_dir': config.get('output_dir', './generated_code'),
|
||||
'version': config.get('version', '1.0.0'),
|
||||
'description': config.get('description', ''),
|
||||
'author': config.get('author', ''),
|
||||
'global_settings': config.get('global_settings', {})
|
||||
}
|
||||
|
||||
return validated_config
|
||||
|
||||
def _validate_model_config(self, model: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
"""
|
||||
验证单个模型配置
|
||||
|
||||
Args:
|
||||
model: 模型配置字典
|
||||
index: 模型在列表中的索引
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 验证后的模型配置
|
||||
|
||||
Raises:
|
||||
ValueError: 当模型配置验证失败时
|
||||
"""
|
||||
if not isinstance(model, dict):
|
||||
raise ValueError(f"模型配置[{index}]必须是字典格式")
|
||||
|
||||
# 验证必需字段
|
||||
if 'name' not in model:
|
||||
raise ValueError(f"模型配置[{index}]缺少必需字段: name")
|
||||
|
||||
if 'fields' not in model:
|
||||
raise ValueError(f"模型配置[{index}]缺少必需字段: fields")
|
||||
|
||||
model_name = model['name']
|
||||
if not isinstance(model_name, str) or not model_name.strip():
|
||||
raise ValueError(f"模型配置[{index}]的name必须是非空字符串")
|
||||
|
||||
# 验证字段配置
|
||||
fields = model['fields']
|
||||
if not isinstance(fields, list) or len(fields) == 0:
|
||||
raise ValueError(f"模型{model_name}的fields必须是非空列表")
|
||||
|
||||
validated_fields = []
|
||||
for j, field in enumerate(fields):
|
||||
validated_field = self._validate_field_config(field, model_name, j)
|
||||
validated_fields.append(validated_field)
|
||||
|
||||
# 验证操作配置
|
||||
operations = model.get('operations', ['create', 'read', 'update', 'delete', 'list'])
|
||||
if not isinstance(operations, list):
|
||||
raise ValueError(f"模型{model_name}的operations必须是列表")
|
||||
|
||||
for op in operations:
|
||||
if op not in self.supported_operations:
|
||||
raise ValueError(f"模型{model_name}包含不支持的操作: {op}")
|
||||
|
||||
# 构建验证后的模型配置
|
||||
validated_model = {
|
||||
'name': model_name.strip(),
|
||||
'fields': validated_fields,
|
||||
'operations': operations,
|
||||
'table_name': model.get('table_name'),
|
||||
'verbose_name': model.get('verbose_name', model_name),
|
||||
'verbose_name_plural': model.get('verbose_name_plural'),
|
||||
'ordering': model.get('ordering', []),
|
||||
'permissions': model.get('permissions', []),
|
||||
'validators': model.get('validators', {}),
|
||||
'meta_options': model.get('meta_options', {}),
|
||||
'admin_config': model.get('admin_config', {}),
|
||||
'api_config': model.get('api_config', {})
|
||||
}
|
||||
|
||||
return validated_model
|
||||
|
||||
def _validate_field_config(self, field: Dict[str, Any], model_name: str, index: int) -> Dict[str, Any]:
|
||||
"""
|
||||
验证单个字段配置
|
||||
|
||||
Args:
|
||||
field: 字段配置字典
|
||||
model_name: 所属模型名称
|
||||
index: 字段在列表中的索引
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 验证后的字段配置
|
||||
|
||||
Raises:
|
||||
ValueError: 当字段配置验证失败时
|
||||
"""
|
||||
if not isinstance(field, dict):
|
||||
raise ValueError(f"模型{model_name}的字段配置[{index}]必须是字典格式")
|
||||
|
||||
# 验证必需字段
|
||||
if 'name' not in field:
|
||||
raise ValueError(f"模型{model_name}的字段配置[{index}]缺少必需字段: name")
|
||||
|
||||
if 'type' not in field:
|
||||
raise ValueError(f"模型{model_name}的字段配置[{index}]缺少必需字段: type")
|
||||
|
||||
field_name = field['name']
|
||||
field_type = field['type']
|
||||
|
||||
if not isinstance(field_name, str) or not field_name.strip():
|
||||
raise ValueError(f"模型{model_name}的字段配置[{index}]的name必须是非空字符串")
|
||||
|
||||
# 检查字段类型是否支持(包括通用类型和Django类型)
|
||||
if field_type not in self.supported_field_types and field_type not in self.field_type_mapping:
|
||||
raise ValueError(f"模型{model_name}的字段{field_name}使用了不支持的类型: {field_type}")
|
||||
|
||||
# 如果是通用类型,转换为Django字段类型
|
||||
django_field_type = self.field_type_mapping.get(field_type, field_type)
|
||||
# 移除调试输出
|
||||
print(f"字段类型转换: {field_type} -> {django_field_type}")
|
||||
|
||||
# 构建验证后的字段配置
|
||||
validated_field = {
|
||||
'name': field['name'],
|
||||
'type': django_field_type, # 使用转换后的Django字段类型
|
||||
'verbose_name': field.get('verbose_name', field['name']),
|
||||
'help_text': field.get('help_text', f"{field['name']}字段"),
|
||||
'required': field.get('required', False),
|
||||
'null': not field.get('required', False),
|
||||
'blank': not field.get('required', False)
|
||||
}
|
||||
|
||||
# 添加其他字段属性
|
||||
for key in ['max_length', 'default', 'unique', 'choices', 'max_digits', 'decimal_places', 'upload_to']:
|
||||
if key in field:
|
||||
validated_field[key] = field[key]
|
||||
|
||||
# 移除None值
|
||||
validated_field = {k: v for k, v in validated_field.items() if v is not None}
|
||||
|
||||
return validated_field
|
||||
|
||||
def generate_yaml_template(self, output_path: str = 'app_template.yaml') -> str:
|
||||
"""
|
||||
生成YAML配置文件模板
|
||||
|
||||
Args:
|
||||
output_path: 输出文件路径
|
||||
|
||||
Returns:
|
||||
str: 生成的模板内容
|
||||
"""
|
||||
template = {
|
||||
'app_name': 'hertz_studio_django_example',
|
||||
'version': '1.0.0',
|
||||
'description': 'Django应用示例',
|
||||
'author': 'Hertz Studio',
|
||||
'output_dir': './generated_code',
|
||||
'global_settings': {
|
||||
'use_uuid_primary_key': False,
|
||||
'add_created_updated_fields': True,
|
||||
'add_status_field': True,
|
||||
'default_permissions': ['add', 'change', 'delete', 'view']
|
||||
},
|
||||
'models': [
|
||||
{
|
||||
'name': 'User',
|
||||
'verbose_name': '用户',
|
||||
'verbose_name_plural': '用户列表',
|
||||
'table_name': 'example_user',
|
||||
'ordering': ['-created_at'],
|
||||
'fields': [
|
||||
{
|
||||
'name': 'username',
|
||||
'type': 'CharField',
|
||||
'max_length': 150,
|
||||
'unique': True,
|
||||
'verbose_name': '用户名',
|
||||
'help_text': '用户登录名'
|
||||
},
|
||||
{
|
||||
'name': 'email',
|
||||
'type': 'EmailField',
|
||||
'unique': True,
|
||||
'verbose_name': '邮箱',
|
||||
'help_text': '用户邮箱地址'
|
||||
},
|
||||
{
|
||||
'name': 'phone',
|
||||
'type': 'CharField',
|
||||
'max_length': 20,
|
||||
'blank': True,
|
||||
'null': True,
|
||||
'verbose_name': '手机号',
|
||||
'help_text': '用户手机号码'
|
||||
},
|
||||
{
|
||||
'name': 'avatar',
|
||||
'type': 'ImageField',
|
||||
'upload_to': 'avatars/',
|
||||
'blank': True,
|
||||
'null': True,
|
||||
'verbose_name': '头像',
|
||||
'help_text': '用户头像图片'
|
||||
},
|
||||
{
|
||||
'name': 'is_active',
|
||||
'type': 'BooleanField',
|
||||
'default': True,
|
||||
'verbose_name': '是否激活',
|
||||
'help_text': '用户账户是否激活'
|
||||
}
|
||||
],
|
||||
'operations': ['create', 'read', 'update', 'delete', 'list', 'search'],
|
||||
'permissions': ['add_user', 'change_user', 'delete_user', 'view_user'],
|
||||
'validators': {
|
||||
'username': 'validate_username',
|
||||
'email': 'validate_email'
|
||||
},
|
||||
'admin_config': {
|
||||
'list_display': ['username', 'email', 'is_active', 'created_at'],
|
||||
'list_filter': ['is_active', 'created_at'],
|
||||
'search_fields': ['username', 'email']
|
||||
},
|
||||
'api_config': {
|
||||
'pagination': True,
|
||||
'filters': ['is_active'],
|
||||
'search_fields': ['username', 'email'],
|
||||
'ordering_fields': ['username', 'created_at']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as file:
|
||||
yaml.dump(template, file, default_flow_style=False, allow_unicode=True, indent=2)
|
||||
|
||||
return yaml.dump(template, default_flow_style=False, allow_unicode=True, indent=2)
|
||||
0
hertz_studio_django_utils/config/__init__.py
Normal file
0
hertz_studio_django_utils/config/__init__.py
Normal file
24
hertz_studio_django_utils/config/departments_config.py
Normal file
24
hertz_studio_django_utils/config/departments_config.py
Normal file
@@ -0,0 +1,24 @@
|
||||
departments = [
|
||||
{
|
||||
'dept_name': 'Hertz科技',
|
||||
'dept_code': 'hertz_tech',
|
||||
'leader': '超级管理员',
|
||||
'email': 'admin@hertz.com',
|
||||
'sort_order': 1,
|
||||
'parent_id': None
|
||||
},
|
||||
{
|
||||
'dept_name': '技术部',
|
||||
'dept_code': 'tech_dept',
|
||||
'leader': '技术总监',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'hertz_tech'
|
||||
},
|
||||
{
|
||||
'dept_name': '运营部',
|
||||
'dept_code': 'ops_dept',
|
||||
'leader': '运营总监',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'hertz_tech'
|
||||
}
|
||||
]
|
||||
625
hertz_studio_django_utils/config/menus_config.py
Normal file
625
hertz_studio_django_utils/config/menus_config.py
Normal file
@@ -0,0 +1,625 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
菜单配置文件
|
||||
包含系统所有菜单配置和动态菜单生成功能
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
def add_new_menus(new_menus: List[Dict[str, Any]]) -> None:
|
||||
"""
|
||||
动态添加新菜单到配置中
|
||||
|
||||
Args:
|
||||
new_menus: 新菜单配置列表
|
||||
"""
|
||||
global menus
|
||||
menus.extend(new_menus)
|
||||
print(f"已添加 {len(new_menus)} 个新菜单到配置中")
|
||||
|
||||
|
||||
def get_menus_by_parent(parent_code: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
根据父级代码获取菜单列表
|
||||
|
||||
Args:
|
||||
parent_code: 父级菜单代码,None表示获取顶级菜单
|
||||
|
||||
Returns:
|
||||
List[Dict]: 菜单列表
|
||||
"""
|
||||
return [menu for menu in menus if menu.get('parent_code') == parent_code]
|
||||
|
||||
|
||||
def get_menu_by_code(menu_code: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
根据菜单代码获取菜单配置
|
||||
|
||||
Args:
|
||||
menu_code: 菜单代码
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: 菜单配置,未找到返回None
|
||||
"""
|
||||
for menu in menus:
|
||||
if menu.get('menu_code') == menu_code:
|
||||
return menu
|
||||
return None
|
||||
|
||||
|
||||
def generate_menu_permissions() -> List[str]:
|
||||
"""
|
||||
生成所有菜单权限列表
|
||||
|
||||
Returns:
|
||||
List[str]: 权限代码列表
|
||||
"""
|
||||
permissions = []
|
||||
for menu in menus:
|
||||
if menu.get('permission'):
|
||||
permissions.append(menu['permission'])
|
||||
return permissions
|
||||
|
||||
|
||||
menus = [
|
||||
# 系统管理目录
|
||||
{
|
||||
'menu_name': '系统管理',
|
||||
'menu_code': 'system',
|
||||
'menu_type': 1, # 目录
|
||||
'path': '/system',
|
||||
'icon': 'system',
|
||||
'sort_order': 1,
|
||||
'parent_code': None
|
||||
},
|
||||
|
||||
# 用户管理菜单
|
||||
{
|
||||
'menu_name': '用户管理',
|
||||
'menu_code': 'system:user',
|
||||
'menu_type': 2, # 菜单
|
||||
'path': '/system/user',
|
||||
'component': 'system/user/index',
|
||||
'icon': 'user',
|
||||
'permission': 'system:user:list',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'system'
|
||||
},
|
||||
{
|
||||
'menu_name': '用户查询',
|
||||
'menu_code': 'system:user:query',
|
||||
'menu_type': 3, # 按钮
|
||||
'permission': 'system:user:query',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'system:user'
|
||||
},
|
||||
{
|
||||
'menu_name': '用户新增',
|
||||
'menu_code': 'system:user:add',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:user:add',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'system:user'
|
||||
},
|
||||
{
|
||||
'menu_name': '用户修改',
|
||||
'menu_code': 'system:user:edit',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:user:edit',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'system:user'
|
||||
},
|
||||
{
|
||||
'menu_name': '用户删除',
|
||||
'menu_code': 'system:user:remove',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:user:remove',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'system:user'
|
||||
},
|
||||
{
|
||||
'menu_name': '分配角色',
|
||||
'menu_code': 'system:user:role',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:user:role',
|
||||
'sort_order': 5,
|
||||
'parent_code': 'system:user'
|
||||
},
|
||||
|
||||
# 角色管理菜单
|
||||
{
|
||||
'menu_name': '角色管理',
|
||||
'menu_code': 'system:role',
|
||||
'menu_type': 2,
|
||||
'path': '/system/role',
|
||||
'component': 'system/role/index',
|
||||
'icon': 'role',
|
||||
'permission': 'system:role:list',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'system'
|
||||
},
|
||||
{
|
||||
'menu_name': '角色查询',
|
||||
'menu_code': 'system:role:query',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:role:query',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'system:role'
|
||||
},
|
||||
{
|
||||
'menu_name': '角色新增',
|
||||
'menu_code': 'system:role:add',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:role:add',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'system:role'
|
||||
},
|
||||
{
|
||||
'menu_name': '角色修改',
|
||||
'menu_code': 'system:role:edit',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:role:edit',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'system:role'
|
||||
},
|
||||
{
|
||||
'menu_name': '角色删除',
|
||||
'menu_code': 'system:role:remove',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:role:remove',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'system:role'
|
||||
},
|
||||
{
|
||||
'menu_name': '分配权限',
|
||||
'menu_code': 'system:role:menu',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:role:menu',
|
||||
'sort_order': 5,
|
||||
'parent_code': 'system:role'
|
||||
},
|
||||
|
||||
# 菜单管理菜单
|
||||
{
|
||||
'menu_name': '菜单管理',
|
||||
'menu_code': 'system:menu',
|
||||
'menu_type': 2,
|
||||
'path': '/system/menu',
|
||||
'component': 'system/menu/index',
|
||||
'icon': 'menu',
|
||||
'permission': 'system:menu:list',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'system'
|
||||
},
|
||||
{
|
||||
'menu_name': '菜单查询',
|
||||
'menu_code': 'system:menu:query',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:menu:query',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'system:menu'
|
||||
},
|
||||
{
|
||||
'menu_name': '菜单新增',
|
||||
'menu_code': 'system:menu:add',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:menu:add',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'system:menu'
|
||||
},
|
||||
{
|
||||
'menu_name': '菜单修改',
|
||||
'menu_code': 'system:menu:edit',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:menu:edit',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'system:menu'
|
||||
},
|
||||
{
|
||||
'menu_name': '菜单删除',
|
||||
'menu_code': 'system:menu:remove',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:menu:remove',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'system:menu'
|
||||
},
|
||||
|
||||
# 部门管理菜单
|
||||
{
|
||||
'menu_name': '部门管理',
|
||||
'menu_code': 'system:dept',
|
||||
'menu_type': 2,
|
||||
'path': '/system/dept',
|
||||
'component': 'system/dept/index',
|
||||
'icon': 'dept',
|
||||
'permission': 'system:dept:list',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'system'
|
||||
},
|
||||
{
|
||||
'menu_name': '部门查询',
|
||||
'menu_code': 'system:dept:query',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:dept:query',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'system:dept'
|
||||
},
|
||||
{
|
||||
'menu_name': '部门新增',
|
||||
'menu_code': 'system:dept:add',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:dept:add',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'system:dept'
|
||||
},
|
||||
{
|
||||
'menu_name': '部门修改',
|
||||
'menu_code': 'system:dept:edit',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:dept:edit',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'system:dept'
|
||||
},
|
||||
{
|
||||
'menu_name': '部门删除',
|
||||
'menu_code': 'system:dept:remove',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:dept:remove',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'system:dept'
|
||||
},
|
||||
|
||||
# ==================== 工作室模块 ====================
|
||||
# 工作室目录
|
||||
{
|
||||
'menu_name': '工作室',
|
||||
'menu_code': 'studio',
|
||||
'menu_type': 1, # 目录
|
||||
'path': '/studio',
|
||||
'icon': 'appstore',
|
||||
'sort_order': 10,
|
||||
'parent_code': None
|
||||
},
|
||||
|
||||
# ==================== 通知公告模块 ====================
|
||||
# 通知公告菜单
|
||||
{
|
||||
'menu_name': '通知公告',
|
||||
'menu_code': 'studio:notice',
|
||||
'menu_type': 2, # 菜单
|
||||
'path': '/studio/notice',
|
||||
'component': 'studio/notice/index',
|
||||
'icon': 'notice',
|
||||
'permission': 'studio:notice:list',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'studio'
|
||||
},
|
||||
{
|
||||
'menu_name': '通知查询',
|
||||
'menu_code': 'studio:notice:query',
|
||||
'menu_type': 3, # 按钮
|
||||
'permission': 'studio:notice:query',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'studio:notice'
|
||||
},
|
||||
{
|
||||
'menu_name': '通知新增',
|
||||
'menu_code': 'studio:notice:add',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:notice:add',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'studio:notice'
|
||||
},
|
||||
{
|
||||
'menu_name': '通知修改',
|
||||
'menu_code': 'studio:notice:edit',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:notice:edit',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'studio:notice'
|
||||
},
|
||||
{
|
||||
'menu_name': '通知删除',
|
||||
'menu_code': 'studio:notice:remove',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:notice:remove',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'studio:notice'
|
||||
},
|
||||
|
||||
# ==================== AI对话模块 ====================
|
||||
# AI对话菜单
|
||||
{
|
||||
'menu_name': 'AI对话',
|
||||
'menu_code': 'studio:ai',
|
||||
'menu_type': 2, # 菜单
|
||||
'path': '/studio/ai',
|
||||
'component': 'studio/ai/index',
|
||||
'icon': 'robot',
|
||||
'permission': 'studio:ai:list',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'studio'
|
||||
},
|
||||
{
|
||||
'menu_name': 'AI查询',
|
||||
'menu_code': 'studio:ai:query',
|
||||
'menu_type': 3, # 按钮
|
||||
'permission': 'studio:ai:query',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'studio:ai'
|
||||
},
|
||||
{
|
||||
'menu_name': 'AI新增',
|
||||
'menu_code': 'studio:ai:add',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:ai:add',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'studio:ai'
|
||||
},
|
||||
{
|
||||
'menu_name': 'AI修改',
|
||||
'menu_code': 'studio:ai:edit',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:ai:edit',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'studio:ai'
|
||||
},
|
||||
{
|
||||
'menu_name': 'AI删除',
|
||||
'menu_code': 'studio:ai:remove',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:ai:remove',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'studio:ai'
|
||||
},
|
||||
|
||||
# ==================== 系统监控模块 ====================
|
||||
# 系统监控菜单
|
||||
{
|
||||
'menu_name': '系统监控',
|
||||
'menu_code': 'studio:system_monitor',
|
||||
'menu_type': 2, # 菜单
|
||||
'path': '/studio/monitor',
|
||||
'component': 'studio/system_monitor/index',
|
||||
'icon': 'monitor',
|
||||
'permission': 'studio:system_monitor:list',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'studio'
|
||||
},
|
||||
{
|
||||
'menu_name': '监控查询',
|
||||
'menu_code': 'studio:system_monitor:query',
|
||||
'menu_type': 3, # 按钮
|
||||
'permission': 'studio:system_monitor:query',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'studio:system_monitor'
|
||||
},
|
||||
{
|
||||
'menu_name': '监控新增',
|
||||
'menu_code': 'studio:system_monitor:add',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:system_monitor:add',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'studio:system_monitor'
|
||||
},
|
||||
{
|
||||
'menu_name': '监控修改',
|
||||
'menu_code': 'studio:system_monitor:edit',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:system_monitor:edit',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'studio:system_monitor'
|
||||
},
|
||||
{
|
||||
'menu_name': '监控删除',
|
||||
'menu_code': 'studio:system_monitor:remove',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:system_monitor:remove',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'studio:system_monitor'
|
||||
},
|
||||
|
||||
# 知识管理菜单
|
||||
{
|
||||
'menu_name': '知识管理',
|
||||
'menu_code': 'system:wiki',
|
||||
'menu_type': 2, # 菜单
|
||||
'path': '/system/wiki',
|
||||
'component': 'system/wiki/index',
|
||||
'icon': 'book',
|
||||
'permission': 'system:wiki:list',
|
||||
'sort_order': 6,
|
||||
'parent_code': 'system'
|
||||
},
|
||||
|
||||
# 知识分类管理
|
||||
{
|
||||
'menu_name': '知识分类',
|
||||
'menu_code': 'system:wiki:category',
|
||||
'menu_type': 2, # 菜单
|
||||
'path': '/system/wiki/category',
|
||||
'component': 'system/wiki/category/index',
|
||||
'icon': 'folder',
|
||||
'permission': 'system:wiki:category:list',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'system:wiki'
|
||||
},
|
||||
{
|
||||
'menu_name': '分类查询',
|
||||
'menu_code': 'system:wiki:category:query',
|
||||
'menu_type': 3, # 按钮
|
||||
'permission': 'system:wiki:category:query',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'system:wiki:category'
|
||||
},
|
||||
{
|
||||
'menu_name': '分类新增',
|
||||
'menu_code': 'system:wiki:category:add',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:wiki:category:add',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'system:wiki:category'
|
||||
},
|
||||
{
|
||||
'menu_name': '分类修改',
|
||||
'menu_code': 'system:wiki:category:edit',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:wiki:category:update',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'system:wiki:category'
|
||||
},
|
||||
{
|
||||
'menu_name': '分类删除',
|
||||
'menu_code': 'system:wiki:category:remove',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:wiki:category:remove',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'system:wiki:category'
|
||||
},
|
||||
|
||||
# 知识文章管理
|
||||
{
|
||||
'menu_name': '知识文章',
|
||||
'menu_code': 'system:wiki:article',
|
||||
'menu_type': 2, # 菜单
|
||||
'path': '/system/wiki/article',
|
||||
'component': 'system/wiki/article/index',
|
||||
'icon': 'file-text',
|
||||
'permission': 'system:wiki:article:list',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'system:wiki'
|
||||
},
|
||||
{
|
||||
'menu_name': '文章查询',
|
||||
'menu_code': 'system:wiki:article:query',
|
||||
'menu_type': 3, # 按钮
|
||||
'permission': 'system:wiki:article:query',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'system:wiki:article'
|
||||
},
|
||||
{
|
||||
'menu_name': '文章新增',
|
||||
'menu_code': 'system:wiki:article:add',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:wiki:article:add',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'system:wiki:article'
|
||||
},
|
||||
{
|
||||
'menu_name': '文章修改',
|
||||
'menu_code': 'system:wiki:article:edit',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:wiki:article:edit',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'system:wiki:article'
|
||||
},
|
||||
{
|
||||
'menu_name': '文章删除',
|
||||
'menu_code': 'system:wiki:article:remove',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:wiki:article:remove',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'system:wiki:article'
|
||||
},
|
||||
{
|
||||
'menu_name': '文章发布',
|
||||
'menu_code': 'system:wiki:article:publish',
|
||||
'menu_type': 3,
|
||||
'permission': 'system:wiki:article:publish',
|
||||
'sort_order': 5,
|
||||
'parent_code': 'system:wiki:article'
|
||||
},
|
||||
|
||||
# 日志管理菜单
|
||||
{
|
||||
'menu_name': '日志管理',
|
||||
'menu_code': 'system:log',
|
||||
'menu_type': 2, # 菜单
|
||||
'path': '/system/log',
|
||||
'component': 'system/log/index',
|
||||
'icon': 'log',
|
||||
'permission': 'log.view_operationlog',
|
||||
'sort_order': 7,
|
||||
'parent_code': 'system'
|
||||
},
|
||||
{
|
||||
'menu_name': '日志查询',
|
||||
'menu_code': 'system:log:query',
|
||||
'menu_type': 3, # 按钮
|
||||
'permission': 'log.view_operationlog',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'system:log'
|
||||
},
|
||||
{
|
||||
'menu_name': '日志详情',
|
||||
'menu_code': 'system:log:detail',
|
||||
'menu_type': 3,
|
||||
'permission': 'log.view_operationlog',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'system:log'
|
||||
},
|
||||
|
||||
# YOLO古建筑识别模块
|
||||
{
|
||||
'menu_name': 'YOLO识别',
|
||||
'menu_code': 'studio:yolo',
|
||||
'menu_type': 2, # 菜单
|
||||
'path': '/studio/yolo',
|
||||
'component': 'studio/yolo/index',
|
||||
'icon': 'camera',
|
||||
'permission': 'studio:yolo:list',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'studio'
|
||||
},
|
||||
{
|
||||
'menu_name': '图像识别',
|
||||
'menu_code': 'studio:yolo:recognition',
|
||||
'menu_type': 3, # 按钮
|
||||
'permission': 'studio:yolo:recognition',
|
||||
'sort_order': 1,
|
||||
'parent_code': 'studio:yolo'
|
||||
},
|
||||
{
|
||||
'menu_name': '识别历史',
|
||||
'menu_code': 'studio:yolo:history',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:yolo:history',
|
||||
'sort_order': 2,
|
||||
'parent_code': 'studio:yolo'
|
||||
},
|
||||
{
|
||||
'menu_name': '问答记录',
|
||||
'menu_code': 'studio:yolo:question',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:yolo:question',
|
||||
'sort_order': 3,
|
||||
'parent_code': 'studio:yolo'
|
||||
},
|
||||
{
|
||||
'menu_name': '记录收藏',
|
||||
'menu_code': 'studio:yolo:favorite',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:yolo:favorite',
|
||||
'sort_order': 4,
|
||||
'parent_code': 'studio:yolo'
|
||||
},
|
||||
{
|
||||
'menu_name': '记录删除',
|
||||
'menu_code': 'studio:yolo:delete',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:yolo:delete',
|
||||
'sort_order': 5,
|
||||
'parent_code': 'studio:yolo'
|
||||
},
|
||||
{
|
||||
'menu_name': '统计信息',
|
||||
'menu_code': 'studio:yolo:statistics',
|
||||
'menu_type': 3,
|
||||
'permission': 'studio:yolo:statistics',
|
||||
'sort_order': 6,
|
||||
'parent_code': 'studio:yolo'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
20
hertz_studio_django_utils/config/roles_config.py
Normal file
20
hertz_studio_django_utils/config/roles_config.py
Normal file
@@ -0,0 +1,20 @@
|
||||
roles = [
|
||||
{
|
||||
'role_name': '超级管理员',
|
||||
'role_code': 'super_admin',
|
||||
'description': '系统超级管理员,拥有所有权限',
|
||||
'sort_order': 1
|
||||
},
|
||||
{
|
||||
'role_name': '系统管理员',
|
||||
'role_code': 'system_admin',
|
||||
'description': '系统管理员,拥有系统管理权限',
|
||||
'sort_order': 2
|
||||
},
|
||||
{
|
||||
'role_name': '普通用户',
|
||||
'role_code': 'normal_user',
|
||||
'description': '普通用户,基础权限',
|
||||
'sort_order': 3
|
||||
}
|
||||
]
|
||||
4
hertz_studio_django_utils/crypto/__init__.py
Normal file
4
hertz_studio_django_utils/crypto/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .encryption_utils import EncryptionUtils
|
||||
from .password_hashers import MD5PasswordHasher
|
||||
|
||||
__all__ = ['EncryptionUtils', 'MD5PasswordHasher']
|
||||
160
hertz_studio_django_utils/crypto/encryption_utils.py
Normal file
160
hertz_studio_django_utils/crypto/encryption_utils.py
Normal file
@@ -0,0 +1,160 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import secrets
|
||||
from typing import Optional
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
|
||||
class EncryptionUtils:
|
||||
"""
|
||||
加密工具类
|
||||
提供各种加密解密功能
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def generate_salt(length: int = 32) -> str:
|
||||
"""
|
||||
生成随机盐值
|
||||
|
||||
Args:
|
||||
length: 盐值长度
|
||||
|
||||
Returns:
|
||||
str: Base64编码的盐值
|
||||
"""
|
||||
salt = secrets.token_bytes(length)
|
||||
return base64.b64encode(salt).decode('utf-8')
|
||||
|
||||
@staticmethod
|
||||
def md5_hash(data: str, salt: str = '') -> str:
|
||||
"""
|
||||
MD5哈希加密
|
||||
|
||||
Args:
|
||||
data: 待加密数据
|
||||
salt: 盐值
|
||||
|
||||
Returns:
|
||||
str: MD5哈希值
|
||||
"""
|
||||
combined = data + salt
|
||||
md5_hash = hashlib.md5(combined.encode('utf-8'))
|
||||
return md5_hash.hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def sha256_hash(data: str, salt: str = '') -> str:
|
||||
"""
|
||||
SHA256哈希加密
|
||||
|
||||
Args:
|
||||
data: 待加密数据
|
||||
salt: 盐值
|
||||
|
||||
Returns:
|
||||
str: SHA256哈希值
|
||||
"""
|
||||
combined = data + salt
|
||||
sha256_hash = hashlib.sha256(combined.encode('utf-8'))
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def generate_key_from_password(password: str, salt: bytes) -> bytes:
|
||||
"""
|
||||
从密码生成加密密钥
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
salt: 盐值
|
||||
|
||||
Returns:
|
||||
bytes: 加密密钥
|
||||
"""
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
)
|
||||
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
def encrypt_data(data: str, password: str) -> Optional[str]:
|
||||
"""
|
||||
使用密码加密数据
|
||||
|
||||
Args:
|
||||
data: 待加密数据
|
||||
password: 加密密码
|
||||
|
||||
Returns:
|
||||
Optional[str]: 加密后的数据(Base64编码),失败返回None
|
||||
"""
|
||||
try:
|
||||
# 生成随机盐值
|
||||
salt = secrets.token_bytes(16)
|
||||
|
||||
# 从密码生成密钥
|
||||
key = EncryptionUtils.generate_key_from_password(password, salt)
|
||||
|
||||
# 创建Fernet实例
|
||||
fernet = Fernet(key)
|
||||
|
||||
# 加密数据
|
||||
encrypted_data = fernet.encrypt(data.encode('utf-8'))
|
||||
|
||||
# 将盐值和加密数据组合
|
||||
combined = salt + encrypted_data
|
||||
|
||||
# 返回Base64编码的结果
|
||||
return base64.b64encode(combined).decode('utf-8')
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def decrypt_data(encrypted_data: str, password: str) -> Optional[str]:
|
||||
"""
|
||||
使用密码解密数据
|
||||
|
||||
Args:
|
||||
encrypted_data: 加密后的数据(Base64编码)
|
||||
password: 解密密码
|
||||
|
||||
Returns:
|
||||
Optional[str]: 解密后的数据,失败返回None
|
||||
"""
|
||||
try:
|
||||
# Base64解码
|
||||
combined = base64.b64decode(encrypted_data.encode('utf-8'))
|
||||
|
||||
# 分离盐值和加密数据
|
||||
salt = combined[:16]
|
||||
encrypted_bytes = combined[16:]
|
||||
|
||||
# 从密码生成密钥
|
||||
key = EncryptionUtils.generate_key_from_password(password, salt)
|
||||
|
||||
# 创建Fernet实例
|
||||
fernet = Fernet(key)
|
||||
|
||||
# 解密数据
|
||||
decrypted_data = fernet.decrypt(encrypted_bytes)
|
||||
|
||||
return decrypted_data.decode('utf-8')
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def generate_random_key() -> str:
|
||||
"""
|
||||
生成随机加密密钥
|
||||
|
||||
Returns:
|
||||
str: Base64编码的随机密钥
|
||||
"""
|
||||
key = Fernet.generate_key()
|
||||
return key.decode('utf-8')
|
||||
79
hertz_studio_django_utils/crypto/password_hashers.py
Normal file
79
hertz_studio_django_utils/crypto/password_hashers.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import hashlib
|
||||
from django.contrib.auth.hashers import BasePasswordHasher
|
||||
from django.utils.crypto import constant_time_compare
|
||||
|
||||
|
||||
class MD5PasswordHasher(BasePasswordHasher):
|
||||
"""
|
||||
MD5密码哈希器
|
||||
用于兼容旧系统的MD5密码加密
|
||||
"""
|
||||
algorithm = 'md5'
|
||||
library = 'hashlib'
|
||||
|
||||
def encode(self, password, salt):
|
||||
"""
|
||||
编码密码
|
||||
|
||||
Args:
|
||||
password: 原始密码
|
||||
salt: 盐值
|
||||
|
||||
Returns:
|
||||
str: 编码后的密码
|
||||
"""
|
||||
hash_obj = hashlib.md5((salt + password).encode('utf-8'))
|
||||
hash_value = hash_obj.hexdigest()
|
||||
return f'{self.algorithm}${salt}${hash_value}'
|
||||
|
||||
def verify(self, password, encoded):
|
||||
"""
|
||||
验证密码
|
||||
|
||||
Args:
|
||||
password: 原始密码
|
||||
encoded: 编码后的密码
|
||||
|
||||
Returns:
|
||||
bool: 验证结果
|
||||
"""
|
||||
algorithm, salt, hash_value = encoded.split('$', 2)
|
||||
assert algorithm == self.algorithm
|
||||
encoded_2 = self.encode(password, salt)
|
||||
return constant_time_compare(encoded, encoded_2)
|
||||
|
||||
def safe_summary(self, encoded):
|
||||
"""
|
||||
返回密码的安全摘要信息
|
||||
|
||||
Args:
|
||||
encoded: 编码后的密码
|
||||
|
||||
Returns:
|
||||
dict: 摘要信息
|
||||
"""
|
||||
algorithm, salt, hash_value = encoded.split('$', 2)
|
||||
assert algorithm == self.algorithm
|
||||
return {
|
||||
'algorithm': algorithm,
|
||||
'salt': salt[:6] + '...',
|
||||
'hash': hash_value[:6] + '...',
|
||||
}
|
||||
|
||||
def harden_runtime(self, password, encoded):
|
||||
"""
|
||||
硬化运行时间(MD5不需要)
|
||||
"""
|
||||
pass
|
||||
|
||||
def must_update(self, encoded):
|
||||
"""
|
||||
检查是否需要更新密码编码
|
||||
|
||||
Args:
|
||||
encoded: 编码后的密码
|
||||
|
||||
Returns:
|
||||
bool: 是否需要更新
|
||||
"""
|
||||
return False
|
||||
3
hertz_studio_django_utils/email/__init__.py
Normal file
3
hertz_studio_django_utils/email/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .email_service import EmailService
|
||||
|
||||
__all__ = ['EmailService']
|
||||
174
hertz_studio_django_utils/email/email_service.py
Normal file
174
hertz_studio_django_utils/email/email_service.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.conf import settings
|
||||
from django.utils.html import strip_tags
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailService:
|
||||
"""
|
||||
邮件发送服务类
|
||||
提供统一的邮件发送接口
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def send_email(
|
||||
recipient_email: str,
|
||||
subject: str,
|
||||
html_content: str,
|
||||
text_content: Optional[str] = None,
|
||||
from_email: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
发送邮件
|
||||
|
||||
Args:
|
||||
recipient_email: 收件人邮箱
|
||||
subject: 邮件主题
|
||||
html_content: HTML内容
|
||||
text_content: 纯文本内容(可选)
|
||||
from_email: 发件人邮箱(可选)
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
try:
|
||||
# 检查邮件配置
|
||||
if not settings.EMAIL_HOST_USER or not settings.EMAIL_HOST_PASSWORD:
|
||||
logger.warning("邮件配置不完整,使用控制台输出模式")
|
||||
logger.info(f"收件人: {recipient_email}")
|
||||
logger.info(f"主题: {subject}")
|
||||
logger.info(f"内容: {text_content or strip_tags(html_content)[:200]}...")
|
||||
return True
|
||||
|
||||
# 如果没有提供纯文本内容,从HTML中提取
|
||||
if not text_content:
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
# 创建邮件
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=from_email or settings.DEFAULT_FROM_EMAIL,
|
||||
to=[recipient_email]
|
||||
)
|
||||
|
||||
# 添加HTML内容
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
|
||||
# 发送邮件
|
||||
email.send()
|
||||
logger.info(f"邮件发送成功: {recipient_email}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"邮件发送失败: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_verification_code(
|
||||
recipient_email: str,
|
||||
recipient_name: str,
|
||||
verification_code: str,
|
||||
code_type: str = 'register'
|
||||
) -> bool:
|
||||
"""
|
||||
发送验证码邮件
|
||||
|
||||
Args:
|
||||
recipient_email: 收件人邮箱
|
||||
recipient_name: 收件人姓名
|
||||
verification_code: 验证码
|
||||
code_type: 验证码类型(register/reset_password)
|
||||
|
||||
Returns:
|
||||
bool: 发送是否成功
|
||||
"""
|
||||
# 根据类型生成邮件内容
|
||||
email_content = EmailService._generate_verification_email_content(
|
||||
recipient_name, verification_code, code_type
|
||||
)
|
||||
|
||||
return EmailService.send_email(
|
||||
recipient_email=recipient_email,
|
||||
subject=email_content['subject'],
|
||||
html_content=email_content['html_content'],
|
||||
text_content=email_content['text_content']
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _generate_verification_email_content(
|
||||
recipient_name: str,
|
||||
verification_code: str,
|
||||
code_type: str
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
生成验证码邮件内容
|
||||
|
||||
Args:
|
||||
recipient_name: 收件人姓名
|
||||
verification_code: 验证码
|
||||
code_type: 验证码类型
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含subject, html_content, text_content的字典
|
||||
"""
|
||||
if code_type == 'register':
|
||||
subject = '🔐 注册验证码 - Hertz Server Django'
|
||||
title = '注册验证码'
|
||||
description = '感谢您注册 Hertz Server Django!请使用以下验证码完成注册:'
|
||||
elif code_type == 'reset_password':
|
||||
subject = '🔐 密码重置验证码 - Hertz Server Django'
|
||||
title = '密码重置验证码'
|
||||
description = '您正在重置密码,请使用以下验证码完成操作:'
|
||||
else:
|
||||
subject = '🔐 邮箱验证码 - Hertz Server Django'
|
||||
title = '邮箱验证码'
|
||||
description = '请使用以下验证码完成验证:'
|
||||
|
||||
html_content = f'''
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: #28a745; color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 24px;">🔐 {title}</h1>
|
||||
</div>
|
||||
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong>,</p>
|
||||
<p>{description}</p>
|
||||
<div style="text-align: center; margin: 40px 0;">
|
||||
<div style="background: #f8f9fa; border: 2px dashed #28a745; padding: 20px; border-radius: 10px; display: inline-block;">
|
||||
<span style="font-size: 32px; font-weight: bold; color: #28a745; letter-spacing: 5px;">{verification_code}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 14px;">验证码有效期为5分钟,请尽快使用。如果您没有进行此操作,请忽略此邮件。</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
text_content = f'''
|
||||
{title}
|
||||
|
||||
亲爱的 {recipient_name},
|
||||
|
||||
{description}
|
||||
|
||||
验证码:{verification_code}
|
||||
|
||||
验证码有效期为5分钟,请尽快使用。如果您没有进行此操作,请忽略此邮件。
|
||||
|
||||
此致
|
||||
Hertz Server Django 团队
|
||||
'''
|
||||
|
||||
return {
|
||||
'subject': subject,
|
||||
'html_content': html_content,
|
||||
'text_content': text_content
|
||||
}
|
||||
0
hertz_studio_django_utils/log/__init__.py
Normal file
0
hertz_studio_django_utils/log/__init__.py
Normal file
368
hertz_studio_django_utils/log/log_decorator.py
Normal file
368
hertz_studio_django_utils/log/log_decorator.py
Normal file
@@ -0,0 +1,368 @@
|
||||
from functools import wraps
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import JsonResponse
|
||||
import json
|
||||
import logging
|
||||
|
||||
# 设置日志记录器
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def operation_log(action_type, module, description=None, target_model=None):
|
||||
"""
|
||||
操作日志装饰器
|
||||
|
||||
Args:
|
||||
action_type (str): 操作类型,如 'create', 'update', 'delete' 等
|
||||
module (str): 操作模块,如 '用户管理', '通知管理' 等
|
||||
description (str, optional): 操作描述,如果不提供则自动生成
|
||||
target_model (str, optional): 目标模型名称
|
||||
|
||||
Usage:
|
||||
@operation_log('create', '用户管理', '创建新用户', 'User')
|
||||
def create_user(request):
|
||||
# 视图函数逻辑
|
||||
pass
|
||||
"""
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# 检查是否是类视图方法(第一个参数是self,第二个是request)
|
||||
if len(args) >= 2 and hasattr(args[0], '__class__') and hasattr(args[1], 'META'):
|
||||
# 类视图方法
|
||||
self_instance = args[0]
|
||||
request = args[1]
|
||||
print(f"\n=== 类视图装饰器被调用 ===")
|
||||
print(f"类名: {self_instance.__class__.__name__}")
|
||||
print(f"函数名: {view_func.__name__}")
|
||||
elif len(args) >= 1 and hasattr(args[0], 'META'):
|
||||
# 函数视图
|
||||
request = args[0]
|
||||
print(f"\n=== 函数视图装饰器被调用 ===")
|
||||
print(f"函数名: {view_func.__name__}")
|
||||
else:
|
||||
# 无法识别的调用方式,直接执行原函数
|
||||
return view_func(*args, **kwargs)
|
||||
|
||||
print(f"操作类型: {action_type}")
|
||||
print(f"模块: {module}")
|
||||
print(f"请求方法: {request.method}")
|
||||
print(f"请求路径: {request.path}")
|
||||
print(f"=== 装饰器调用信息结束 ===\n")
|
||||
# 延迟导入避免循环导入
|
||||
from hertz_studio_django_log.models import OperationLog
|
||||
|
||||
# 获取用户信息
|
||||
user = None
|
||||
if hasattr(request, 'user') and request.user.is_authenticated:
|
||||
user = request.user
|
||||
|
||||
# 获取请求信息
|
||||
ip_address = get_client_ip(request)
|
||||
user_agent = request.META.get('HTTP_USER_AGENT', '')[:500] # 限制长度
|
||||
|
||||
# 获取请求数据
|
||||
request_data = get_request_data(request)
|
||||
|
||||
response = None
|
||||
response_status = 200
|
||||
target_id = None
|
||||
|
||||
try:
|
||||
# 执行原始视图函数
|
||||
if len(args) >= 2 and hasattr(args[0], '__class__') and hasattr(args[1], 'META'):
|
||||
# 类视图方法,去掉self参数
|
||||
response = view_func(args[0], request, *args[2:], **kwargs)
|
||||
else:
|
||||
# 函数视图
|
||||
response = view_func(request, *args[1:], **kwargs)
|
||||
|
||||
# 获取响应状态码
|
||||
if hasattr(response, 'status_code'):
|
||||
response_status = response.status_code
|
||||
|
||||
# 尝试获取目标ID
|
||||
target_id = extract_target_id(response, kwargs)
|
||||
|
||||
except Exception as e:
|
||||
response_status = 500
|
||||
logger.error(f"视图函数执行错误: {str(e)}")
|
||||
# 打印详细的错误信息到控制台
|
||||
import traceback
|
||||
print(f"\n=== 装饰器捕获到异常 ===")
|
||||
print(f"异常类型: {type(e).__name__}")
|
||||
print(f"异常信息: {str(e)}")
|
||||
print(f"异常堆栈:")
|
||||
traceback.print_exc()
|
||||
print(f"=== 装饰器异常信息结束 ===\n")
|
||||
# 重新抛出异常,让Django处理
|
||||
raise
|
||||
|
||||
# 异步记录操作日志
|
||||
try:
|
||||
print(f"\n=== 开始记录操作日志 ===")
|
||||
print(f"用户: {user}")
|
||||
print(f"操作类型: {action_type}")
|
||||
print(f"模块: {module}")
|
||||
print(f"描述: {description or f'{action_type}操作 - {module}'}")
|
||||
print(f"目标模型: {target_model}")
|
||||
print(f"目标ID: {target_id}")
|
||||
print(f"IP地址: {ip_address}")
|
||||
print(f"请求数据: {request_data}")
|
||||
print(f"响应状态: {response_status}")
|
||||
|
||||
log_operation(
|
||||
user=user,
|
||||
action_type=action_type,
|
||||
module=module,
|
||||
description=description or f"{action_type}操作 - {module}",
|
||||
target_model=target_model,
|
||||
target_id=target_id,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
request_data=request_data,
|
||||
response_status=response_status
|
||||
)
|
||||
print(f"操作日志记录成功")
|
||||
print(f"=== 操作日志记录结束 ===\n")
|
||||
except Exception as log_error:
|
||||
# 日志记录失败不应该影响正常业务
|
||||
print(f"\n=== 操作日志记录失败 ===")
|
||||
print(f"错误类型: {type(log_error).__name__}")
|
||||
print(f"错误信息: {str(log_error)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f"=== 操作日志记录失败信息结束 ===\n")
|
||||
logger.error(f"操作日志记录失败: {log_error}")
|
||||
|
||||
return response
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def get_client_ip(request):
|
||||
"""
|
||||
获取客户端真实IP地址
|
||||
"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0].strip()
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
|
||||
def get_request_data(request):
|
||||
"""
|
||||
安全地获取请求数据
|
||||
"""
|
||||
request_data = {}
|
||||
try:
|
||||
if request.method == 'GET':
|
||||
request_data = dict(request.GET)
|
||||
elif request.method == 'POST':
|
||||
content_type = request.META.get('CONTENT_TYPE', '')
|
||||
if 'application/json' in content_type:
|
||||
if hasattr(request, 'body') and request.body:
|
||||
request_data = json.loads(request.body.decode('utf-8'))
|
||||
else:
|
||||
request_data = dict(request.POST)
|
||||
elif request.method in ['PUT', 'PATCH', 'DELETE']:
|
||||
if hasattr(request, 'body') and request.body:
|
||||
request_data = json.loads(request.body.decode('utf-8'))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError, AttributeError) as e:
|
||||
logger.warning(f"解析请求数据失败: {str(e)}")
|
||||
request_data = {}
|
||||
|
||||
# 过滤敏感信息
|
||||
sensitive_fields = ['password', 'token', 'secret', 'key', 'csrf_token']
|
||||
if isinstance(request_data, dict):
|
||||
for field in sensitive_fields:
|
||||
if field in request_data:
|
||||
request_data[field] = '***'
|
||||
|
||||
return request_data
|
||||
|
||||
def extract_target_id(response, kwargs):
|
||||
"""
|
||||
从响应或URL参数中提取目标ID
|
||||
"""
|
||||
target_id = None
|
||||
|
||||
# 从响应中获取ID
|
||||
try:
|
||||
if hasattr(response, 'data') and isinstance(response.data, dict):
|
||||
if 'id' in response.data:
|
||||
target_id = response.data['id']
|
||||
elif 'data' in response.data and isinstance(response.data['data'], dict):
|
||||
if 'id' in response.data['data']:
|
||||
target_id = response.data['data']['id']
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
|
||||
# 从URL参数中获取ID
|
||||
if not target_id and kwargs:
|
||||
for key, value in kwargs.items():
|
||||
if key.endswith('_id') or key == 'pk' or key == 'id':
|
||||
try:
|
||||
target_id = int(value)
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return target_id
|
||||
|
||||
def log_operation(user, action_type, module, description, target_model=None,
|
||||
target_id=None, ip_address=None, user_agent=None,
|
||||
request_data=None, response_status=None):
|
||||
"""
|
||||
记录操作日志
|
||||
"""
|
||||
from hertz_studio_django_log.models import OperationLog
|
||||
|
||||
# 如果用户未登录,记录为匿名用户操作(可选择是否记录)
|
||||
if user is None:
|
||||
# 对于某些重要操作,即使是匿名用户也要记录
|
||||
# 可以通过配置决定是否记录匿名用户操作
|
||||
logger.info(f"记录匿名用户的操作日志: {action_type} - {module} - {description}")
|
||||
# 为匿名用户创建一个临时用户对象或跳过用户字段
|
||||
# 这里我们选择跳过匿名用户的日志记录,保持原有逻辑
|
||||
# 如果需要记录匿名用户操作,可以注释掉下面的return语句
|
||||
return
|
||||
|
||||
# 限制数据长度避免数据库错误
|
||||
if user_agent and len(user_agent) > 500:
|
||||
user_agent = user_agent[:500]
|
||||
|
||||
if description and len(description) > 255:
|
||||
description = description[:255]
|
||||
|
||||
# 限制请求数据大小
|
||||
if request_data and len(str(request_data)) > 5000:
|
||||
request_data = {'message': '请求数据过大,已省略'}
|
||||
|
||||
# 使用模型的create_log方法来创建日志
|
||||
OperationLog.create_log(
|
||||
user=user,
|
||||
action_type=action_type,
|
||||
module=module,
|
||||
description=description,
|
||||
target_model=target_model,
|
||||
target_id=target_id,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
request_data=request_data,
|
||||
response_status=response_status
|
||||
)
|
||||
|
||||
def auto_log(action_type, module=None):
|
||||
"""
|
||||
自动日志装饰器,根据视图类名和方法名自动推断模块和描述
|
||||
|
||||
Args:
|
||||
action_type (str): 操作类型
|
||||
module (str, optional): 模块名称,如果不提供则从视图类名推断
|
||||
"""
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# 延迟导入避免循环导入
|
||||
from hertz_studio_django_log.models import OperationLog
|
||||
|
||||
# 获取request对象
|
||||
request = None
|
||||
if args and hasattr(args[0], '__class__') and hasattr(args[0], '__module__'):
|
||||
# 这是类方法,request是第二个参数
|
||||
request = args[1] if len(args) > 1 else None
|
||||
view_instance = args[0]
|
||||
class_name = view_instance.__class__.__name__
|
||||
else:
|
||||
# 这是函数视图,request是第一个参数
|
||||
request = args[0] if args else None
|
||||
class_name = view_func.__name__
|
||||
|
||||
# 检查request对象是否有效
|
||||
if not request or not hasattr(request, 'META'):
|
||||
return view_func(*args, **kwargs)
|
||||
|
||||
# 获取用户信息
|
||||
user = None
|
||||
if hasattr(request, 'user') and request.user.is_authenticated:
|
||||
user = request.user
|
||||
|
||||
# 自动推断模块名称
|
||||
auto_module = module
|
||||
if not auto_module:
|
||||
if 'User' in class_name:
|
||||
auto_module = '用户管理'
|
||||
elif 'Notification' in class_name:
|
||||
auto_module = '通知管理'
|
||||
elif 'Config' in class_name:
|
||||
auto_module = '系统配置'
|
||||
elif 'File' in class_name:
|
||||
auto_module = '文件管理'
|
||||
elif 'AI' in class_name or 'Chat' in class_name:
|
||||
auto_module = 'AI助手'
|
||||
elif 'Wiki' in class_name:
|
||||
auto_module = '知识管理'
|
||||
else:
|
||||
auto_module = '系统管理'
|
||||
|
||||
# 自动生成描述
|
||||
action_map = {
|
||||
'create': '创建',
|
||||
'update': '更新',
|
||||
'delete': '删除',
|
||||
'view': '查看',
|
||||
'list': '列表查看',
|
||||
'login': '登录',
|
||||
'logout': '登出'
|
||||
}
|
||||
auto_description = f"{action_map.get(action_type, action_type)} - {auto_module}"
|
||||
|
||||
# 获取请求信息
|
||||
ip_address = get_client_ip(request)
|
||||
user_agent = request.META.get('HTTP_USER_AGENT', '')[:500]
|
||||
request_data = get_request_data(request)
|
||||
|
||||
response = None
|
||||
response_status = 200
|
||||
target_id = None
|
||||
|
||||
try:
|
||||
# 执行原始视图函数
|
||||
response = view_func(*args, **kwargs)
|
||||
|
||||
# 获取响应状态码
|
||||
if hasattr(response, 'status_code'):
|
||||
response_status = response.status_code
|
||||
|
||||
# 尝试获取目标ID
|
||||
target_id = extract_target_id(response, kwargs)
|
||||
|
||||
except Exception as e:
|
||||
response_status = 500
|
||||
logger.error(f"视图函数执行错误: {str(e)}")
|
||||
# 重新抛出异常,让Django处理
|
||||
raise
|
||||
|
||||
# 异步记录操作日志
|
||||
try:
|
||||
log_operation(
|
||||
user=user,
|
||||
action_type=action_type,
|
||||
module=auto_module,
|
||||
description=auto_description,
|
||||
target_model=auto_module,
|
||||
target_id=target_id,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
request_data=request_data,
|
||||
response_status=response_status
|
||||
)
|
||||
except Exception as log_error:
|
||||
# 日志记录失败不应该影响正常业务
|
||||
logger.error(f"操作日志记录失败: {log_error}")
|
||||
|
||||
return response
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
0
hertz_studio_django_utils/ollama/__init__.py
Normal file
0
hertz_studio_django_utils/ollama/__init__.py
Normal file
207
hertz_studio_django_utils/ollama/ollama_client.py
Normal file
207
hertz_studio_django_utils/ollama/ollama_client.py
Normal file
@@ -0,0 +1,207 @@
|
||||
import json
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import requests
|
||||
from typing import List, Dict, Any, AsyncGenerator, Optional
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class OllamaClient:
|
||||
"""
|
||||
Ollama API客户端
|
||||
用于与Ollama服务通信,获取LLM响应
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: str = None):
|
||||
"""
|
||||
初始化Ollama客户端
|
||||
|
||||
Args:
|
||||
base_url: Ollama服务地址,默认从settings中获取
|
||||
"""
|
||||
self.base_url = base_url or getattr(settings, "OLLAMA_BASE_URL", "http://localhost:11434")
|
||||
self.generate_url = f"{self.base_url}/api/generate"
|
||||
self.chat_url = f"{self.base_url}/api/chat"
|
||||
|
||||
async def generate_stream(
|
||||
self,
|
||||
model: str,
|
||||
prompt: str,
|
||||
system_prompt: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
top_p: float = 0.9,
|
||||
top_k: int = 40,
|
||||
max_tokens: int = 2048
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
生成文本流
|
||||
|
||||
Args:
|
||||
model: 模型名称,如deepseek-r1:1.5b
|
||||
prompt: 用户输入的提示
|
||||
system_prompt: 系统提示
|
||||
temperature: 温度参数
|
||||
top_p: top-p采样参数
|
||||
top_k: top-k采样参数
|
||||
max_tokens: 最大生成token数
|
||||
|
||||
Yields:
|
||||
生成的文本片段
|
||||
"""
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"stream": True,
|
||||
"options": {
|
||||
"temperature": temperature,
|
||||
"top_p": top_p,
|
||||
"top_k": top_k,
|
||||
"num_predict": max_tokens
|
||||
}
|
||||
}
|
||||
|
||||
if system_prompt:
|
||||
payload["system"] = system_prompt
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(self.generate_url, json=payload) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
raise Exception(f"Ollama API error: {response.status} - {error_text}")
|
||||
|
||||
# 流式读取响应
|
||||
async for line in response.content:
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
if "response" in data:
|
||||
yield data["response"]
|
||||
|
||||
# 如果接收到完成标志,退出
|
||||
if data.get("done", False):
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
async def chat_stream(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.7,
|
||||
top_p: float = 0.9,
|
||||
top_k: int = 40,
|
||||
max_tokens: int = 2048
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
流式聊天接口
|
||||
|
||||
Args:
|
||||
model: 模型名称
|
||||
messages: 消息历史,格式为[{"role": "user", "content": "..."}, ...]
|
||||
temperature: 温度参数
|
||||
top_p: top-p采样参数
|
||||
top_k: top-k采样参数
|
||||
max_tokens: 最大生成token数
|
||||
|
||||
Yields:
|
||||
生成的文本片段
|
||||
"""
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"stream": True,
|
||||
"options": {
|
||||
"temperature": temperature,
|
||||
"top_p": top_p,
|
||||
"top_k": top_k,
|
||||
"num_predict": max_tokens
|
||||
}
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(self.chat_url, json=payload) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
raise Exception(f"Ollama API error: {response.status} - {error_text}")
|
||||
|
||||
# 流式读取响应
|
||||
async for line in response.content:
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
if "message" in data and "content" in data["message"]:
|
||||
yield data["message"]["content"]
|
||||
|
||||
# 如果接收到完成标志,退出
|
||||
if data.get("done", False):
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
async def check_model_availability(self, model: str) -> bool:
|
||||
"""
|
||||
检查模型是否可用
|
||||
|
||||
Args:
|
||||
model: 模型名称
|
||||
|
||||
Returns:
|
||||
模型是否可用
|
||||
"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{self.base_url}/api/tags") as response:
|
||||
if response.status != 200:
|
||||
return False
|
||||
|
||||
data = await response.json()
|
||||
models = data.get("models", [])
|
||||
return any(m["name"] == model for m in models)
|
||||
|
||||
def chat_completion(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.7,
|
||||
top_p: float = 0.9,
|
||||
top_k: int = 40,
|
||||
max_tokens: int = 2048
|
||||
) -> str:
|
||||
"""
|
||||
同步聊天接口,用于REST API
|
||||
|
||||
Args:
|
||||
model: 模型名称
|
||||
messages: 消息历史,格式为[{"role": "user", "content": "..."}, ...]
|
||||
temperature: 温度参数
|
||||
top_p: top-p采样参数
|
||||
top_k: top-k采样参数
|
||||
max_tokens: 最大生成token数
|
||||
|
||||
Returns:
|
||||
生成的完整回复文本
|
||||
"""
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"stream": False, # 非流式
|
||||
"options": {
|
||||
"temperature": temperature,
|
||||
"top_p": top_p,
|
||||
"top_k": top_k,
|
||||
"num_predict": max_tokens
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(self.chat_url, json=payload)
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Ollama API error: {response.status_code} - {response.text}")
|
||||
|
||||
data = response.json()
|
||||
if "message" in data and "content" in data["message"]:
|
||||
return data["message"]["content"]
|
||||
|
||||
raise Exception("Unexpected response format from Ollama API")
|
||||
185
hertz_studio_django_utils/responses/HertzResponse.py
Normal file
185
hertz_studio_django_utils/responses/HertzResponse.py
Normal file
@@ -0,0 +1,185 @@
|
||||
from django.http import JsonResponse
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class HertzResponse:
|
||||
"""
|
||||
Hertz统一响应类
|
||||
提供标准化的API响应格式
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def success(data: Any = None, message: str = "操作成功", code: int = 200) -> JsonResponse:
|
||||
"""
|
||||
成功响应
|
||||
|
||||
Args:
|
||||
data: 响应数据
|
||||
message: 响应消息
|
||||
code: HTTP状态码
|
||||
|
||||
Returns:
|
||||
JsonResponse: 标准化的成功响应
|
||||
"""
|
||||
response_data = {
|
||||
'success': True,
|
||||
'code': code,
|
||||
'message': message,
|
||||
'data': data
|
||||
}
|
||||
return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
@staticmethod
|
||||
def fail(message: str = "操作失败", data: Any = None, code: int = 400) -> JsonResponse:
|
||||
"""
|
||||
失败响应(业务逻辑失败)
|
||||
|
||||
Args:
|
||||
message: 失败消息
|
||||
data: 响应数据
|
||||
code: HTTP状态码
|
||||
|
||||
Returns:
|
||||
JsonResponse: 标准化的失败响应
|
||||
"""
|
||||
response_data = {
|
||||
'success': False,
|
||||
'code': code,
|
||||
'message': message,
|
||||
'data': data
|
||||
}
|
||||
return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
@staticmethod
|
||||
def error(message: str = "系统错误", error: str = None, code: int = 500) -> JsonResponse:
|
||||
"""
|
||||
错误响应(系统错误)
|
||||
|
||||
Args:
|
||||
message: 错误消息
|
||||
error: 详细错误信息
|
||||
code: HTTP状态码
|
||||
|
||||
Returns:
|
||||
JsonResponse: 标准化的错误响应
|
||||
"""
|
||||
response_data = {
|
||||
'success': False,
|
||||
'code': code,
|
||||
'message': message
|
||||
}
|
||||
|
||||
if error:
|
||||
response_data['error'] = error
|
||||
|
||||
return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
@staticmethod
|
||||
def unauthorized(message: str = "未授权访问", code: int = 401) -> JsonResponse:
|
||||
"""
|
||||
未授权响应
|
||||
|
||||
Args:
|
||||
message: 响应消息
|
||||
code: HTTP状态码
|
||||
|
||||
Returns:
|
||||
JsonResponse: 标准化的未授权响应
|
||||
"""
|
||||
response_data = {
|
||||
'success': False,
|
||||
'code': code,
|
||||
'message': message
|
||||
}
|
||||
return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
@staticmethod
|
||||
def forbidden(message: str = "禁止访问", code: int = 403) -> JsonResponse:
|
||||
"""
|
||||
禁止访问响应
|
||||
|
||||
Args:
|
||||
message: 响应消息
|
||||
code: HTTP状态码
|
||||
|
||||
Returns:
|
||||
JsonResponse: 标准化的禁止访问响应
|
||||
"""
|
||||
response_data = {
|
||||
'success': False,
|
||||
'code': code,
|
||||
'message': message
|
||||
}
|
||||
return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
@staticmethod
|
||||
def not_found(message: str = "资源未找到", code: int = 404) -> JsonResponse:
|
||||
"""
|
||||
资源未找到响应
|
||||
|
||||
Args:
|
||||
message: 响应消息
|
||||
code: HTTP状态码
|
||||
|
||||
Returns:
|
||||
JsonResponse: 标准化的资源未找到响应
|
||||
"""
|
||||
response_data = {
|
||||
'success': False,
|
||||
'code': code,
|
||||
'message': message
|
||||
}
|
||||
return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
@staticmethod
|
||||
def validation_error(message: str = "参数验证失败", errors: Dict = None, code: int = 422) -> JsonResponse:
|
||||
"""
|
||||
参数验证错误响应
|
||||
|
||||
Args:
|
||||
message: 响应消息
|
||||
errors: 验证错误详情
|
||||
code: HTTP状态码
|
||||
|
||||
Returns:
|
||||
JsonResponse: 标准化的参数验证错误响应
|
||||
"""
|
||||
response_data = {
|
||||
'success': False,
|
||||
'code': code,
|
||||
'message': message
|
||||
}
|
||||
|
||||
if errors:
|
||||
response_data['errors'] = errors
|
||||
|
||||
return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False})
|
||||
|
||||
@staticmethod
|
||||
def custom(success: bool, message: str, data: Any = None, code: int = 200, **kwargs) -> JsonResponse:
|
||||
"""
|
||||
自定义响应
|
||||
|
||||
Args:
|
||||
success: 是否成功
|
||||
message: 响应消息
|
||||
data: 响应数据
|
||||
code: HTTP状态码
|
||||
**kwargs: 其他自定义字段
|
||||
|
||||
Returns:
|
||||
JsonResponse: 自定义响应
|
||||
"""
|
||||
response_data = {
|
||||
'success': success,
|
||||
'code': code,
|
||||
'message': message
|
||||
}
|
||||
|
||||
if data is not None:
|
||||
response_data['data'] = data
|
||||
|
||||
# 添加其他自定义字段
|
||||
response_data.update(kwargs)
|
||||
|
||||
return JsonResponse(response_data, status=code, json_dumps_params={'ensure_ascii': False})
|
||||
3
hertz_studio_django_utils/responses/__init__.py
Normal file
3
hertz_studio_django_utils/responses/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .HertzResponse import HertzResponse
|
||||
|
||||
__all__ = ['HertzResponse']
|
||||
5
hertz_studio_django_utils/validators/__init__.py
Normal file
5
hertz_studio_django_utils/validators/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .email_validator import EmailValidator
|
||||
from .phone_validator import PhoneValidator
|
||||
from .password_validator import PasswordValidator
|
||||
|
||||
__all__ = ['EmailValidator', 'PhoneValidator', 'PasswordValidator']
|
||||
117
hertz_studio_django_utils/validators/email_validator.py
Normal file
117
hertz_studio_django_utils/validators/email_validator.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class EmailValidator:
|
||||
"""
|
||||
邮箱验证器
|
||||
提供邮箱格式验证功能
|
||||
"""
|
||||
|
||||
# 邮箱正则表达式
|
||||
EMAIL_PATTERN = re.compile(
|
||||
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_valid_email(email: str) -> bool:
|
||||
"""
|
||||
验证邮箱格式是否正确
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
bool: 邮箱格式是否正确
|
||||
"""
|
||||
if not email or not isinstance(email, str):
|
||||
return False
|
||||
|
||||
return bool(EmailValidator.EMAIL_PATTERN.match(email.strip()))
|
||||
|
||||
@staticmethod
|
||||
def validate_email(email: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
验证邮箱并返回详细信息
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否有效, 提示信息)
|
||||
"""
|
||||
if not email:
|
||||
return False, "邮箱地址不能为空"
|
||||
|
||||
if not isinstance(email, str):
|
||||
return False, "邮箱地址必须是字符串"
|
||||
|
||||
email = email.strip()
|
||||
|
||||
if len(email) == 0:
|
||||
return False, "邮箱地址不能为空"
|
||||
|
||||
if len(email) > 254:
|
||||
return False, "邮箱地址长度不能超过254个字符"
|
||||
|
||||
if not EmailValidator.EMAIL_PATTERN.match(email):
|
||||
return False, "邮箱地址格式不正确"
|
||||
|
||||
# 检查本地部分长度(@符号前的部分)
|
||||
local_part = email.split('@')[0]
|
||||
if len(local_part) > 64:
|
||||
return False, "邮箱用户名部分长度不能超过64个字符"
|
||||
|
||||
return True, "邮箱地址格式正确"
|
||||
|
||||
@staticmethod
|
||||
def normalize_email(email: str) -> str:
|
||||
"""
|
||||
标准化邮箱地址
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
str: 标准化后的邮箱地址
|
||||
"""
|
||||
if not email or not isinstance(email, str):
|
||||
return ''
|
||||
|
||||
# 去除首尾空格并转换为小写
|
||||
return email.strip().lower()
|
||||
|
||||
@staticmethod
|
||||
def get_email_domain(email: str) -> str:
|
||||
"""
|
||||
获取邮箱域名
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
str: 邮箱域名
|
||||
"""
|
||||
if not EmailValidator.is_valid_email(email):
|
||||
return ''
|
||||
|
||||
return email.split('@')[1].lower()
|
||||
|
||||
@staticmethod
|
||||
def is_common_email_provider(email: str) -> bool:
|
||||
"""
|
||||
检查是否为常见邮箱服务商
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
bool: 是否为常见邮箱服务商
|
||||
"""
|
||||
common_providers = {
|
||||
'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com',
|
||||
'163.com', '126.com', 'qq.com', 'sina.com', 'sohu.com'
|
||||
}
|
||||
|
||||
domain = EmailValidator.get_email_domain(email)
|
||||
return domain in common_providers
|
||||
229
hertz_studio_django_utils/validators/password_validator.py
Normal file
229
hertz_studio_django_utils/validators/password_validator.py
Normal file
@@ -0,0 +1,229 @@
|
||||
import re
|
||||
from typing import Tuple, List
|
||||
|
||||
|
||||
class PasswordValidator:
|
||||
"""
|
||||
密码验证器
|
||||
提供密码强度验证功能
|
||||
"""
|
||||
|
||||
"""
|
||||
生产环境
|
||||
"""
|
||||
# @staticmethod
|
||||
# def validate_password_strength(password: str, min_length: int = 8, max_length: int = 128) -> Tuple[bool, List[str]]:
|
||||
# """
|
||||
# 验证密码强度
|
||||
#
|
||||
# Args:
|
||||
# password: 密码
|
||||
# min_length: 最小长度
|
||||
# max_length: 最大长度
|
||||
#
|
||||
# Returns:
|
||||
# Tuple[bool, List[str]]: (是否通过验证, 错误信息列表)
|
||||
# """
|
||||
# errors = []
|
||||
#
|
||||
# if not password:
|
||||
# errors.append("密码不能为空")
|
||||
# return False, errors
|
||||
#
|
||||
# if not isinstance(password, str):
|
||||
# errors.append("密码必须是字符串")
|
||||
# return False, errors
|
||||
#
|
||||
# # 检查长度
|
||||
# if len(password) < min_length:
|
||||
# errors.append(f"密码长度至少{min_length}位")
|
||||
#
|
||||
# if len(password) > max_length:
|
||||
# errors.append(f"密码长度不能超过{max_length}位")
|
||||
#
|
||||
# # 检查是否包含数字
|
||||
# if not re.search(r'\d', password):
|
||||
# errors.append("密码必须包含至少一个数字")
|
||||
#
|
||||
# # 检查是否包含小写字母
|
||||
# if not re.search(r'[a-z]', password):
|
||||
# errors.append("密码必须包含至少一个小写字母")
|
||||
#
|
||||
# # 检查是否包含大写字母
|
||||
# if not re.search(r'[A-Z]', password):
|
||||
# errors.append("密码必须包含至少一个大写字母")
|
||||
#
|
||||
# return len(errors) == 0, errors
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
开发环境
|
||||
"""
|
||||
# 默认使用下面开发环境,在生产环境中请使用上面的校验规则!
|
||||
@staticmethod
|
||||
def validate_password_strength(password: str,
|
||||
min_length: int = 6,
|
||||
max_length: int = 128) -> Tuple[bool, List[str]]:
|
||||
"""
|
||||
验证密码强度(仅检查长度,不少于 6 位即可)
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
min_length: 最小长度(默认 6)
|
||||
max_length: 最大长度(默认 128)
|
||||
|
||||
Returns:
|
||||
Tuple[bool, List[str]]: (是否通过验证, 错误信息列表)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
if not password:
|
||||
errors.append("密码不能为空")
|
||||
return False, errors
|
||||
|
||||
if not isinstance(password, str):
|
||||
errors.append("密码必须是字符串")
|
||||
return False, errors
|
||||
|
||||
# 仅长度检查
|
||||
if len(password) < min_length:
|
||||
errors.append(f"密码长度至少{min_length}位")
|
||||
|
||||
if len(password) > max_length:
|
||||
errors.append(f"密码长度不能超过{max_length}位")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
|
||||
@staticmethod
|
||||
def validate_simple_password(password: str, min_length: int = 6, max_length: int = 128) -> Tuple[bool, str]:
|
||||
"""
|
||||
简单密码验证(只检查长度和基本字符)
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
min_length: 最小长度
|
||||
max_length: 最大长度
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否通过验证, 错误信息)
|
||||
"""
|
||||
if not password:
|
||||
return False, "密码不能为空"
|
||||
|
||||
if not isinstance(password, str):
|
||||
return False, "密码必须是字符串"
|
||||
|
||||
if len(password) < min_length:
|
||||
return False, f"密码长度至少{min_length}位"
|
||||
|
||||
if len(password) > max_length:
|
||||
return False, f"密码长度不能超过{max_length}位"
|
||||
|
||||
# 检查是否包含数字或字母
|
||||
if not re.search(r'[a-zA-Z0-9]', password):
|
||||
return False, "密码必须包含字母或数字"
|
||||
|
||||
return True, "密码格式正确"
|
||||
|
||||
@staticmethod
|
||||
def check_common_passwords(password: str) -> bool:
|
||||
"""
|
||||
检查是否为常见弱密码
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
|
||||
Returns:
|
||||
bool: 是否为常见弱密码
|
||||
"""
|
||||
common_passwords = {
|
||||
'123456', 'password', '123456789', '12345678', '12345',
|
||||
'1234567', '1234567890', 'qwerty', 'abc123', '111111',
|
||||
'123123123', 'admin', 'letmein', 'welcome', 'monkey',
|
||||
'1234', 'dragon', 'pass', 'master', 'hello',
|
||||
'freedom', 'whatever', 'qazwsx', 'trustno1', 'jordan23'
|
||||
}
|
||||
|
||||
return password.lower() in common_passwords
|
||||
|
||||
@staticmethod
|
||||
def calculate_password_score(password: str) -> int:
|
||||
"""
|
||||
计算密码强度分数(0-100)
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
|
||||
Returns:
|
||||
int: 密码强度分数
|
||||
"""
|
||||
if not password:
|
||||
return 0
|
||||
|
||||
score = 0
|
||||
|
||||
# 长度分数(最多30分)
|
||||
length_score = min(len(password) * 2, 30)
|
||||
score += length_score
|
||||
|
||||
# 字符类型分数
|
||||
if re.search(r'[a-z]', password): # 小写字母
|
||||
score += 10
|
||||
|
||||
if re.search(r'[A-Z]', password): # 大写字母
|
||||
score += 10
|
||||
|
||||
if re.search(r'\d', password): # 数字
|
||||
score += 10
|
||||
|
||||
if re.search(r'[!@#$%^&*(),.?":{}|<>]', password): # 特殊字符
|
||||
score += 15
|
||||
|
||||
# 字符多样性分数
|
||||
unique_chars = len(set(password))
|
||||
diversity_score = min(unique_chars * 2, 25)
|
||||
score += diversity_score
|
||||
|
||||
# 扣分项
|
||||
if PasswordValidator.check_common_passwords(password):
|
||||
score -= 30
|
||||
|
||||
# 重复字符扣分
|
||||
if re.search(r'(.)\1{2,}', password): # 连续3个相同字符
|
||||
score -= 10
|
||||
|
||||
# 连续数字或字母扣分
|
||||
if re.search(r'(012|123|234|345|456|567|678|789|890)', password):
|
||||
score -= 5
|
||||
|
||||
if re.search(r'(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)', password.lower()):
|
||||
score -= 5
|
||||
|
||||
return max(0, min(100, score))
|
||||
|
||||
@staticmethod
|
||||
def get_password_strength_level(password: str) -> str:
|
||||
"""
|
||||
获取密码强度等级
|
||||
|
||||
Args:
|
||||
password: 密码
|
||||
|
||||
Returns:
|
||||
str: 密码强度等级
|
||||
"""
|
||||
score = PasswordValidator.calculate_password_score(password)
|
||||
|
||||
if score >= 80:
|
||||
return "很强"
|
||||
elif score >= 60:
|
||||
return "强"
|
||||
elif score >= 40:
|
||||
return "中等"
|
||||
elif score >= 20:
|
||||
return "弱"
|
||||
else:
|
||||
return "很弱"
|
||||
178
hertz_studio_django_utils/validators/phone_validator.py
Normal file
178
hertz_studio_django_utils/validators/phone_validator.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class PhoneValidator:
|
||||
"""
|
||||
手机号验证器
|
||||
提供手机号格式验证功能
|
||||
"""
|
||||
|
||||
# 中国大陆手机号正则表达式
|
||||
CHINA_MOBILE_PATTERN = re.compile(
|
||||
r'^1[3-9]\d{9}$'
|
||||
)
|
||||
|
||||
# 国际手机号正则表达式(简化版)
|
||||
INTERNATIONAL_PATTERN = re.compile(
|
||||
r'^\+?[1-9]\d{1,14}$'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_valid_china_mobile(phone: str) -> bool:
|
||||
"""
|
||||
验证中国大陆手机号格式是否正确
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
bool: 手机号格式是否正确
|
||||
"""
|
||||
if not phone or not isinstance(phone, str):
|
||||
return False
|
||||
|
||||
# 去除空格和连字符
|
||||
phone = phone.replace(' ', '').replace('-', '')
|
||||
|
||||
return bool(PhoneValidator.CHINA_MOBILE_PATTERN.match(phone))
|
||||
|
||||
@staticmethod
|
||||
def is_valid_international_phone(phone: str) -> bool:
|
||||
"""
|
||||
验证国际手机号格式是否正确
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
bool: 手机号格式是否正确
|
||||
"""
|
||||
if not phone or not isinstance(phone, str):
|
||||
return False
|
||||
|
||||
# 去除空格和连字符
|
||||
phone = phone.replace(' ', '').replace('-', '')
|
||||
|
||||
return bool(PhoneValidator.INTERNATIONAL_PATTERN.match(phone))
|
||||
|
||||
@staticmethod
|
||||
def is_valid_phone(phone: str) -> bool:
|
||||
"""
|
||||
验证手机号格式是否正确(默认使用中国大陆手机号验证)
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
bool: 手机号格式是否正确
|
||||
"""
|
||||
return PhoneValidator.is_valid_china_mobile(phone)
|
||||
|
||||
@staticmethod
|
||||
def validate_china_mobile(phone: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
验证中国大陆手机号并返回详细信息
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否有效, 提示信息)
|
||||
"""
|
||||
if not phone:
|
||||
return False, "手机号不能为空"
|
||||
|
||||
if not isinstance(phone, str):
|
||||
return False, "手机号必须是字符串"
|
||||
|
||||
# 去除空格和连字符
|
||||
phone = phone.replace(' ', '').replace('-', '')
|
||||
|
||||
if len(phone) == 0:
|
||||
return False, "手机号不能为空"
|
||||
|
||||
if len(phone) != 11:
|
||||
return False, "手机号长度必须为11位"
|
||||
|
||||
if not phone.isdigit():
|
||||
return False, "手机号只能包含数字"
|
||||
|
||||
if not phone.startswith('1'):
|
||||
return False, "手机号必须以1开头"
|
||||
|
||||
if phone[1] not in '3456789':
|
||||
return False, "手机号第二位必须是3-9之间的数字"
|
||||
|
||||
return True, "手机号格式正确"
|
||||
|
||||
@staticmethod
|
||||
def normalize_phone(phone: str) -> str:
|
||||
"""
|
||||
标准化手机号
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
str: 标准化后的手机号
|
||||
"""
|
||||
if not phone or not isinstance(phone, str):
|
||||
return ''
|
||||
|
||||
# 去除空格、连字符和括号
|
||||
phone = phone.replace(' ', '').replace('-', '').replace('(', '').replace(')', '')
|
||||
|
||||
# 如果是中国大陆手机号且以+86开头,去除+86
|
||||
if phone.startswith('+86') and len(phone) == 14:
|
||||
phone = phone[3:]
|
||||
elif phone.startswith('86') and len(phone) == 13:
|
||||
phone = phone[2:]
|
||||
|
||||
return phone
|
||||
|
||||
@staticmethod
|
||||
def get_mobile_carrier(phone: str) -> str:
|
||||
"""
|
||||
获取手机号运营商(仅支持中国大陆)
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
str: 运营商名称
|
||||
"""
|
||||
if not PhoneValidator.is_valid_china_mobile(phone):
|
||||
return '未知'
|
||||
|
||||
phone = PhoneValidator.normalize_phone(phone)
|
||||
prefix = phone[:3]
|
||||
|
||||
# 中国移动
|
||||
china_mobile_prefixes = {
|
||||
'134', '135', '136', '137', '138', '139',
|
||||
'147', '150', '151', '152', '157', '158', '159',
|
||||
'172', '178', '182', '183', '184', '187', '188',
|
||||
'195', '197', '198'
|
||||
}
|
||||
|
||||
# 中国联通
|
||||
china_unicom_prefixes = {
|
||||
'130', '131', '132', '145', '155', '156',
|
||||
'166', '171', '175', '176', '185', '186', '196'
|
||||
}
|
||||
|
||||
# 中国电信
|
||||
china_telecom_prefixes = {
|
||||
'133', '149', '153', '173', '174', '177',
|
||||
'180', '181', '189', '191', '193', '199'
|
||||
}
|
||||
|
||||
if prefix in china_mobile_prefixes:
|
||||
return '中国移动'
|
||||
elif prefix in china_unicom_prefixes:
|
||||
return '中国联通'
|
||||
elif prefix in china_telecom_prefixes:
|
||||
return '中国电信'
|
||||
else:
|
||||
return '未知运营商'
|
||||
BIN
hertz_studio_django_utils/yolo/Train/pt/yolo11n.pt
Normal file
BIN
hertz_studio_django_utils/yolo/Train/pt/yolo11n.pt
Normal file
Binary file not shown.
BIN
hertz_studio_django_utils/yolo/Train/pt/yolov8n.pt
Normal file
BIN
hertz_studio_django_utils/yolo/Train/pt/yolov8n.pt
Normal file
Binary file not shown.
54
hertz_studio_django_utils/yolo/Train/train.py
Normal file
54
hertz_studio_django_utils/yolo/Train/train.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import warnings
|
||||
import shutil
|
||||
import os
|
||||
|
||||
warnings.filterwarnings('ignore')
|
||||
from ultralytics import YOLO
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 数据集路径和项目配置
|
||||
dataset_name = "Car_Accident_Detection.v2i.yolov8" # 数据集文件夹名称(保证放在Dataset文件夹下)
|
||||
|
||||
dataset_path = os.path.join("Dataset", dataset_name, "data.yaml") #数据集完整路径
|
||||
project_name = 'runs' #训练结果输出文件夹
|
||||
# 模型配置
|
||||
model = YOLO('ultralytics/cfg/models/v8/yolov8.yaml')
|
||||
# 如何切换模型版本, 上面的ymal文件可以改为 yolov11s.yaml就是使用的v11s,
|
||||
# 类似某个改进的yaml文件名称为yolov11-XXX.yaml那么如果想使用其它版本就把上面的名称改为yolov11l-XXX.yaml即可(改的是上面YOLO中间的名字不是配置文件的)!
|
||||
model.load('pt/yolov8n.pt') # 是否加载预训练权重,科研不建议大家加载否则很难提升精度
|
||||
|
||||
# 开始训练
|
||||
results = model.train(data=dataset_path,
|
||||
# 如果大家任务是其它的'ultralytics/cfg/default.yaml'找到这里修改task可以改成detect, segment, classify, pose
|
||||
cache=False,
|
||||
imgsz=640,
|
||||
epochs=100,
|
||||
single_cls=False, # 是否是单类别检测
|
||||
batch=16,
|
||||
close_mosaic=0,
|
||||
workers=4,
|
||||
device='0',
|
||||
optimizer='SGD', # using SGD 优化器 默认为auto建议大家使用固定的.
|
||||
# resume=, # 续训的话这里填写True, yaml文件的地方改为lats.pt的地址,需要注意的是如果你设置训练200轮次模型训练了200轮次是没有办法进行续训的.
|
||||
amp=True, # 如果出现训练损失为Nan可以关闭amp
|
||||
project=project_name,
|
||||
name=dataset_name,
|
||||
)
|
||||
|
||||
# 训练完成后,复制data.yaml文件到训练结果文件夹,方便导入系统使用
|
||||
try:
|
||||
# 构建训练结果文件夹路径
|
||||
result_folder = os.path.join(project_name, dataset_name)
|
||||
|
||||
# 确保结果文件夹存在
|
||||
if os.path.exists(result_folder):
|
||||
# 复制data.yaml文件到结果文件夹
|
||||
source_yaml = dataset_path
|
||||
destination_yaml = os.path.join(result_folder, 'data.yaml')
|
||||
|
||||
shutil.copy2(source_yaml, destination_yaml)
|
||||
else:
|
||||
print(f"训练结果文件夹不存在: {result_folder}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"复制 data.yaml 文件时出错: {str(e)}")
|
||||
30
hertz_studio_django_utils/yolo/Train/ultralytics/__init__.py
Normal file
30
hertz_studio_django_utils/yolo/Train/ultralytics/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
__version__ = "8.3.176"
|
||||
|
||||
import os
|
||||
|
||||
# Set ENV variables (place before imports)
|
||||
if not os.environ.get("OMP_NUM_THREADS"):
|
||||
os.environ["OMP_NUM_THREADS"] = "1" # default for reduced CPU utilization during training
|
||||
|
||||
from ultralytics.models import NAS, RTDETR, SAM, YOLO, YOLOE, FastSAM, YOLOWorld
|
||||
from ultralytics.utils import ASSETS, SETTINGS
|
||||
from ultralytics.utils.checks import check_yolo as checks
|
||||
from ultralytics.utils.downloads import download
|
||||
|
||||
settings = SETTINGS
|
||||
__all__ = (
|
||||
"__version__",
|
||||
"ASSETS",
|
||||
"YOLO",
|
||||
"YOLOWorld",
|
||||
"YOLOE",
|
||||
"NAS",
|
||||
"SAM",
|
||||
"FastSAM",
|
||||
"RTDETR",
|
||||
"checks",
|
||||
"download",
|
||||
"settings",
|
||||
)
|
||||
BIN
hertz_studio_django_utils/yolo/Train/ultralytics/assets/bus.jpg
Normal file
BIN
hertz_studio_django_utils/yolo/Train/ultralytics/assets/bus.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
1025
hertz_studio_django_utils/yolo/Train/ultralytics/cfg/__init__.py
Normal file
1025
hertz_studio_django_utils/yolo/Train/ultralytics/cfg/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Argoverse-HD dataset (ring-front-center camera) https://www.cs.cmu.edu/~mengtial/proj/streaming/ by Argo AI
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/argoverse/
|
||||
# Example usage: yolo train data=Argoverse.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── Argoverse ← downloads here (31.5 GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: Argoverse # dataset root dir
|
||||
train: Argoverse-1.1/images/train/ # train images (relative to 'path') 39384 images
|
||||
val: Argoverse-1.1/images/val/ # val images (relative to 'path') 15062 images
|
||||
test: Argoverse-1.1/images/test/ # test images (optional) https://eval.ai/web/challenges/challenge-page/800/overview
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: person
|
||||
1: bicycle
|
||||
2: car
|
||||
3: motorcycle
|
||||
4: bus
|
||||
5: truck
|
||||
6: traffic_light
|
||||
7: stop_sign
|
||||
|
||||
# Download script/URL (optional) ---------------------------------------------------------------------------------------
|
||||
download: |
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from tqdm import tqdm
|
||||
from ultralytics.utils.downloads import download
|
||||
|
||||
def argoverse2yolo(set):
|
||||
"""Convert Argoverse dataset annotations to YOLO format for object detection tasks."""
|
||||
labels = {}
|
||||
a = json.load(open(set, "rb"))
|
||||
for annot in tqdm(a["annotations"], desc=f"Converting {set} to YOLOv5 format..."):
|
||||
img_id = annot["image_id"]
|
||||
img_name = a["images"][img_id]["name"]
|
||||
img_label_name = f"{img_name[:-3]}txt"
|
||||
|
||||
cls = annot["category_id"] # instance class id
|
||||
x_center, y_center, width, height = annot["bbox"]
|
||||
x_center = (x_center + width / 2) / 1920.0 # offset and scale
|
||||
y_center = (y_center + height / 2) / 1200.0 # offset and scale
|
||||
width /= 1920.0 # scale
|
||||
height /= 1200.0 # scale
|
||||
|
||||
img_dir = set.parents[2] / "Argoverse-1.1" / "labels" / a["seq_dirs"][a["images"][annot["image_id"]]["sid"]]
|
||||
if not img_dir.exists():
|
||||
img_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
k = str(img_dir / img_label_name)
|
||||
if k not in labels:
|
||||
labels[k] = []
|
||||
labels[k].append(f"{cls} {x_center} {y_center} {width} {height}\n")
|
||||
|
||||
for k in labels:
|
||||
with open(k, "w", encoding="utf-8") as f:
|
||||
f.writelines(labels[k])
|
||||
|
||||
|
||||
# Download 'https://argoverse-hd.s3.us-east-2.amazonaws.com/Argoverse-HD-Full.zip' (deprecated S3 link)
|
||||
dir = Path(yaml["path"]) # dataset root dir
|
||||
urls = ["https://drive.google.com/file/d/1st9qW3BeIwQsnR0t8mRpvbsSWIo16ACi/view?usp=drive_link"]
|
||||
print("\n\nWARNING: Argoverse dataset MUST be downloaded manually, autodownload will NOT work.")
|
||||
print(f"WARNING: Manually download Argoverse dataset '{urls[0]}' to '{dir}' and re-run your command.\n\n")
|
||||
# download(urls, dir=dir)
|
||||
|
||||
# Convert
|
||||
annotations_dir = "Argoverse-HD/annotations/"
|
||||
(dir / "Argoverse-1.1" / "tracking").rename(dir / "Argoverse-1.1" / "images") # rename 'tracking' to 'images'
|
||||
for d in "train.json", "val.json":
|
||||
argoverse2yolo(dir / annotations_dir / d) # convert Argoverse annotations to YOLO labels
|
||||
@@ -0,0 +1,37 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# DOTA 1.5 dataset https://captain-whu.github.io/DOTA/index.html for object detection in aerial images by Wuhan University
|
||||
# Documentation: https://docs.ultralytics.com/datasets/obb/dota-v2/
|
||||
# Example usage: yolo train model=yolov8n-obb.pt data=DOTAv1.5.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── dota1.5 ← downloads here (2GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: DOTAv1.5 # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 1411 images
|
||||
val: images/val # val images (relative to 'path') 458 images
|
||||
test: images/test # test images (optional) 937 images
|
||||
|
||||
# Classes for DOTA 1.5
|
||||
names:
|
||||
0: plane
|
||||
1: ship
|
||||
2: storage tank
|
||||
3: baseball diamond
|
||||
4: tennis court
|
||||
5: basketball court
|
||||
6: ground track field
|
||||
7: harbor
|
||||
8: bridge
|
||||
9: large vehicle
|
||||
10: small vehicle
|
||||
11: helicopter
|
||||
12: roundabout
|
||||
13: soccer ball field
|
||||
14: swimming pool
|
||||
15: container crane
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/DOTAv1.5.zip
|
||||
@@ -0,0 +1,36 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# DOTA 1.0 dataset https://captain-whu.github.io/DOTA/index.html for object detection in aerial images by Wuhan University
|
||||
# Documentation: https://docs.ultralytics.com/datasets/obb/dota-v2/
|
||||
# Example usage: yolo train model=yolov8n-obb.pt data=DOTAv1.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── dota1 ← downloads here (2GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: DOTAv1 # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 1411 images
|
||||
val: images/val # val images (relative to 'path') 458 images
|
||||
test: images/test # test images (optional) 937 images
|
||||
|
||||
# Classes for DOTA 1.0
|
||||
names:
|
||||
0: plane
|
||||
1: ship
|
||||
2: storage tank
|
||||
3: baseball diamond
|
||||
4: tennis court
|
||||
5: basketball court
|
||||
6: ground track field
|
||||
7: harbor
|
||||
8: bridge
|
||||
9: large vehicle
|
||||
10: small vehicle
|
||||
11: helicopter
|
||||
12: roundabout
|
||||
13: soccer ball field
|
||||
14: swimming pool
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/DOTAv1.zip
|
||||
@@ -0,0 +1,68 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Global Wheat 2020 dataset https://www.global-wheat.com/ by University of Saskatchewan
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/globalwheat2020/
|
||||
# Example usage: yolo train data=GlobalWheat2020.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── GlobalWheat2020 ← downloads here (7.0 GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: GlobalWheat2020 # dataset root dir
|
||||
train: # train images (relative to 'path') 3422 images
|
||||
- images/arvalis_1
|
||||
- images/arvalis_2
|
||||
- images/arvalis_3
|
||||
- images/ethz_1
|
||||
- images/rres_1
|
||||
- images/inrae_1
|
||||
- images/usask_1
|
||||
val: # val images (relative to 'path') 748 images (WARNING: train set contains ethz_1)
|
||||
- images/ethz_1
|
||||
test: # test images (optional) 1276 images
|
||||
- images/utokyo_1
|
||||
- images/utokyo_2
|
||||
- images/nau_1
|
||||
- images/uq_1
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: wheat_head
|
||||
|
||||
# Download script/URL (optional) ---------------------------------------------------------------------------------------
|
||||
download: |
|
||||
from pathlib import Path
|
||||
|
||||
from ultralytics.utils.downloads import download
|
||||
|
||||
# Download
|
||||
dir = Path(yaml["path"]) # dataset root dir
|
||||
urls = [
|
||||
"https://zenodo.org/record/4298502/files/global-wheat-codalab-official.zip",
|
||||
"https://github.com/ultralytics/assets/releases/download/v0.0.0/GlobalWheat2020_labels.zip",
|
||||
]
|
||||
download(urls, dir=dir)
|
||||
|
||||
# Make Directories
|
||||
for p in "annotations", "images", "labels":
|
||||
(dir / p).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Move
|
||||
for p in (
|
||||
"arvalis_1",
|
||||
"arvalis_2",
|
||||
"arvalis_3",
|
||||
"ethz_1",
|
||||
"rres_1",
|
||||
"inrae_1",
|
||||
"usask_1",
|
||||
"utokyo_1",
|
||||
"utokyo_2",
|
||||
"nau_1",
|
||||
"uq_1",
|
||||
):
|
||||
(dir / "global-wheat-codalab-official" / p).rename(dir / "images" / p) # move to /images
|
||||
f = (dir / "global-wheat-codalab-official" / p).with_suffix(".json") # json file
|
||||
if f.exists():
|
||||
f.rename((dir / "annotations" / p).with_suffix(".json")) # move to /annotations
|
||||
@@ -0,0 +1,32 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# HomeObjects-3K dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/homeobjects-3k/
|
||||
# Example usage: yolo train data=HomeObjects-3K.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── homeobjects-3K ← downloads here (390 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: homeobjects-3K # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 2285 images
|
||||
val: images/val # val images (relative to 'path') 404 images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: bed
|
||||
1: sofa
|
||||
2: chair
|
||||
3: table
|
||||
4: lamp
|
||||
5: tv
|
||||
6: laptop
|
||||
7: wardrobe
|
||||
8: window
|
||||
9: door
|
||||
10: potted plant
|
||||
11: photo frame
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/homeobjects-3K.zip
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,443 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Objects365 dataset https://www.objects365.org/ by Megvii
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/objects365/
|
||||
# Example usage: yolo train data=Objects365.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── Objects365 ← downloads here (712 GB = 367G data + 345G zips)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: Objects365 # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 1742289 images
|
||||
val: images/val # val images (relative to 'path') 80000 images
|
||||
test: # test images (optional)
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: Person
|
||||
1: Sneakers
|
||||
2: Chair
|
||||
3: Other Shoes
|
||||
4: Hat
|
||||
5: Car
|
||||
6: Lamp
|
||||
7: Glasses
|
||||
8: Bottle
|
||||
9: Desk
|
||||
10: Cup
|
||||
11: Street Lights
|
||||
12: Cabinet/shelf
|
||||
13: Handbag/Satchel
|
||||
14: Bracelet
|
||||
15: Plate
|
||||
16: Picture/Frame
|
||||
17: Helmet
|
||||
18: Book
|
||||
19: Gloves
|
||||
20: Storage box
|
||||
21: Boat
|
||||
22: Leather Shoes
|
||||
23: Flower
|
||||
24: Bench
|
||||
25: Potted Plant
|
||||
26: Bowl/Basin
|
||||
27: Flag
|
||||
28: Pillow
|
||||
29: Boots
|
||||
30: Vase
|
||||
31: Microphone
|
||||
32: Necklace
|
||||
33: Ring
|
||||
34: SUV
|
||||
35: Wine Glass
|
||||
36: Belt
|
||||
37: Monitor/TV
|
||||
38: Backpack
|
||||
39: Umbrella
|
||||
40: Traffic Light
|
||||
41: Speaker
|
||||
42: Watch
|
||||
43: Tie
|
||||
44: Trash bin Can
|
||||
45: Slippers
|
||||
46: Bicycle
|
||||
47: Stool
|
||||
48: Barrel/bucket
|
||||
49: Van
|
||||
50: Couch
|
||||
51: Sandals
|
||||
52: Basket
|
||||
53: Drum
|
||||
54: Pen/Pencil
|
||||
55: Bus
|
||||
56: Wild Bird
|
||||
57: High Heels
|
||||
58: Motorcycle
|
||||
59: Guitar
|
||||
60: Carpet
|
||||
61: Cell Phone
|
||||
62: Bread
|
||||
63: Camera
|
||||
64: Canned
|
||||
65: Truck
|
||||
66: Traffic cone
|
||||
67: Cymbal
|
||||
68: Lifesaver
|
||||
69: Towel
|
||||
70: Stuffed Toy
|
||||
71: Candle
|
||||
72: Sailboat
|
||||
73: Laptop
|
||||
74: Awning
|
||||
75: Bed
|
||||
76: Faucet
|
||||
77: Tent
|
||||
78: Horse
|
||||
79: Mirror
|
||||
80: Power outlet
|
||||
81: Sink
|
||||
82: Apple
|
||||
83: Air Conditioner
|
||||
84: Knife
|
||||
85: Hockey Stick
|
||||
86: Paddle
|
||||
87: Pickup Truck
|
||||
88: Fork
|
||||
89: Traffic Sign
|
||||
90: Balloon
|
||||
91: Tripod
|
||||
92: Dog
|
||||
93: Spoon
|
||||
94: Clock
|
||||
95: Pot
|
||||
96: Cow
|
||||
97: Cake
|
||||
98: Dining Table
|
||||
99: Sheep
|
||||
100: Hanger
|
||||
101: Blackboard/Whiteboard
|
||||
102: Napkin
|
||||
103: Other Fish
|
||||
104: Orange/Tangerine
|
||||
105: Toiletry
|
||||
106: Keyboard
|
||||
107: Tomato
|
||||
108: Lantern
|
||||
109: Machinery Vehicle
|
||||
110: Fan
|
||||
111: Green Vegetables
|
||||
112: Banana
|
||||
113: Baseball Glove
|
||||
114: Airplane
|
||||
115: Mouse
|
||||
116: Train
|
||||
117: Pumpkin
|
||||
118: Soccer
|
||||
119: Skiboard
|
||||
120: Luggage
|
||||
121: Nightstand
|
||||
122: Tea pot
|
||||
123: Telephone
|
||||
124: Trolley
|
||||
125: Head Phone
|
||||
126: Sports Car
|
||||
127: Stop Sign
|
||||
128: Dessert
|
||||
129: Scooter
|
||||
130: Stroller
|
||||
131: Crane
|
||||
132: Remote
|
||||
133: Refrigerator
|
||||
134: Oven
|
||||
135: Lemon
|
||||
136: Duck
|
||||
137: Baseball Bat
|
||||
138: Surveillance Camera
|
||||
139: Cat
|
||||
140: Jug
|
||||
141: Broccoli
|
||||
142: Piano
|
||||
143: Pizza
|
||||
144: Elephant
|
||||
145: Skateboard
|
||||
146: Surfboard
|
||||
147: Gun
|
||||
148: Skating and Skiing shoes
|
||||
149: Gas stove
|
||||
150: Donut
|
||||
151: Bow Tie
|
||||
152: Carrot
|
||||
153: Toilet
|
||||
154: Kite
|
||||
155: Strawberry
|
||||
156: Other Balls
|
||||
157: Shovel
|
||||
158: Pepper
|
||||
159: Computer Box
|
||||
160: Toilet Paper
|
||||
161: Cleaning Products
|
||||
162: Chopsticks
|
||||
163: Microwave
|
||||
164: Pigeon
|
||||
165: Baseball
|
||||
166: Cutting/chopping Board
|
||||
167: Coffee Table
|
||||
168: Side Table
|
||||
169: Scissors
|
||||
170: Marker
|
||||
171: Pie
|
||||
172: Ladder
|
||||
173: Snowboard
|
||||
174: Cookies
|
||||
175: Radiator
|
||||
176: Fire Hydrant
|
||||
177: Basketball
|
||||
178: Zebra
|
||||
179: Grape
|
||||
180: Giraffe
|
||||
181: Potato
|
||||
182: Sausage
|
||||
183: Tricycle
|
||||
184: Violin
|
||||
185: Egg
|
||||
186: Fire Extinguisher
|
||||
187: Candy
|
||||
188: Fire Truck
|
||||
189: Billiards
|
||||
190: Converter
|
||||
191: Bathtub
|
||||
192: Wheelchair
|
||||
193: Golf Club
|
||||
194: Briefcase
|
||||
195: Cucumber
|
||||
196: Cigar/Cigarette
|
||||
197: Paint Brush
|
||||
198: Pear
|
||||
199: Heavy Truck
|
||||
200: Hamburger
|
||||
201: Extractor
|
||||
202: Extension Cord
|
||||
203: Tong
|
||||
204: Tennis Racket
|
||||
205: Folder
|
||||
206: American Football
|
||||
207: earphone
|
||||
208: Mask
|
||||
209: Kettle
|
||||
210: Tennis
|
||||
211: Ship
|
||||
212: Swing
|
||||
213: Coffee Machine
|
||||
214: Slide
|
||||
215: Carriage
|
||||
216: Onion
|
||||
217: Green beans
|
||||
218: Projector
|
||||
219: Frisbee
|
||||
220: Washing Machine/Drying Machine
|
||||
221: Chicken
|
||||
222: Printer
|
||||
223: Watermelon
|
||||
224: Saxophone
|
||||
225: Tissue
|
||||
226: Toothbrush
|
||||
227: Ice cream
|
||||
228: Hot-air balloon
|
||||
229: Cello
|
||||
230: French Fries
|
||||
231: Scale
|
||||
232: Trophy
|
||||
233: Cabbage
|
||||
234: Hot dog
|
||||
235: Blender
|
||||
236: Peach
|
||||
237: Rice
|
||||
238: Wallet/Purse
|
||||
239: Volleyball
|
||||
240: Deer
|
||||
241: Goose
|
||||
242: Tape
|
||||
243: Tablet
|
||||
244: Cosmetics
|
||||
245: Trumpet
|
||||
246: Pineapple
|
||||
247: Golf Ball
|
||||
248: Ambulance
|
||||
249: Parking meter
|
||||
250: Mango
|
||||
251: Key
|
||||
252: Hurdle
|
||||
253: Fishing Rod
|
||||
254: Medal
|
||||
255: Flute
|
||||
256: Brush
|
||||
257: Penguin
|
||||
258: Megaphone
|
||||
259: Corn
|
||||
260: Lettuce
|
||||
261: Garlic
|
||||
262: Swan
|
||||
263: Helicopter
|
||||
264: Green Onion
|
||||
265: Sandwich
|
||||
266: Nuts
|
||||
267: Speed Limit Sign
|
||||
268: Induction Cooker
|
||||
269: Broom
|
||||
270: Trombone
|
||||
271: Plum
|
||||
272: Rickshaw
|
||||
273: Goldfish
|
||||
274: Kiwi fruit
|
||||
275: Router/modem
|
||||
276: Poker Card
|
||||
277: Toaster
|
||||
278: Shrimp
|
||||
279: Sushi
|
||||
280: Cheese
|
||||
281: Notepaper
|
||||
282: Cherry
|
||||
283: Pliers
|
||||
284: CD
|
||||
285: Pasta
|
||||
286: Hammer
|
||||
287: Cue
|
||||
288: Avocado
|
||||
289: Hami melon
|
||||
290: Flask
|
||||
291: Mushroom
|
||||
292: Screwdriver
|
||||
293: Soap
|
||||
294: Recorder
|
||||
295: Bear
|
||||
296: Eggplant
|
||||
297: Board Eraser
|
||||
298: Coconut
|
||||
299: Tape Measure/Ruler
|
||||
300: Pig
|
||||
301: Showerhead
|
||||
302: Globe
|
||||
303: Chips
|
||||
304: Steak
|
||||
305: Crosswalk Sign
|
||||
306: Stapler
|
||||
307: Camel
|
||||
308: Formula 1
|
||||
309: Pomegranate
|
||||
310: Dishwasher
|
||||
311: Crab
|
||||
312: Hoverboard
|
||||
313: Meatball
|
||||
314: Rice Cooker
|
||||
315: Tuba
|
||||
316: Calculator
|
||||
317: Papaya
|
||||
318: Antelope
|
||||
319: Parrot
|
||||
320: Seal
|
||||
321: Butterfly
|
||||
322: Dumbbell
|
||||
323: Donkey
|
||||
324: Lion
|
||||
325: Urinal
|
||||
326: Dolphin
|
||||
327: Electric Drill
|
||||
328: Hair Dryer
|
||||
329: Egg tart
|
||||
330: Jellyfish
|
||||
331: Treadmill
|
||||
332: Lighter
|
||||
333: Grapefruit
|
||||
334: Game board
|
||||
335: Mop
|
||||
336: Radish
|
||||
337: Baozi
|
||||
338: Target
|
||||
339: French
|
||||
340: Spring Rolls
|
||||
341: Monkey
|
||||
342: Rabbit
|
||||
343: Pencil Case
|
||||
344: Yak
|
||||
345: Red Cabbage
|
||||
346: Binoculars
|
||||
347: Asparagus
|
||||
348: Barbell
|
||||
349: Scallop
|
||||
350: Noddles
|
||||
351: Comb
|
||||
352: Dumpling
|
||||
353: Oyster
|
||||
354: Table Tennis paddle
|
||||
355: Cosmetics Brush/Eyeliner Pencil
|
||||
356: Chainsaw
|
||||
357: Eraser
|
||||
358: Lobster
|
||||
359: Durian
|
||||
360: Okra
|
||||
361: Lipstick
|
||||
362: Cosmetics Mirror
|
||||
363: Curling
|
||||
364: Table Tennis
|
||||
|
||||
# Download script/URL (optional) ---------------------------------------------------------------------------------------
|
||||
download: |
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
from ultralytics.utils.checks import check_requirements
|
||||
from ultralytics.utils.downloads import download
|
||||
from ultralytics.utils.ops import xyxy2xywhn
|
||||
|
||||
check_requirements("faster-coco-eval")
|
||||
from faster_coco_eval import COCO
|
||||
|
||||
# Make Directories
|
||||
dir = Path(yaml["path"]) # dataset root dir
|
||||
for p in "images", "labels":
|
||||
(dir / p).mkdir(parents=True, exist_ok=True)
|
||||
for q in "train", "val":
|
||||
(dir / p / q).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Train, Val Splits
|
||||
for split, patches in [("train", 50 + 1), ("val", 43 + 1)]:
|
||||
print(f"Processing {split} in {patches} patches ...")
|
||||
images, labels = dir / "images" / split, dir / "labels" / split
|
||||
|
||||
# Download
|
||||
url = f"https://dorc.ks3-cn-beijing.ksyun.com/data-set/2020Objects365%E6%95%B0%E6%8D%AE%E9%9B%86/{split}/"
|
||||
if split == "train":
|
||||
download([f"{url}zhiyuan_objv2_{split}.tar.gz"], dir=dir) # annotations json
|
||||
download([f"{url}patch{i}.tar.gz" for i in range(patches)], dir=images, curl=True, threads=8)
|
||||
elif split == "val":
|
||||
download([f"{url}zhiyuan_objv2_{split}.json"], dir=dir) # annotations json
|
||||
download([f"{url}images/v1/patch{i}.tar.gz" for i in range(15 + 1)], dir=images, curl=True, threads=8)
|
||||
download([f"{url}images/v2/patch{i}.tar.gz" for i in range(16, patches)], dir=images, curl=True, threads=8)
|
||||
|
||||
# Move
|
||||
for f in tqdm(images.rglob("*.jpg"), desc=f"Moving {split} images"):
|
||||
f.rename(images / f.name) # move to /images/{split}
|
||||
|
||||
# Labels
|
||||
coco = COCO(dir / f"zhiyuan_objv2_{split}.json")
|
||||
names = [x["name"] for x in coco.loadCats(coco.getCatIds())]
|
||||
for cid, cat in enumerate(names):
|
||||
catIds = coco.getCatIds(catNms=[cat])
|
||||
imgIds = coco.getImgIds(catIds=catIds)
|
||||
for im in tqdm(coco.loadImgs(imgIds), desc=f"Class {cid + 1}/{len(names)} {cat}"):
|
||||
width, height = im["width"], im["height"]
|
||||
path = Path(im["file_name"]) # image filename
|
||||
try:
|
||||
with open(labels / path.with_suffix(".txt").name, "a", encoding="utf-8") as file:
|
||||
annIds = coco.getAnnIds(imgIds=im["id"], catIds=catIds, iscrowd=None)
|
||||
for a in coco.loadAnns(annIds):
|
||||
x, y, w, h = a["bbox"] # bounding box in xywh (xy top-left corner)
|
||||
xyxy = np.array([x, y, x + w, y + h])[None] # pixels(1,4)
|
||||
x, y, w, h = xyxy2xywhn(xyxy, w=width, h=height, clip=True)[0] # normalized and clipped
|
||||
file.write(f"{cid} {x:.5f} {y:.5f} {w:.5f} {h:.5f}\n")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -0,0 +1,58 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# SKU-110K retail items dataset https://github.com/eg4000/SKU110K_CVPR19 by Trax Retail
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/sku-110k/
|
||||
# Example usage: yolo train data=SKU-110K.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── SKU-110K ← downloads here (13.6 GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: SKU-110K # dataset root dir
|
||||
train: train.txt # train images (relative to 'path') 8219 images
|
||||
val: val.txt # val images (relative to 'path') 588 images
|
||||
test: test.txt # test images (optional) 2936 images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: object
|
||||
|
||||
# Download script/URL (optional) ---------------------------------------------------------------------------------------
|
||||
download: |
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
|
||||
from ultralytics.utils.downloads import download
|
||||
from ultralytics.utils.ops import xyxy2xywh
|
||||
|
||||
# Download
|
||||
dir = Path(yaml["path"]) # dataset root dir
|
||||
parent = Path(dir.parent) # download dir
|
||||
urls = ["http://trax-geometry.s3.amazonaws.com/cvpr_challenge/SKU110K_fixed.tar.gz"]
|
||||
download(urls, dir=parent)
|
||||
|
||||
# Rename directories
|
||||
if dir.exists():
|
||||
shutil.rmtree(dir)
|
||||
(parent / "SKU110K_fixed").rename(dir) # rename dir
|
||||
(dir / "labels").mkdir(parents=True, exist_ok=True) # create labels dir
|
||||
|
||||
# Convert labels
|
||||
names = "image", "x1", "y1", "x2", "y2", "class", "image_width", "image_height" # column names
|
||||
for d in "annotations_train.csv", "annotations_val.csv", "annotations_test.csv":
|
||||
x = pd.read_csv(dir / "annotations" / d, names=names).values # annotations
|
||||
images, unique_images = x[:, 0], np.unique(x[:, 0])
|
||||
with open((dir / d).with_suffix(".txt").__str__().replace("annotations_", ""), "w", encoding="utf-8") as f:
|
||||
f.writelines(f"./images/{s}\n" for s in unique_images)
|
||||
for im in tqdm(unique_images, desc=f"Converting {dir / d}"):
|
||||
cls = 0 # single-class dataset
|
||||
with open((dir / "labels" / im).with_suffix(".txt"), "a", encoding="utf-8") as f:
|
||||
for r in x[images == im]:
|
||||
w, h = r[6], r[7] # image width, height
|
||||
xywh = xyxy2xywh(np.array([[r[1] / w, r[2] / h, r[3] / w, r[4] / h]]))[0] # instance
|
||||
f.write(f"{cls} {xywh[0]:.5f} {xywh[1]:.5f} {xywh[2]:.5f} {xywh[3]:.5f}\n") # write label
|
||||
@@ -0,0 +1,106 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# PASCAL VOC dataset http://host.robots.ox.ac.uk/pascal/VOC by University of Oxford
|
||||
# Documentation: # Documentation: https://docs.ultralytics.com/datasets/detect/voc/
|
||||
# Example usage: yolo train data=VOC.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── VOC ← downloads here (2.8 GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: VOC
|
||||
train: # train images (relative to 'path') 16551 images
|
||||
- images/train2012
|
||||
- images/train2007
|
||||
- images/val2012
|
||||
- images/val2007
|
||||
val: # val images (relative to 'path') 4952 images
|
||||
- images/test2007
|
||||
test: # test images (optional)
|
||||
- images/test2007
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: aeroplane
|
||||
1: bicycle
|
||||
2: bird
|
||||
3: boat
|
||||
4: bottle
|
||||
5: bus
|
||||
6: car
|
||||
7: cat
|
||||
8: chair
|
||||
9: cow
|
||||
10: diningtable
|
||||
11: dog
|
||||
12: horse
|
||||
13: motorbike
|
||||
14: person
|
||||
15: pottedplant
|
||||
16: sheep
|
||||
17: sofa
|
||||
18: train
|
||||
19: tvmonitor
|
||||
|
||||
# Download script/URL (optional) ---------------------------------------------------------------------------------------
|
||||
download: |
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from ultralytics.utils.downloads import download
|
||||
|
||||
|
||||
def convert_label(path, lb_path, year, image_id):
|
||||
"""Converts XML annotations from VOC format to YOLO format by extracting bounding boxes and class IDs."""
|
||||
|
||||
def convert_box(size, box):
|
||||
dw, dh = 1.0 / size[0], 1.0 / size[1]
|
||||
x, y, w, h = (box[0] + box[1]) / 2.0 - 1, (box[2] + box[3]) / 2.0 - 1, box[1] - box[0], box[3] - box[2]
|
||||
return x * dw, y * dh, w * dw, h * dh
|
||||
|
||||
in_file = open(path / f"VOC{year}/Annotations/{image_id}.xml")
|
||||
out_file = open(lb_path, "w")
|
||||
tree = ET.parse(in_file)
|
||||
root = tree.getroot()
|
||||
size = root.find("size")
|
||||
w = int(size.find("width").text)
|
||||
h = int(size.find("height").text)
|
||||
|
||||
names = list(yaml["names"].values()) # names list
|
||||
for obj in root.iter("object"):
|
||||
cls = obj.find("name").text
|
||||
if cls in names and int(obj.find("difficult").text) != 1:
|
||||
xmlbox = obj.find("bndbox")
|
||||
bb = convert_box((w, h), [float(xmlbox.find(x).text) for x in ("xmin", "xmax", "ymin", "ymax")])
|
||||
cls_id = names.index(cls) # class id
|
||||
out_file.write(" ".join(str(a) for a in (cls_id, *bb)) + "\n")
|
||||
|
||||
|
||||
# Download
|
||||
dir = Path(yaml["path"]) # dataset root dir
|
||||
url = "https://github.com/ultralytics/assets/releases/download/v0.0.0/"
|
||||
urls = [
|
||||
f"{url}VOCtrainval_06-Nov-2007.zip", # 446MB, 5012 images
|
||||
f"{url}VOCtest_06-Nov-2007.zip", # 438MB, 4953 images
|
||||
f"{url}VOCtrainval_11-May-2012.zip", # 1.95GB, 17126 images
|
||||
]
|
||||
download(urls, dir=dir / "images", curl=True, threads=3, exist_ok=True) # download and unzip over existing (required)
|
||||
|
||||
# Convert
|
||||
path = dir / "images/VOCdevkit"
|
||||
for year, image_set in ("2012", "train"), ("2012", "val"), ("2007", "train"), ("2007", "val"), ("2007", "test"):
|
||||
imgs_path = dir / "images" / f"{image_set}{year}"
|
||||
lbs_path = dir / "labels" / f"{image_set}{year}"
|
||||
imgs_path.mkdir(exist_ok=True, parents=True)
|
||||
lbs_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
with open(path / f"VOC{year}/ImageSets/Main/{image_set}.txt") as f:
|
||||
image_ids = f.read().strip().split()
|
||||
for id in tqdm(image_ids, desc=f"{image_set}{year}"):
|
||||
f = path / f"VOC{year}/JPEGImages/{id}.jpg" # old img path
|
||||
lb_path = (lbs_path / f.name).with_suffix(".txt") # new label path
|
||||
f.rename(imgs_path / f.name) # move image
|
||||
convert_label(path, lb_path, year, id) # convert labels to YOLO format
|
||||
@@ -0,0 +1,87 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# VisDrone2019-DET dataset https://github.com/VisDrone/VisDrone-Dataset by Tianjin University
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/visdrone/
|
||||
# Example usage: yolo train data=VisDrone.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── VisDrone ← downloads here (2.3 GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: VisDrone # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 6471 images
|
||||
val: images/val # val images (relative to 'path') 548 images
|
||||
test: images/test # test-dev images (optional) 1610 images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: pedestrian
|
||||
1: people
|
||||
2: bicycle
|
||||
3: car
|
||||
4: van
|
||||
5: truck
|
||||
6: tricycle
|
||||
7: awning-tricycle
|
||||
8: bus
|
||||
9: motor
|
||||
|
||||
# Download script/URL (optional) ---------------------------------------------------------------------------------------
|
||||
download: |
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from ultralytics.utils.downloads import download
|
||||
|
||||
|
||||
def visdrone2yolo(dir, split, source_name=None):
|
||||
"""Convert VisDrone annotations to YOLO format with images/{split} and labels/{split} structure."""
|
||||
from PIL import Image
|
||||
from tqdm import tqdm
|
||||
|
||||
source_dir = dir / (source_name or f"VisDrone2019-DET-{split}")
|
||||
images_dir = dir / "images" / split
|
||||
labels_dir = dir / "labels" / split
|
||||
labels_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Move images to new structure
|
||||
if (source_images_dir := source_dir / "images").exists():
|
||||
images_dir.mkdir(parents=True, exist_ok=True)
|
||||
for img in source_images_dir.glob("*.jpg"):
|
||||
img.rename(images_dir / img.name)
|
||||
|
||||
for f in tqdm((source_dir / "annotations").glob("*.txt"), desc=f"Converting {split}"):
|
||||
img_size = Image.open(images_dir / f.with_suffix(".jpg").name).size
|
||||
dw, dh = 1.0 / img_size[0], 1.0 / img_size[1]
|
||||
lines = []
|
||||
|
||||
with open(f, encoding="utf-8") as file:
|
||||
for row in [x.split(",") for x in file.read().strip().splitlines()]:
|
||||
if row[4] != "0": # Skip ignored regions
|
||||
x, y, w, h = map(int, row[:4])
|
||||
cls = int(row[5]) - 1
|
||||
# Convert to YOLO format
|
||||
x_center, y_center = (x + w / 2) * dw, (y + h / 2) * dh
|
||||
w_norm, h_norm = w * dw, h * dh
|
||||
lines.append(f"{cls} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n")
|
||||
|
||||
(labels_dir / f.name).write_text("".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
# Download (ignores test-challenge split)
|
||||
dir = Path(yaml["path"]) # dataset root dir
|
||||
urls = [
|
||||
"https://github.com/ultralytics/assets/releases/download/v0.0.0/VisDrone2019-DET-train.zip",
|
||||
"https://github.com/ultralytics/assets/releases/download/v0.0.0/VisDrone2019-DET-val.zip",
|
||||
"https://github.com/ultralytics/assets/releases/download/v0.0.0/VisDrone2019-DET-test-dev.zip",
|
||||
# "https://github.com/ultralytics/assets/releases/download/v0.0.0/VisDrone2019-DET-test-challenge.zip",
|
||||
]
|
||||
download(urls, dir=dir, curl=True, threads=4)
|
||||
|
||||
# Convert
|
||||
splits = {"VisDrone2019-DET-train": "train", "VisDrone2019-DET-val": "val", "VisDrone2019-DET-test-dev": "test"}
|
||||
for folder, split in splits.items():
|
||||
visdrone2yolo(dir, split, folder) # convert VisDrone annotations to YOLO labels
|
||||
shutil.rmtree(dir / folder) # cleanup original directory
|
||||
@@ -0,0 +1,25 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# African-wildlife dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/african-wildlife/
|
||||
# Example usage: yolo train data=african-wildlife.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── african-wildlife ← downloads here (100 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: african-wildlife # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 1052 images
|
||||
val: images/val # val images (relative to 'path') 225 images
|
||||
test: images/test # test images (relative to 'path') 227 images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: buffalo
|
||||
1: elephant
|
||||
2: rhino
|
||||
3: zebra
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/african-wildlife.zip
|
||||
@@ -0,0 +1,22 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Brain-tumor dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/brain-tumor/
|
||||
# Example usage: yolo train data=brain-tumor.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── brain-tumor ← downloads here (4.21 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: brain-tumor # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 893 images
|
||||
val: images/val # val images (relative to 'path') 223 images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: negative
|
||||
1: positive
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/brain-tumor.zip
|
||||
@@ -0,0 +1,44 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Carparts-seg dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/segment/carparts-seg/
|
||||
# Example usage: yolo train data=carparts-seg.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── carparts-seg ← downloads here (133 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: carparts-seg # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 3516 images
|
||||
val: images/val # val images (relative to 'path') 276 images
|
||||
test: images/test # test images (relative to 'path') 401 images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: back_bumper
|
||||
1: back_door
|
||||
2: back_glass
|
||||
3: back_left_door
|
||||
4: back_left_light
|
||||
5: back_light
|
||||
6: back_right_door
|
||||
7: back_right_light
|
||||
8: front_bumper
|
||||
9: front_door
|
||||
10: front_glass
|
||||
11: front_left_door
|
||||
12: front_left_light
|
||||
13: front_light
|
||||
14: front_right_door
|
||||
15: front_right_light
|
||||
16: hood
|
||||
17: left_mirror
|
||||
18: object
|
||||
19: right_mirror
|
||||
20: tailgate
|
||||
21: trunk
|
||||
22: wheel
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/carparts-seg.zip
|
||||
@@ -0,0 +1,42 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# COCO 2017 Keypoints dataset https://cocodataset.org by Microsoft
|
||||
# Documentation: https://docs.ultralytics.com/datasets/pose/coco/
|
||||
# Example usage: yolo train data=coco-pose.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── coco-pose ← downloads here (20.1 GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: coco-pose # dataset root dir
|
||||
train: train2017.txt # train images (relative to 'path') 56599 images
|
||||
val: val2017.txt # val images (relative to 'path') 2346 images
|
||||
test: test-dev2017.txt # 20288 of 40670 images, submit to https://codalab.lisn.upsaclay.fr/competitions/7403
|
||||
|
||||
# Keypoints
|
||||
kpt_shape: [17, 3] # number of keypoints, number of dims (2 for x,y or 3 for x,y,visible)
|
||||
flip_idx: [0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15]
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: person
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: |
|
||||
from pathlib import Path
|
||||
|
||||
from ultralytics.utils.downloads import download
|
||||
|
||||
# Download labels
|
||||
dir = Path(yaml["path"]) # dataset root dir
|
||||
url = "https://github.com/ultralytics/assets/releases/download/v0.0.0/"
|
||||
urls = [f"{url}coco2017labels-pose.zip"]
|
||||
download(urls, dir=dir.parent)
|
||||
# Download data
|
||||
urls = [
|
||||
"http://images.cocodataset.org/zips/train2017.zip", # 19G, 118k images
|
||||
"http://images.cocodataset.org/zips/val2017.zip", # 1G, 5k images
|
||||
"http://images.cocodataset.org/zips/test2017.zip", # 7G, 41k images (optional)
|
||||
]
|
||||
download(urls, dir=dir / "images", threads=3)
|
||||
@@ -0,0 +1,118 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# COCO 2017 dataset https://cocodataset.org by Microsoft
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/coco/
|
||||
# Example usage: yolo train data=coco.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── coco ← downloads here (20.1 GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: coco # dataset root dir
|
||||
train: train2017.txt # train images (relative to 'path') 118287 images
|
||||
val: val2017.txt # val images (relative to 'path') 5000 images
|
||||
test: test-dev2017.txt # 20288 of 40670 images, submit to https://competitions.codalab.org/competitions/20794
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: person
|
||||
1: bicycle
|
||||
2: car
|
||||
3: motorcycle
|
||||
4: airplane
|
||||
5: bus
|
||||
6: train
|
||||
7: truck
|
||||
8: boat
|
||||
9: traffic light
|
||||
10: fire hydrant
|
||||
11: stop sign
|
||||
12: parking meter
|
||||
13: bench
|
||||
14: bird
|
||||
15: cat
|
||||
16: dog
|
||||
17: horse
|
||||
18: sheep
|
||||
19: cow
|
||||
20: elephant
|
||||
21: bear
|
||||
22: zebra
|
||||
23: giraffe
|
||||
24: backpack
|
||||
25: umbrella
|
||||
26: handbag
|
||||
27: tie
|
||||
28: suitcase
|
||||
29: frisbee
|
||||
30: skis
|
||||
31: snowboard
|
||||
32: sports ball
|
||||
33: kite
|
||||
34: baseball bat
|
||||
35: baseball glove
|
||||
36: skateboard
|
||||
37: surfboard
|
||||
38: tennis racket
|
||||
39: bottle
|
||||
40: wine glass
|
||||
41: cup
|
||||
42: fork
|
||||
43: knife
|
||||
44: spoon
|
||||
45: bowl
|
||||
46: banana
|
||||
47: apple
|
||||
48: sandwich
|
||||
49: orange
|
||||
50: broccoli
|
||||
51: carrot
|
||||
52: hot dog
|
||||
53: pizza
|
||||
54: donut
|
||||
55: cake
|
||||
56: chair
|
||||
57: couch
|
||||
58: potted plant
|
||||
59: bed
|
||||
60: dining table
|
||||
61: toilet
|
||||
62: tv
|
||||
63: laptop
|
||||
64: mouse
|
||||
65: remote
|
||||
66: keyboard
|
||||
67: cell phone
|
||||
68: microwave
|
||||
69: oven
|
||||
70: toaster
|
||||
71: sink
|
||||
72: refrigerator
|
||||
73: book
|
||||
74: clock
|
||||
75: vase
|
||||
76: scissors
|
||||
77: teddy bear
|
||||
78: hair drier
|
||||
79: toothbrush
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: |
|
||||
from pathlib import Path
|
||||
|
||||
from ultralytics.utils.downloads import download
|
||||
|
||||
# Download labels
|
||||
segments = True # segment or box labels
|
||||
dir = Path(yaml["path"]) # dataset root dir
|
||||
url = "https://github.com/ultralytics/assets/releases/download/v0.0.0/"
|
||||
urls = [url + ("coco2017labels-segments.zip" if segments else "coco2017labels.zip")] # labels
|
||||
download(urls, dir=dir.parent)
|
||||
# Download data
|
||||
urls = [
|
||||
"http://images.cocodataset.org/zips/train2017.zip", # 19G, 118k images
|
||||
"http://images.cocodataset.org/zips/val2017.zip", # 1G, 5k images
|
||||
"http://images.cocodataset.org/zips/test2017.zip", # 7G, 41k images (optional)
|
||||
]
|
||||
download(urls, dir=dir / "images", threads=3)
|
||||
@@ -0,0 +1,101 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# COCO128-seg dataset https://www.kaggle.com/datasets/ultralytics/coco128 (first 128 images from COCO train2017) by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/segment/coco/
|
||||
# Example usage: yolo train data=coco128.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── coco128-seg ← downloads here (7 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: coco128-seg # dataset root dir
|
||||
train: images/train2017 # train images (relative to 'path') 128 images
|
||||
val: images/train2017 # val images (relative to 'path') 128 images
|
||||
test: # test images (optional)
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: person
|
||||
1: bicycle
|
||||
2: car
|
||||
3: motorcycle
|
||||
4: airplane
|
||||
5: bus
|
||||
6: train
|
||||
7: truck
|
||||
8: boat
|
||||
9: traffic light
|
||||
10: fire hydrant
|
||||
11: stop sign
|
||||
12: parking meter
|
||||
13: bench
|
||||
14: bird
|
||||
15: cat
|
||||
16: dog
|
||||
17: horse
|
||||
18: sheep
|
||||
19: cow
|
||||
20: elephant
|
||||
21: bear
|
||||
22: zebra
|
||||
23: giraffe
|
||||
24: backpack
|
||||
25: umbrella
|
||||
26: handbag
|
||||
27: tie
|
||||
28: suitcase
|
||||
29: frisbee
|
||||
30: skis
|
||||
31: snowboard
|
||||
32: sports ball
|
||||
33: kite
|
||||
34: baseball bat
|
||||
35: baseball glove
|
||||
36: skateboard
|
||||
37: surfboard
|
||||
38: tennis racket
|
||||
39: bottle
|
||||
40: wine glass
|
||||
41: cup
|
||||
42: fork
|
||||
43: knife
|
||||
44: spoon
|
||||
45: bowl
|
||||
46: banana
|
||||
47: apple
|
||||
48: sandwich
|
||||
49: orange
|
||||
50: broccoli
|
||||
51: carrot
|
||||
52: hot dog
|
||||
53: pizza
|
||||
54: donut
|
||||
55: cake
|
||||
56: chair
|
||||
57: couch
|
||||
58: potted plant
|
||||
59: bed
|
||||
60: dining table
|
||||
61: toilet
|
||||
62: tv
|
||||
63: laptop
|
||||
64: mouse
|
||||
65: remote
|
||||
66: keyboard
|
||||
67: cell phone
|
||||
68: microwave
|
||||
69: oven
|
||||
70: toaster
|
||||
71: sink
|
||||
72: refrigerator
|
||||
73: book
|
||||
74: clock
|
||||
75: vase
|
||||
76: scissors
|
||||
77: teddy bear
|
||||
78: hair drier
|
||||
79: toothbrush
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/coco128-seg.zip
|
||||
@@ -0,0 +1,101 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# COCO128 dataset https://www.kaggle.com/datasets/ultralytics/coco128 (first 128 images from COCO train2017) by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/coco/
|
||||
# Example usage: yolo train data=coco128.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── coco128 ← downloads here (7 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: coco128 # dataset root dir
|
||||
train: images/train2017 # train images (relative to 'path') 128 images
|
||||
val: images/train2017 # val images (relative to 'path') 128 images
|
||||
test: # test images (optional)
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: person
|
||||
1: bicycle
|
||||
2: car
|
||||
3: motorcycle
|
||||
4: airplane
|
||||
5: bus
|
||||
6: train
|
||||
7: truck
|
||||
8: boat
|
||||
9: traffic light
|
||||
10: fire hydrant
|
||||
11: stop sign
|
||||
12: parking meter
|
||||
13: bench
|
||||
14: bird
|
||||
15: cat
|
||||
16: dog
|
||||
17: horse
|
||||
18: sheep
|
||||
19: cow
|
||||
20: elephant
|
||||
21: bear
|
||||
22: zebra
|
||||
23: giraffe
|
||||
24: backpack
|
||||
25: umbrella
|
||||
26: handbag
|
||||
27: tie
|
||||
28: suitcase
|
||||
29: frisbee
|
||||
30: skis
|
||||
31: snowboard
|
||||
32: sports ball
|
||||
33: kite
|
||||
34: baseball bat
|
||||
35: baseball glove
|
||||
36: skateboard
|
||||
37: surfboard
|
||||
38: tennis racket
|
||||
39: bottle
|
||||
40: wine glass
|
||||
41: cup
|
||||
42: fork
|
||||
43: knife
|
||||
44: spoon
|
||||
45: bowl
|
||||
46: banana
|
||||
47: apple
|
||||
48: sandwich
|
||||
49: orange
|
||||
50: broccoli
|
||||
51: carrot
|
||||
52: hot dog
|
||||
53: pizza
|
||||
54: donut
|
||||
55: cake
|
||||
56: chair
|
||||
57: couch
|
||||
58: potted plant
|
||||
59: bed
|
||||
60: dining table
|
||||
61: toilet
|
||||
62: tv
|
||||
63: laptop
|
||||
64: mouse
|
||||
65: remote
|
||||
66: keyboard
|
||||
67: cell phone
|
||||
68: microwave
|
||||
69: oven
|
||||
70: toaster
|
||||
71: sink
|
||||
72: refrigerator
|
||||
73: book
|
||||
74: clock
|
||||
75: vase
|
||||
76: scissors
|
||||
77: teddy bear
|
||||
78: hair drier
|
||||
79: toothbrush
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/coco128.zip
|
||||
@@ -0,0 +1,103 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# COCO8-Grayscale dataset (first 8 images from COCO train2017) by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/coco8-grayscale/
|
||||
# Example usage: yolo train data=coco8-grayscale.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── coco8-grayscale ← downloads here (1 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: coco8-grayscale # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 4 images
|
||||
val: images/val # val images (relative to 'path') 4 images
|
||||
test: # test images (optional)
|
||||
|
||||
channels: 1
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: person
|
||||
1: bicycle
|
||||
2: car
|
||||
3: motorcycle
|
||||
4: airplane
|
||||
5: bus
|
||||
6: train
|
||||
7: truck
|
||||
8: boat
|
||||
9: traffic light
|
||||
10: fire hydrant
|
||||
11: stop sign
|
||||
12: parking meter
|
||||
13: bench
|
||||
14: bird
|
||||
15: cat
|
||||
16: dog
|
||||
17: horse
|
||||
18: sheep
|
||||
19: cow
|
||||
20: elephant
|
||||
21: bear
|
||||
22: zebra
|
||||
23: giraffe
|
||||
24: backpack
|
||||
25: umbrella
|
||||
26: handbag
|
||||
27: tie
|
||||
28: suitcase
|
||||
29: frisbee
|
||||
30: skis
|
||||
31: snowboard
|
||||
32: sports ball
|
||||
33: kite
|
||||
34: baseball bat
|
||||
35: baseball glove
|
||||
36: skateboard
|
||||
37: surfboard
|
||||
38: tennis racket
|
||||
39: bottle
|
||||
40: wine glass
|
||||
41: cup
|
||||
42: fork
|
||||
43: knife
|
||||
44: spoon
|
||||
45: bowl
|
||||
46: banana
|
||||
47: apple
|
||||
48: sandwich
|
||||
49: orange
|
||||
50: broccoli
|
||||
51: carrot
|
||||
52: hot dog
|
||||
53: pizza
|
||||
54: donut
|
||||
55: cake
|
||||
56: chair
|
||||
57: couch
|
||||
58: potted plant
|
||||
59: bed
|
||||
60: dining table
|
||||
61: toilet
|
||||
62: tv
|
||||
63: laptop
|
||||
64: mouse
|
||||
65: remote
|
||||
66: keyboard
|
||||
67: cell phone
|
||||
68: microwave
|
||||
69: oven
|
||||
70: toaster
|
||||
71: sink
|
||||
72: refrigerator
|
||||
73: book
|
||||
74: clock
|
||||
75: vase
|
||||
76: scissors
|
||||
77: teddy bear
|
||||
78: hair drier
|
||||
79: toothbrush
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/coco8-grayscale.zip
|
||||
@@ -0,0 +1,104 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# COCO8-Multispectral dataset (COCO8 images interpolated across 10 channels in the visual spectrum) by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/coco8-multispectral/
|
||||
# Example usage: yolo train data=coco8-multispectral.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── coco8-multispectral ← downloads here (20.2 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: coco8-multispectral # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 4 images
|
||||
val: images/val # val images (relative to 'path') 4 images
|
||||
test: # test images (optional)
|
||||
|
||||
# Number of multispectral image channels
|
||||
channels: 10
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: person
|
||||
1: bicycle
|
||||
2: car
|
||||
3: motorcycle
|
||||
4: airplane
|
||||
5: bus
|
||||
6: train
|
||||
7: truck
|
||||
8: boat
|
||||
9: traffic light
|
||||
10: fire hydrant
|
||||
11: stop sign
|
||||
12: parking meter
|
||||
13: bench
|
||||
14: bird
|
||||
15: cat
|
||||
16: dog
|
||||
17: horse
|
||||
18: sheep
|
||||
19: cow
|
||||
20: elephant
|
||||
21: bear
|
||||
22: zebra
|
||||
23: giraffe
|
||||
24: backpack
|
||||
25: umbrella
|
||||
26: handbag
|
||||
27: tie
|
||||
28: suitcase
|
||||
29: frisbee
|
||||
30: skis
|
||||
31: snowboard
|
||||
32: sports ball
|
||||
33: kite
|
||||
34: baseball bat
|
||||
35: baseball glove
|
||||
36: skateboard
|
||||
37: surfboard
|
||||
38: tennis racket
|
||||
39: bottle
|
||||
40: wine glass
|
||||
41: cup
|
||||
42: fork
|
||||
43: knife
|
||||
44: spoon
|
||||
45: bowl
|
||||
46: banana
|
||||
47: apple
|
||||
48: sandwich
|
||||
49: orange
|
||||
50: broccoli
|
||||
51: carrot
|
||||
52: hot dog
|
||||
53: pizza
|
||||
54: donut
|
||||
55: cake
|
||||
56: chair
|
||||
57: couch
|
||||
58: potted plant
|
||||
59: bed
|
||||
60: dining table
|
||||
61: toilet
|
||||
62: tv
|
||||
63: laptop
|
||||
64: mouse
|
||||
65: remote
|
||||
66: keyboard
|
||||
67: cell phone
|
||||
68: microwave
|
||||
69: oven
|
||||
70: toaster
|
||||
71: sink
|
||||
72: refrigerator
|
||||
73: book
|
||||
74: clock
|
||||
75: vase
|
||||
76: scissors
|
||||
77: teddy bear
|
||||
78: hair drier
|
||||
79: toothbrush
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/coco8-multispectral.zip
|
||||
@@ -0,0 +1,26 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# COCO8-pose dataset (first 8 images from COCO train2017) by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/pose/coco8-pose/
|
||||
# Example usage: yolo train data=coco8-pose.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── coco8-pose ← downloads here (1 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: coco8-pose # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 4 images
|
||||
val: images/val # val images (relative to 'path') 4 images
|
||||
test: # test images (optional)
|
||||
|
||||
# Keypoints
|
||||
kpt_shape: [17, 3] # number of keypoints, number of dims (2 for x,y or 3 for x,y,visible)
|
||||
flip_idx: [0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15]
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: person
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/coco8-pose.zip
|
||||
@@ -0,0 +1,101 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# COCO8-seg dataset (first 8 images from COCO train2017) by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/segment/coco8-seg/
|
||||
# Example usage: yolo train data=coco8-seg.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── coco8-seg ← downloads here (1 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: coco8-seg # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 4 images
|
||||
val: images/val # val images (relative to 'path') 4 images
|
||||
test: # test images (optional)
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: person
|
||||
1: bicycle
|
||||
2: car
|
||||
3: motorcycle
|
||||
4: airplane
|
||||
5: bus
|
||||
6: train
|
||||
7: truck
|
||||
8: boat
|
||||
9: traffic light
|
||||
10: fire hydrant
|
||||
11: stop sign
|
||||
12: parking meter
|
||||
13: bench
|
||||
14: bird
|
||||
15: cat
|
||||
16: dog
|
||||
17: horse
|
||||
18: sheep
|
||||
19: cow
|
||||
20: elephant
|
||||
21: bear
|
||||
22: zebra
|
||||
23: giraffe
|
||||
24: backpack
|
||||
25: umbrella
|
||||
26: handbag
|
||||
27: tie
|
||||
28: suitcase
|
||||
29: frisbee
|
||||
30: skis
|
||||
31: snowboard
|
||||
32: sports ball
|
||||
33: kite
|
||||
34: baseball bat
|
||||
35: baseball glove
|
||||
36: skateboard
|
||||
37: surfboard
|
||||
38: tennis racket
|
||||
39: bottle
|
||||
40: wine glass
|
||||
41: cup
|
||||
42: fork
|
||||
43: knife
|
||||
44: spoon
|
||||
45: bowl
|
||||
46: banana
|
||||
47: apple
|
||||
48: sandwich
|
||||
49: orange
|
||||
50: broccoli
|
||||
51: carrot
|
||||
52: hot dog
|
||||
53: pizza
|
||||
54: donut
|
||||
55: cake
|
||||
56: chair
|
||||
57: couch
|
||||
58: potted plant
|
||||
59: bed
|
||||
60: dining table
|
||||
61: toilet
|
||||
62: tv
|
||||
63: laptop
|
||||
64: mouse
|
||||
65: remote
|
||||
66: keyboard
|
||||
67: cell phone
|
||||
68: microwave
|
||||
69: oven
|
||||
70: toaster
|
||||
71: sink
|
||||
72: refrigerator
|
||||
73: book
|
||||
74: clock
|
||||
75: vase
|
||||
76: scissors
|
||||
77: teddy bear
|
||||
78: hair drier
|
||||
79: toothbrush
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/coco8-seg.zip
|
||||
@@ -0,0 +1,101 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# COCO8 dataset (first 8 images from COCO train2017) by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/coco8/
|
||||
# Example usage: yolo train data=coco8.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── coco8 ← downloads here (1 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: coco8 # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 4 images
|
||||
val: images/val # val images (relative to 'path') 4 images
|
||||
test: # test images (optional)
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: person
|
||||
1: bicycle
|
||||
2: car
|
||||
3: motorcycle
|
||||
4: airplane
|
||||
5: bus
|
||||
6: train
|
||||
7: truck
|
||||
8: boat
|
||||
9: traffic light
|
||||
10: fire hydrant
|
||||
11: stop sign
|
||||
12: parking meter
|
||||
13: bench
|
||||
14: bird
|
||||
15: cat
|
||||
16: dog
|
||||
17: horse
|
||||
18: sheep
|
||||
19: cow
|
||||
20: elephant
|
||||
21: bear
|
||||
22: zebra
|
||||
23: giraffe
|
||||
24: backpack
|
||||
25: umbrella
|
||||
26: handbag
|
||||
27: tie
|
||||
28: suitcase
|
||||
29: frisbee
|
||||
30: skis
|
||||
31: snowboard
|
||||
32: sports ball
|
||||
33: kite
|
||||
34: baseball bat
|
||||
35: baseball glove
|
||||
36: skateboard
|
||||
37: surfboard
|
||||
38: tennis racket
|
||||
39: bottle
|
||||
40: wine glass
|
||||
41: cup
|
||||
42: fork
|
||||
43: knife
|
||||
44: spoon
|
||||
45: bowl
|
||||
46: banana
|
||||
47: apple
|
||||
48: sandwich
|
||||
49: orange
|
||||
50: broccoli
|
||||
51: carrot
|
||||
52: hot dog
|
||||
53: pizza
|
||||
54: donut
|
||||
55: cake
|
||||
56: chair
|
||||
57: couch
|
||||
58: potted plant
|
||||
59: bed
|
||||
60: dining table
|
||||
61: toilet
|
||||
62: tv
|
||||
63: laptop
|
||||
64: mouse
|
||||
65: remote
|
||||
66: keyboard
|
||||
67: cell phone
|
||||
68: microwave
|
||||
69: oven
|
||||
70: toaster
|
||||
71: sink
|
||||
72: refrigerator
|
||||
73: book
|
||||
74: clock
|
||||
75: vase
|
||||
76: scissors
|
||||
77: teddy bear
|
||||
78: hair drier
|
||||
79: toothbrush
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/coco8.zip
|
||||
@@ -0,0 +1,22 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Crack-seg dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/segment/crack-seg/
|
||||
# Example usage: yolo train data=crack-seg.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── crack-seg ← downloads here (91.6 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: crack-seg # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 3717 images
|
||||
val: images/val # val images (relative to 'path') 112 images
|
||||
test: images/test # test images (relative to 'path') 200 images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: crack
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/crack-seg.zip
|
||||
@@ -0,0 +1,24 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Dogs dataset http://vision.stanford.edu/aditya86/ImageNetDogs/ by Stanford
|
||||
# Documentation: https://docs.ultralytics.com/datasets/pose/dog-pose/
|
||||
# Example usage: yolo train data=dog-pose.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── dog-pose ← downloads here (337 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: dog-pose # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 6773 images
|
||||
val: images/val # val images (relative to 'path') 1703 images
|
||||
|
||||
# Keypoints
|
||||
kpt_shape: [24, 3] # number of keypoints, number of dims (2 for x,y or 3 for x,y,visible)
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: dog
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/dog-pose.zip
|
||||
@@ -0,0 +1,38 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# DOTA8-Multispectral dataset (DOTA8 interpolated across 10 channels in the visual spectrum) by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/obb/dota8/
|
||||
# Example usage: yolo train model=yolov8n-obb.pt data=dota8-multispectral.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── dota8-multispectral ← downloads here (37.3MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: dota8-multispectral # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 4 images
|
||||
val: images/val # val images (relative to 'path') 4 images
|
||||
|
||||
# Number of multispectral image channels
|
||||
channels: 10
|
||||
|
||||
# Classes for DOTA 1.0
|
||||
names:
|
||||
0: plane
|
||||
1: ship
|
||||
2: storage tank
|
||||
3: baseball diamond
|
||||
4: tennis court
|
||||
5: basketball court
|
||||
6: ground track field
|
||||
7: harbor
|
||||
8: bridge
|
||||
9: large vehicle
|
||||
10: small vehicle
|
||||
11: helicopter
|
||||
12: roundabout
|
||||
13: soccer ball field
|
||||
14: swimming pool
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/dota8-multispectral.zip
|
||||
@@ -0,0 +1,35 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# DOTA8 dataset 8 images from split DOTAv1 dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/obb/dota8/
|
||||
# Example usage: yolo train model=yolov8n-obb.pt data=dota8.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── dota8 ← downloads here (1MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: dota8 # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 4 images
|
||||
val: images/val # val images (relative to 'path') 4 images
|
||||
|
||||
# Classes for DOTA 1.0
|
||||
names:
|
||||
0: plane
|
||||
1: ship
|
||||
2: storage tank
|
||||
3: baseball diamond
|
||||
4: tennis court
|
||||
5: basketball court
|
||||
6: ground track field
|
||||
7: harbor
|
||||
8: bridge
|
||||
9: large vehicle
|
||||
10: small vehicle
|
||||
11: helicopter
|
||||
12: roundabout
|
||||
13: soccer ball field
|
||||
14: swimming pool
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/dota8.zip
|
||||
@@ -0,0 +1,26 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Hand Keypoints dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/pose/hand-keypoints/
|
||||
# Example usage: yolo train data=hand-keypoints.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── hand-keypoints ← downloads here (369 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: hand-keypoints # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 18776 images
|
||||
val: images/val # val images (relative to 'path') 7992 images
|
||||
|
||||
# Keypoints
|
||||
kpt_shape: [21, 3] # number of keypoints, number of dims (2 for x,y or 3 for x,y,visible)
|
||||
flip_idx:
|
||||
[0, 1, 2, 4, 3, 10, 11, 12, 13, 14, 5, 6, 7, 8, 9, 15, 16, 17, 18, 19, 20]
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: hand
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/hand-keypoints.zip
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Medical-pills dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/medical-pills/
|
||||
# Example usage: yolo train data=medical-pills.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── medical-pills ← downloads here (8.19 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: medical-pills # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 92 images
|
||||
val: images/val # val images (relative to 'path') 23 images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: pill
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/medical-pills.zip
|
||||
@@ -0,0 +1,663 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Open Images v7 dataset https://storage.googleapis.com/openimages/web/index.html by Google
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/open-images-v7/
|
||||
# Example usage: yolo train data=open-images-v7.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── open-images-v7 ← downloads here (561 GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: open-images-v7 # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 1743042 images
|
||||
val: images/val # val images (relative to 'path') 41620 images
|
||||
test: # test images (optional)
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: Accordion
|
||||
1: Adhesive tape
|
||||
2: Aircraft
|
||||
3: Airplane
|
||||
4: Alarm clock
|
||||
5: Alpaca
|
||||
6: Ambulance
|
||||
7: Animal
|
||||
8: Ant
|
||||
9: Antelope
|
||||
10: Apple
|
||||
11: Armadillo
|
||||
12: Artichoke
|
||||
13: Auto part
|
||||
14: Axe
|
||||
15: Backpack
|
||||
16: Bagel
|
||||
17: Baked goods
|
||||
18: Balance beam
|
||||
19: Ball
|
||||
20: Balloon
|
||||
21: Banana
|
||||
22: Band-aid
|
||||
23: Banjo
|
||||
24: Barge
|
||||
25: Barrel
|
||||
26: Baseball bat
|
||||
27: Baseball glove
|
||||
28: Bat (Animal)
|
||||
29: Bathroom accessory
|
||||
30: Bathroom cabinet
|
||||
31: Bathtub
|
||||
32: Beaker
|
||||
33: Bear
|
||||
34: Bed
|
||||
35: Bee
|
||||
36: Beehive
|
||||
37: Beer
|
||||
38: Beetle
|
||||
39: Bell pepper
|
||||
40: Belt
|
||||
41: Bench
|
||||
42: Bicycle
|
||||
43: Bicycle helmet
|
||||
44: Bicycle wheel
|
||||
45: Bidet
|
||||
46: Billboard
|
||||
47: Billiard table
|
||||
48: Binoculars
|
||||
49: Bird
|
||||
50: Blender
|
||||
51: Blue jay
|
||||
52: Boat
|
||||
53: Bomb
|
||||
54: Book
|
||||
55: Bookcase
|
||||
56: Boot
|
||||
57: Bottle
|
||||
58: Bottle opener
|
||||
59: Bow and arrow
|
||||
60: Bowl
|
||||
61: Bowling equipment
|
||||
62: Box
|
||||
63: Boy
|
||||
64: Brassiere
|
||||
65: Bread
|
||||
66: Briefcase
|
||||
67: Broccoli
|
||||
68: Bronze sculpture
|
||||
69: Brown bear
|
||||
70: Building
|
||||
71: Bull
|
||||
72: Burrito
|
||||
73: Bus
|
||||
74: Bust
|
||||
75: Butterfly
|
||||
76: Cabbage
|
||||
77: Cabinetry
|
||||
78: Cake
|
||||
79: Cake stand
|
||||
80: Calculator
|
||||
81: Camel
|
||||
82: Camera
|
||||
83: Can opener
|
||||
84: Canary
|
||||
85: Candle
|
||||
86: Candy
|
||||
87: Cannon
|
||||
88: Canoe
|
||||
89: Cantaloupe
|
||||
90: Car
|
||||
91: Carnivore
|
||||
92: Carrot
|
||||
93: Cart
|
||||
94: Cassette deck
|
||||
95: Castle
|
||||
96: Cat
|
||||
97: Cat furniture
|
||||
98: Caterpillar
|
||||
99: Cattle
|
||||
100: Ceiling fan
|
||||
101: Cello
|
||||
102: Centipede
|
||||
103: Chainsaw
|
||||
104: Chair
|
||||
105: Cheese
|
||||
106: Cheetah
|
||||
107: Chest of drawers
|
||||
108: Chicken
|
||||
109: Chime
|
||||
110: Chisel
|
||||
111: Chopsticks
|
||||
112: Christmas tree
|
||||
113: Clock
|
||||
114: Closet
|
||||
115: Clothing
|
||||
116: Coat
|
||||
117: Cocktail
|
||||
118: Cocktail shaker
|
||||
119: Coconut
|
||||
120: Coffee
|
||||
121: Coffee cup
|
||||
122: Coffee table
|
||||
123: Coffeemaker
|
||||
124: Coin
|
||||
125: Common fig
|
||||
126: Common sunflower
|
||||
127: Computer keyboard
|
||||
128: Computer monitor
|
||||
129: Computer mouse
|
||||
130: Container
|
||||
131: Convenience store
|
||||
132: Cookie
|
||||
133: Cooking spray
|
||||
134: Corded phone
|
||||
135: Cosmetics
|
||||
136: Couch
|
||||
137: Countertop
|
||||
138: Cowboy hat
|
||||
139: Crab
|
||||
140: Cream
|
||||
141: Cricket ball
|
||||
142: Crocodile
|
||||
143: Croissant
|
||||
144: Crown
|
||||
145: Crutch
|
||||
146: Cucumber
|
||||
147: Cupboard
|
||||
148: Curtain
|
||||
149: Cutting board
|
||||
150: Dagger
|
||||
151: Dairy Product
|
||||
152: Deer
|
||||
153: Desk
|
||||
154: Dessert
|
||||
155: Diaper
|
||||
156: Dice
|
||||
157: Digital clock
|
||||
158: Dinosaur
|
||||
159: Dishwasher
|
||||
160: Dog
|
||||
161: Dog bed
|
||||
162: Doll
|
||||
163: Dolphin
|
||||
164: Door
|
||||
165: Door handle
|
||||
166: Doughnut
|
||||
167: Dragonfly
|
||||
168: Drawer
|
||||
169: Dress
|
||||
170: Drill (Tool)
|
||||
171: Drink
|
||||
172: Drinking straw
|
||||
173: Drum
|
||||
174: Duck
|
||||
175: Dumbbell
|
||||
176: Eagle
|
||||
177: Earrings
|
||||
178: Egg (Food)
|
||||
179: Elephant
|
||||
180: Envelope
|
||||
181: Eraser
|
||||
182: Face powder
|
||||
183: Facial tissue holder
|
||||
184: Falcon
|
||||
185: Fashion accessory
|
||||
186: Fast food
|
||||
187: Fax
|
||||
188: Fedora
|
||||
189: Filing cabinet
|
||||
190: Fire hydrant
|
||||
191: Fireplace
|
||||
192: Fish
|
||||
193: Flag
|
||||
194: Flashlight
|
||||
195: Flower
|
||||
196: Flowerpot
|
||||
197: Flute
|
||||
198: Flying disc
|
||||
199: Food
|
||||
200: Food processor
|
||||
201: Football
|
||||
202: Football helmet
|
||||
203: Footwear
|
||||
204: Fork
|
||||
205: Fountain
|
||||
206: Fox
|
||||
207: French fries
|
||||
208: French horn
|
||||
209: Frog
|
||||
210: Fruit
|
||||
211: Frying pan
|
||||
212: Furniture
|
||||
213: Garden Asparagus
|
||||
214: Gas stove
|
||||
215: Giraffe
|
||||
216: Girl
|
||||
217: Glasses
|
||||
218: Glove
|
||||
219: Goat
|
||||
220: Goggles
|
||||
221: Goldfish
|
||||
222: Golf ball
|
||||
223: Golf cart
|
||||
224: Gondola
|
||||
225: Goose
|
||||
226: Grape
|
||||
227: Grapefruit
|
||||
228: Grinder
|
||||
229: Guacamole
|
||||
230: Guitar
|
||||
231: Hair dryer
|
||||
232: Hair spray
|
||||
233: Hamburger
|
||||
234: Hammer
|
||||
235: Hamster
|
||||
236: Hand dryer
|
||||
237: Handbag
|
||||
238: Handgun
|
||||
239: Harbor seal
|
||||
240: Harmonica
|
||||
241: Harp
|
||||
242: Harpsichord
|
||||
243: Hat
|
||||
244: Headphones
|
||||
245: Heater
|
||||
246: Hedgehog
|
||||
247: Helicopter
|
||||
248: Helmet
|
||||
249: High heels
|
||||
250: Hiking equipment
|
||||
251: Hippopotamus
|
||||
252: Home appliance
|
||||
253: Honeycomb
|
||||
254: Horizontal bar
|
||||
255: Horse
|
||||
256: Hot dog
|
||||
257: House
|
||||
258: Houseplant
|
||||
259: Human arm
|
||||
260: Human beard
|
||||
261: Human body
|
||||
262: Human ear
|
||||
263: Human eye
|
||||
264: Human face
|
||||
265: Human foot
|
||||
266: Human hair
|
||||
267: Human hand
|
||||
268: Human head
|
||||
269: Human leg
|
||||
270: Human mouth
|
||||
271: Human nose
|
||||
272: Humidifier
|
||||
273: Ice cream
|
||||
274: Indoor rower
|
||||
275: Infant bed
|
||||
276: Insect
|
||||
277: Invertebrate
|
||||
278: Ipod
|
||||
279: Isopod
|
||||
280: Jacket
|
||||
281: Jacuzzi
|
||||
282: Jaguar (Animal)
|
||||
283: Jeans
|
||||
284: Jellyfish
|
||||
285: Jet ski
|
||||
286: Jug
|
||||
287: Juice
|
||||
288: Kangaroo
|
||||
289: Kettle
|
||||
290: Kitchen & dining room table
|
||||
291: Kitchen appliance
|
||||
292: Kitchen knife
|
||||
293: Kitchen utensil
|
||||
294: Kitchenware
|
||||
295: Kite
|
||||
296: Knife
|
||||
297: Koala
|
||||
298: Ladder
|
||||
299: Ladle
|
||||
300: Ladybug
|
||||
301: Lamp
|
||||
302: Land vehicle
|
||||
303: Lantern
|
||||
304: Laptop
|
||||
305: Lavender (Plant)
|
||||
306: Lemon
|
||||
307: Leopard
|
||||
308: Light bulb
|
||||
309: Light switch
|
||||
310: Lighthouse
|
||||
311: Lily
|
||||
312: Limousine
|
||||
313: Lion
|
||||
314: Lipstick
|
||||
315: Lizard
|
||||
316: Lobster
|
||||
317: Loveseat
|
||||
318: Luggage and bags
|
||||
319: Lynx
|
||||
320: Magpie
|
||||
321: Mammal
|
||||
322: Man
|
||||
323: Mango
|
||||
324: Maple
|
||||
325: Maracas
|
||||
326: Marine invertebrates
|
||||
327: Marine mammal
|
||||
328: Measuring cup
|
||||
329: Mechanical fan
|
||||
330: Medical equipment
|
||||
331: Microphone
|
||||
332: Microwave oven
|
||||
333: Milk
|
||||
334: Miniskirt
|
||||
335: Mirror
|
||||
336: Missile
|
||||
337: Mixer
|
||||
338: Mixing bowl
|
||||
339: Mobile phone
|
||||
340: Monkey
|
||||
341: Moths and butterflies
|
||||
342: Motorcycle
|
||||
343: Mouse
|
||||
344: Muffin
|
||||
345: Mug
|
||||
346: Mule
|
||||
347: Mushroom
|
||||
348: Musical instrument
|
||||
349: Musical keyboard
|
||||
350: Nail (Construction)
|
||||
351: Necklace
|
||||
352: Nightstand
|
||||
353: Oboe
|
||||
354: Office building
|
||||
355: Office supplies
|
||||
356: Orange
|
||||
357: Organ (Musical Instrument)
|
||||
358: Ostrich
|
||||
359: Otter
|
||||
360: Oven
|
||||
361: Owl
|
||||
362: Oyster
|
||||
363: Paddle
|
||||
364: Palm tree
|
||||
365: Pancake
|
||||
366: Panda
|
||||
367: Paper cutter
|
||||
368: Paper towel
|
||||
369: Parachute
|
||||
370: Parking meter
|
||||
371: Parrot
|
||||
372: Pasta
|
||||
373: Pastry
|
||||
374: Peach
|
||||
375: Pear
|
||||
376: Pen
|
||||
377: Pencil case
|
||||
378: Pencil sharpener
|
||||
379: Penguin
|
||||
380: Perfume
|
||||
381: Person
|
||||
382: Personal care
|
||||
383: Personal flotation device
|
||||
384: Piano
|
||||
385: Picnic basket
|
||||
386: Picture frame
|
||||
387: Pig
|
||||
388: Pillow
|
||||
389: Pineapple
|
||||
390: Pitcher (Container)
|
||||
391: Pizza
|
||||
392: Pizza cutter
|
||||
393: Plant
|
||||
394: Plastic bag
|
||||
395: Plate
|
||||
396: Platter
|
||||
397: Plumbing fixture
|
||||
398: Polar bear
|
||||
399: Pomegranate
|
||||
400: Popcorn
|
||||
401: Porch
|
||||
402: Porcupine
|
||||
403: Poster
|
||||
404: Potato
|
||||
405: Power plugs and sockets
|
||||
406: Pressure cooker
|
||||
407: Pretzel
|
||||
408: Printer
|
||||
409: Pumpkin
|
||||
410: Punching bag
|
||||
411: Rabbit
|
||||
412: Raccoon
|
||||
413: Racket
|
||||
414: Radish
|
||||
415: Ratchet (Device)
|
||||
416: Raven
|
||||
417: Rays and skates
|
||||
418: Red panda
|
||||
419: Refrigerator
|
||||
420: Remote control
|
||||
421: Reptile
|
||||
422: Rhinoceros
|
||||
423: Rifle
|
||||
424: Ring binder
|
||||
425: Rocket
|
||||
426: Roller skates
|
||||
427: Rose
|
||||
428: Rugby ball
|
||||
429: Ruler
|
||||
430: Salad
|
||||
431: Salt and pepper shakers
|
||||
432: Sandal
|
||||
433: Sandwich
|
||||
434: Saucer
|
||||
435: Saxophone
|
||||
436: Scale
|
||||
437: Scarf
|
||||
438: Scissors
|
||||
439: Scoreboard
|
||||
440: Scorpion
|
||||
441: Screwdriver
|
||||
442: Sculpture
|
||||
443: Sea lion
|
||||
444: Sea turtle
|
||||
445: Seafood
|
||||
446: Seahorse
|
||||
447: Seat belt
|
||||
448: Segway
|
||||
449: Serving tray
|
||||
450: Sewing machine
|
||||
451: Shark
|
||||
452: Sheep
|
||||
453: Shelf
|
||||
454: Shellfish
|
||||
455: Shirt
|
||||
456: Shorts
|
||||
457: Shotgun
|
||||
458: Shower
|
||||
459: Shrimp
|
||||
460: Sink
|
||||
461: Skateboard
|
||||
462: Ski
|
||||
463: Skirt
|
||||
464: Skull
|
||||
465: Skunk
|
||||
466: Skyscraper
|
||||
467: Slow cooker
|
||||
468: Snack
|
||||
469: Snail
|
||||
470: Snake
|
||||
471: Snowboard
|
||||
472: Snowman
|
||||
473: Snowmobile
|
||||
474: Snowplow
|
||||
475: Soap dispenser
|
||||
476: Sock
|
||||
477: Sofa bed
|
||||
478: Sombrero
|
||||
479: Sparrow
|
||||
480: Spatula
|
||||
481: Spice rack
|
||||
482: Spider
|
||||
483: Spoon
|
||||
484: Sports equipment
|
||||
485: Sports uniform
|
||||
486: Squash (Plant)
|
||||
487: Squid
|
||||
488: Squirrel
|
||||
489: Stairs
|
||||
490: Stapler
|
||||
491: Starfish
|
||||
492: Stationary bicycle
|
||||
493: Stethoscope
|
||||
494: Stool
|
||||
495: Stop sign
|
||||
496: Strawberry
|
||||
497: Street light
|
||||
498: Stretcher
|
||||
499: Studio couch
|
||||
500: Submarine
|
||||
501: Submarine sandwich
|
||||
502: Suit
|
||||
503: Suitcase
|
||||
504: Sun hat
|
||||
505: Sunglasses
|
||||
506: Surfboard
|
||||
507: Sushi
|
||||
508: Swan
|
||||
509: Swim cap
|
||||
510: Swimming pool
|
||||
511: Swimwear
|
||||
512: Sword
|
||||
513: Syringe
|
||||
514: Table
|
||||
515: Table tennis racket
|
||||
516: Tablet computer
|
||||
517: Tableware
|
||||
518: Taco
|
||||
519: Tank
|
||||
520: Tap
|
||||
521: Tart
|
||||
522: Taxi
|
||||
523: Tea
|
||||
524: Teapot
|
||||
525: Teddy bear
|
||||
526: Telephone
|
||||
527: Television
|
||||
528: Tennis ball
|
||||
529: Tennis racket
|
||||
530: Tent
|
||||
531: Tiara
|
||||
532: Tick
|
||||
533: Tie
|
||||
534: Tiger
|
||||
535: Tin can
|
||||
536: Tire
|
||||
537: Toaster
|
||||
538: Toilet
|
||||
539: Toilet paper
|
||||
540: Tomato
|
||||
541: Tool
|
||||
542: Toothbrush
|
||||
543: Torch
|
||||
544: Tortoise
|
||||
545: Towel
|
||||
546: Tower
|
||||
547: Toy
|
||||
548: Traffic light
|
||||
549: Traffic sign
|
||||
550: Train
|
||||
551: Training bench
|
||||
552: Treadmill
|
||||
553: Tree
|
||||
554: Tree house
|
||||
555: Tripod
|
||||
556: Trombone
|
||||
557: Trousers
|
||||
558: Truck
|
||||
559: Trumpet
|
||||
560: Turkey
|
||||
561: Turtle
|
||||
562: Umbrella
|
||||
563: Unicycle
|
||||
564: Van
|
||||
565: Vase
|
||||
566: Vegetable
|
||||
567: Vehicle
|
||||
568: Vehicle registration plate
|
||||
569: Violin
|
||||
570: Volleyball (Ball)
|
||||
571: Waffle
|
||||
572: Waffle iron
|
||||
573: Wall clock
|
||||
574: Wardrobe
|
||||
575: Washing machine
|
||||
576: Waste container
|
||||
577: Watch
|
||||
578: Watercraft
|
||||
579: Watermelon
|
||||
580: Weapon
|
||||
581: Whale
|
||||
582: Wheel
|
||||
583: Wheelchair
|
||||
584: Whisk
|
||||
585: Whiteboard
|
||||
586: Willow
|
||||
587: Window
|
||||
588: Window blind
|
||||
589: Wine
|
||||
590: Wine glass
|
||||
591: Wine rack
|
||||
592: Winter melon
|
||||
593: Wok
|
||||
594: Woman
|
||||
595: Wood-burning stove
|
||||
596: Woodpecker
|
||||
597: Worm
|
||||
598: Wrench
|
||||
599: Zebra
|
||||
600: Zucchini
|
||||
|
||||
# Download script/URL (optional) ---------------------------------------------------------------------------------------
|
||||
download: |
|
||||
import warnings
|
||||
|
||||
from ultralytics.utils import LOGGER, SETTINGS, Path
|
||||
from ultralytics.utils.checks import check_requirements
|
||||
|
||||
check_requirements("fiftyone")
|
||||
|
||||
import fiftyone as fo
|
||||
import fiftyone.zoo as foz
|
||||
|
||||
name = "open-images-v7"
|
||||
fo.config.dataset_zoo_dir = Path(SETTINGS["datasets_dir"]) / "fiftyone" / name
|
||||
fraction = 1.0 # fraction of full dataset to use
|
||||
LOGGER.warning("Open Images V7 dataset requires at least **561 GB of free space. Starting download...")
|
||||
for split in "train", "validation": # 1743042 train, 41620 val images
|
||||
train = split == "train"
|
||||
|
||||
# Load Open Images dataset
|
||||
dataset = foz.load_zoo_dataset(
|
||||
name,
|
||||
split=split,
|
||||
label_types=["detections"],
|
||||
max_samples=round((1743042 if train else 41620) * fraction),
|
||||
)
|
||||
|
||||
# Define classes
|
||||
if train:
|
||||
classes = dataset.default_classes # all classes
|
||||
# classes = dataset.distinct('ground_truth.detections.label') # only observed classes
|
||||
|
||||
# Export to YOLO format
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="fiftyone.utils.yolo")
|
||||
dataset.export(
|
||||
export_dir=str(Path(SETTINGS["datasets_dir"]) / name),
|
||||
dataset_type=fo.types.YOLOv5Dataset,
|
||||
label_field="ground_truth",
|
||||
split="val" if split == "validation" else split,
|
||||
classes=classes,
|
||||
overwrite=train,
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Package-seg dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/segment/package-seg/
|
||||
# Example usage: yolo train data=package-seg.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── package-seg ← downloads here (103 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: package-seg # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 1920 images
|
||||
val: images/val # val images (relative to 'path') 89 images
|
||||
test: images/test # test images (relative to 'path') 188 images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: package
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/package-seg.zip
|
||||
@@ -0,0 +1,21 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Signature dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/signature/
|
||||
# Example usage: yolo train data=signature.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── signature ← downloads here (11.3 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: signature # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 143 images
|
||||
val: images/val # val images (relative to 'path') 35 images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: signature
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/signature.zip
|
||||
@@ -0,0 +1,25 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Tiger Pose dataset by Ultralytics
|
||||
# Documentation: https://docs.ultralytics.com/datasets/pose/tiger-pose/
|
||||
# Example usage: yolo train data=tiger-pose.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── tiger-pose ← downloads here (49.8 MB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: tiger-pose # dataset root dir
|
||||
train: images/train # train images (relative to 'path') 210 images
|
||||
val: images/val # val images (relative to 'path') 53 images
|
||||
|
||||
# Keypoints
|
||||
kpt_shape: [12, 2] # number of keypoints, number of dims (2 for x,y or 3 for x,y,visible)
|
||||
flip_idx: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: tiger
|
||||
|
||||
# Download script/URL (optional)
|
||||
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/tiger-pose.zip
|
||||
@@ -0,0 +1,155 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# DIUx xView 2018 Challenge https://challenge.xviewdataset.org by U.S. National Geospatial-Intelligence Agency (NGA)
|
||||
# -------- DOWNLOAD DATA MANUALLY and jar xf val_images.zip to 'datasets/xView' before running train command! --------
|
||||
# Documentation: https://docs.ultralytics.com/datasets/detect/xview/
|
||||
# Example usage: yolo train data=xView.yaml
|
||||
# parent
|
||||
# ├── ultralytics
|
||||
# └── datasets
|
||||
# └── xView ← downloads here (20.7 GB)
|
||||
|
||||
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
|
||||
path: xView # dataset root dir
|
||||
train: images/autosplit_train.txt # train images (relative to 'path') 90% of 847 train images
|
||||
val: images/autosplit_val.txt # train images (relative to 'path') 10% of 847 train images
|
||||
|
||||
# Classes
|
||||
names:
|
||||
0: Fixed-wing Aircraft
|
||||
1: Small Aircraft
|
||||
2: Cargo Plane
|
||||
3: Helicopter
|
||||
4: Passenger Vehicle
|
||||
5: Small Car
|
||||
6: Bus
|
||||
7: Pickup Truck
|
||||
8: Utility Truck
|
||||
9: Truck
|
||||
10: Cargo Truck
|
||||
11: Truck w/Box
|
||||
12: Truck Tractor
|
||||
13: Trailer
|
||||
14: Truck w/Flatbed
|
||||
15: Truck w/Liquid
|
||||
16: Crane Truck
|
||||
17: Railway Vehicle
|
||||
18: Passenger Car
|
||||
19: Cargo Car
|
||||
20: Flat Car
|
||||
21: Tank car
|
||||
22: Locomotive
|
||||
23: Maritime Vessel
|
||||
24: Motorboat
|
||||
25: Sailboat
|
||||
26: Tugboat
|
||||
27: Barge
|
||||
28: Fishing Vessel
|
||||
29: Ferry
|
||||
30: Yacht
|
||||
31: Container Ship
|
||||
32: Oil Tanker
|
||||
33: Engineering Vehicle
|
||||
34: Tower crane
|
||||
35: Container Crane
|
||||
36: Reach Stacker
|
||||
37: Straddle Carrier
|
||||
38: Mobile Crane
|
||||
39: Dump Truck
|
||||
40: Haul Truck
|
||||
41: Scraper/Tractor
|
||||
42: Front loader/Bulldozer
|
||||
43: Excavator
|
||||
44: Cement Mixer
|
||||
45: Ground Grader
|
||||
46: Hut/Tent
|
||||
47: Shed
|
||||
48: Building
|
||||
49: Aircraft Hangar
|
||||
50: Damaged Building
|
||||
51: Facility
|
||||
52: Construction Site
|
||||
53: Vehicle Lot
|
||||
54: Helipad
|
||||
55: Storage Tank
|
||||
56: Shipping container lot
|
||||
57: Shipping Container
|
||||
58: Pylon
|
||||
59: Tower
|
||||
|
||||
# Download script/URL (optional) ---------------------------------------------------------------------------------------
|
||||
download: |
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from tqdm import tqdm
|
||||
|
||||
from ultralytics.data.utils import autosplit
|
||||
from ultralytics.utils.ops import xyxy2xywhn
|
||||
|
||||
|
||||
def convert_labels(fname=Path("xView/xView_train.geojson")):
|
||||
"""Converts xView geoJSON labels to YOLO format, mapping classes to indices 0-59 and saving as text files."""
|
||||
path = fname.parent
|
||||
with open(fname, encoding="utf-8") as f:
|
||||
print(f"Loading {fname}...")
|
||||
data = json.load(f)
|
||||
|
||||
# Make dirs
|
||||
labels = Path(path / "labels" / "train")
|
||||
os.system(f"rm -rf {labels}")
|
||||
labels.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# xView classes 11-94 to 0-59
|
||||
xview_class2index = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, -1, 3, -1, 4, 5, 6, 7, 8, -1, 9, 10, 11,
|
||||
12, 13, 14, 15, -1, -1, 16, 17, 18, 19, 20, 21, 22, -1, 23, 24, 25, -1, 26, 27, -1, 28, -1,
|
||||
29, 30, 31, 32, 33, 34, 35, 36, 37, -1, 38, 39, 40, 41, 42, 43, 44, 45, -1, -1, -1, -1, 46,
|
||||
47, 48, 49, -1, 50, 51, -1, 52, -1, -1, -1, 53, 54, -1, 55, -1, -1, 56, -1, 57, -1, 58, 59]
|
||||
|
||||
shapes = {}
|
||||
for feature in tqdm(data["features"], desc=f"Converting {fname}"):
|
||||
p = feature["properties"]
|
||||
if p["bounds_imcoords"]:
|
||||
id = p["image_id"]
|
||||
file = path / "train_images" / id
|
||||
if file.exists(): # 1395.tif missing
|
||||
try:
|
||||
box = np.array([int(num) for num in p["bounds_imcoords"].split(",")])
|
||||
assert box.shape[0] == 4, f"incorrect box shape {box.shape[0]}"
|
||||
cls = p["type_id"]
|
||||
cls = xview_class2index[int(cls)] # xView class to 0-60
|
||||
assert 59 >= cls >= 0, f"incorrect class index {cls}"
|
||||
|
||||
# Write YOLO label
|
||||
if id not in shapes:
|
||||
shapes[id] = Image.open(file).size
|
||||
box = xyxy2xywhn(box[None].astype(np.float), w=shapes[id][0], h=shapes[id][1], clip=True)
|
||||
with open((labels / id).with_suffix(".txt"), "a", encoding="utf-8") as f:
|
||||
f.write(f"{cls} {' '.join(f'{x:.6f}' for x in box[0])}\n") # write label.txt
|
||||
except Exception as e:
|
||||
print(f"WARNING: skipping one label for {file}: {e}")
|
||||
|
||||
|
||||
# Download manually from https://challenge.xviewdataset.org
|
||||
dir = Path(yaml["path"]) # dataset root dir
|
||||
# urls = [
|
||||
# "https://d307kc0mrhucc3.cloudfront.net/train_labels.zip", # train labels
|
||||
# "https://d307kc0mrhucc3.cloudfront.net/train_images.zip", # 15G, 847 train images
|
||||
# "https://d307kc0mrhucc3.cloudfront.net/val_images.zip", # 5G, 282 val images (no labels)
|
||||
# ]
|
||||
# download(urls, dir=dir)
|
||||
|
||||
# Convert labels
|
||||
convert_labels(dir / "xView_train.geojson")
|
||||
|
||||
# Move images
|
||||
images = Path(dir / "images")
|
||||
images.mkdir(parents=True, exist_ok=True)
|
||||
Path(dir / "train_images").rename(dir / "images" / "train")
|
||||
Path(dir / "val_images").rename(dir / "images" / "val")
|
||||
|
||||
# Split
|
||||
autosplit(dir / "images" / "train")
|
||||
@@ -0,0 +1,127 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Global configuration YAML with settings and hyperparameters for YOLO training, validation, prediction and export
|
||||
# For documentation see https://docs.ultralytics.com/usage/cfg/
|
||||
|
||||
task: detect # (str) YOLO task, i.e. detect, segment, classify, pose, obb
|
||||
mode: train # (str) YOLO mode, i.e. train, val, predict, export, track, benchmark
|
||||
|
||||
# Train settings -------------------------------------------------------------------------------------------------------
|
||||
model: # (str, optional) path to model file, i.e. yolov8n.pt, yolov8n.yaml
|
||||
data: # (str, optional) path to data file, i.e. coco8.yaml
|
||||
epochs: 100 # (int) number of epochs to train for
|
||||
time: # (float, optional) number of hours to train for, overrides epochs if supplied
|
||||
patience: 100 # (int) epochs to wait for no observable improvement for early stopping of training
|
||||
batch: 16 # (int) number of images per batch (-1 for AutoBatch)
|
||||
imgsz: 640 # (int | list) input images size as int for train and val modes, or list[h,w] for predict and export modes
|
||||
save: True # (bool) save train checkpoints and predict results
|
||||
save_period: -1 # (int) Save checkpoint every x epochs (disabled if < 1)
|
||||
cache: False # (bool) True/ram, disk or False. Use cache for data loading
|
||||
device: # (int | str | list) device: CUDA device=0 or [0,1,2,3] or "cpu/mps" or -1 or [-1,-1] to auto-select idle GPUs
|
||||
workers: 8 # (int) number of worker threads for data loading (per RANK if DDP)
|
||||
project: # (str, optional) project name
|
||||
name: # (str, optional) experiment name, results saved to 'project/name' directory
|
||||
exist_ok: False # (bool) whether to overwrite existing experiment
|
||||
pretrained: True # (bool | str) whether to use a pretrained model (bool) or a model to load weights from (str)
|
||||
optimizer: auto # (str) optimizer to use, choices=[SGD, Adam, Adamax, AdamW, NAdam, RAdam, RMSProp, auto]
|
||||
verbose: True # (bool) whether to print verbose output
|
||||
seed: 0 # (int) random seed for reproducibility
|
||||
deterministic: True # (bool) whether to enable deterministic mode
|
||||
single_cls: False # (bool) train multi-class data as single-class
|
||||
rect: False # (bool) rectangular training if mode='train' or rectangular validation if mode='val'
|
||||
cos_lr: False # (bool) use cosine learning rate scheduler
|
||||
close_mosaic: 10 # (int) disable mosaic augmentation for final epochs (0 to disable)
|
||||
resume: False # (bool) resume training from last checkpoint
|
||||
amp: True # (bool) Automatic Mixed Precision (AMP) training, choices=[True, False], True runs AMP check
|
||||
fraction: 1.0 # (float) dataset fraction to train on (default is 1.0, all images in train set)
|
||||
profile: False # (bool) profile ONNX and TensorRT speeds during training for loggers
|
||||
freeze: # (int | list, optional) freeze first n layers, or freeze list of layer indices during training
|
||||
multi_scale: False # (bool) Whether to use multiscale during training
|
||||
# Segmentation
|
||||
overlap_mask: True # (bool) merge object masks into a single image mask during training (segment train only)
|
||||
mask_ratio: 4 # (int) mask downsample ratio (segment train only)
|
||||
# Classification
|
||||
dropout: 0.0 # (float) use dropout regularization (classify train only)
|
||||
|
||||
# Val/Test settings ----------------------------------------------------------------------------------------------------
|
||||
val: True # (bool) validate/test during training
|
||||
split: val # (str) dataset split to use for validation, i.e. 'val', 'test' or 'train'
|
||||
save_json: False # (bool) save results to JSON file
|
||||
conf: # (float, optional) object confidence threshold for detection (default 0.25 predict, 0.001 val)
|
||||
iou: 0.7 # (float) intersection over union (IoU) threshold for NMS
|
||||
max_det: 300 # (int) maximum number of detections per image
|
||||
half: False # (bool) use half precision (FP16)
|
||||
dnn: False # (bool) use OpenCV DNN for ONNX inference
|
||||
plots: True # (bool) save plots and images during train/val
|
||||
|
||||
# Predict settings -----------------------------------------------------------------------------------------------------
|
||||
source: # (str, optional) source directory for images or videos
|
||||
vid_stride: 1 # (int) video frame-rate stride
|
||||
stream_buffer: False # (bool) buffer all streaming frames (True) or return the most recent frame (False)
|
||||
visualize: False # (bool) visualize model features (predict) or visualize TP, FP, FN (val)
|
||||
augment: False # (bool) apply image augmentation to prediction sources
|
||||
agnostic_nms: False # (bool) class-agnostic NMS
|
||||
classes: # (int | list[int], optional) filter results by class, i.e. classes=0, or classes=[0,2,3]
|
||||
retina_masks: False # (bool) use high-resolution segmentation masks
|
||||
embed: # (list[int], optional) return feature vectors/embeddings from given layers
|
||||
|
||||
# Visualize settings ---------------------------------------------------------------------------------------------------
|
||||
show: False # (bool) show predicted images and videos if environment allows
|
||||
save_frames: False # (bool) save predicted individual video frames
|
||||
save_txt: False # (bool) save results as .txt file
|
||||
save_conf: False # (bool) save results with confidence scores
|
||||
save_crop: False # (bool) save cropped images with results
|
||||
show_labels: True # (bool) show prediction labels, i.e. 'person'
|
||||
show_conf: True # (bool) show prediction confidence, i.e. '0.99'
|
||||
show_boxes: True # (bool) show prediction boxes
|
||||
line_width: # (int, optional) line width of the bounding boxes. Scaled to image size if None.
|
||||
|
||||
# Export settings ------------------------------------------------------------------------------------------------------
|
||||
format: torchscript # (str) format to export to, choices at https://docs.ultralytics.com/modes/export/#export-formats
|
||||
keras: False # (bool) use Kera=s
|
||||
optimize: False # (bool) TorchScript: optimize for mobile
|
||||
int8: False # (bool) CoreML/TF INT8 quantization
|
||||
dynamic: False # (bool) ONNX/TF/TensorRT: dynamic axes
|
||||
simplify: True # (bool) ONNX: simplify model using `onnxslim`
|
||||
opset: # (int, optional) ONNX: opset version
|
||||
workspace: # (float, optional) TensorRT: workspace size (GiB), `None` will let TensorRT auto-allocate memory
|
||||
nms: False # (bool) CoreML: add NMS
|
||||
|
||||
# Hyperparameters ------------------------------------------------------------------------------------------------------
|
||||
lr0: 0.01 # (float) initial learning rate (i.e. SGD=1E-2, Adam=1E-3)
|
||||
lrf: 0.01 # (float) final learning rate (lr0 * lrf)
|
||||
momentum: 0.937 # (float) SGD momentum/Adam beta1
|
||||
weight_decay: 0.0005 # (float) optimizer weight decay 5e-4
|
||||
warmup_epochs: 3.0 # (float) warmup epochs (fractions ok)
|
||||
warmup_momentum: 0.8 # (float) warmup initial momentum
|
||||
warmup_bias_lr: 0.1 # (float) warmup initial bias lr
|
||||
box: 7.5 # (float) box loss gain
|
||||
cls: 0.5 # (float) cls loss gain (scale with pixels)
|
||||
dfl: 1.5 # (float) dfl loss gain
|
||||
pose: 12.0 # (float) pose loss gain
|
||||
kobj: 1.0 # (float) keypoint obj loss gain
|
||||
nbs: 64 # (int) nominal batch size
|
||||
hsv_h: 0.015 # (float) image HSV-Hue augmentation (fraction)
|
||||
hsv_s: 0.7 # (float) image HSV-Saturation augmentation (fraction)
|
||||
hsv_v: 0.4 # (float) image HSV-Value augmentation (fraction)
|
||||
degrees: 0.0 # (float) image rotation (+/- deg)
|
||||
translate: 0.1 # (float) image translation (+/- fraction)
|
||||
scale: 0.5 # (float) image scale (+/- gain)
|
||||
shear: 0.0 # (float) image shear (+/- deg)
|
||||
perspective: 0.0 # (float) image perspective (+/- fraction), range 0-0.001
|
||||
flipud: 0.0 # (float) image flip up-down (probability)
|
||||
fliplr: 0.5 # (float) image flip left-right (probability)
|
||||
bgr: 0.0 # (float) image channel BGR (probability)
|
||||
mosaic: 1.0 # (float) image mosaic (probability)
|
||||
mixup: 0.0 # (float) image mixup (probability)
|
||||
cutmix: 0.0 # (float) image cutmix (probability)
|
||||
copy_paste: 0.0 # (float) segment copy-paste (probability)
|
||||
copy_paste_mode: "flip" # (str) the method to do copy_paste augmentation (flip, mixup)
|
||||
auto_augment: randaugment # (str) auto augmentation policy for classification (randaugment, autoaugment, augmix)
|
||||
erasing: 0.4 # (float) probability of random erasing during classification training (0-0.9), 0 means no erasing, must be less than 1.0.
|
||||
|
||||
# Custom config.yaml ---------------------------------------------------------------------------------------------------
|
||||
cfg: # (str, optional) for overriding defaults.yaml
|
||||
|
||||
# Tracker settings ------------------------------------------------------------------------------------------------------
|
||||
tracker: botsort.yaml # (str) tracker type, choices=[botsort.yaml, bytetrack.yaml]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Ultralytics YOLO11-cls image classification model with ResNet18 backbone
|
||||
# Model docs: https://docs.ultralytics.com/models/yolo11
|
||||
# Task docs: https://docs.ultralytics.com/tasks/classify
|
||||
|
||||
# Parameters
|
||||
nc: 1000 # number of classes
|
||||
|
||||
# ResNet18 backbone
|
||||
backbone:
|
||||
# [from, repeats, module, args]
|
||||
- [-1, 1, TorchVision, [512, resnet18, DEFAULT, True, 2]] # truncate two layers from the end
|
||||
|
||||
# YOLO11n head
|
||||
head:
|
||||
- [-1, 1, Classify, [nc]] # Classify
|
||||
@@ -0,0 +1,33 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Ultralytics YOLO11-cls image classification model
|
||||
# Model docs: https://docs.ultralytics.com/models/yolo11
|
||||
# Task docs: https://docs.ultralytics.com/tasks/classify
|
||||
|
||||
# Parameters
|
||||
nc: 1000 # number of classes
|
||||
scales: # model compound scaling constants, i.e. 'model=yolo11n-cls.yaml' will call yolo11-cls.yaml with scale 'n'
|
||||
# [depth, width, max_channels]
|
||||
n: [0.50, 0.25, 1024] # summary: 86 layers, 1633584 parameters, 1633584 gradients, 0.5 GFLOPs
|
||||
s: [0.50, 0.50, 1024] # summary: 86 layers, 5545488 parameters, 5545488 gradients, 1.6 GFLOPs
|
||||
m: [0.50, 1.00, 512] # summary: 106 layers, 10455696 parameters, 10455696 gradients, 5.0 GFLOPs
|
||||
l: [1.00, 1.00, 512] # summary: 176 layers, 12937104 parameters, 12937104 gradients, 6.2 GFLOPs
|
||||
x: [1.00, 1.50, 512] # summary: 176 layers, 28458544 parameters, 28458544 gradients, 13.7 GFLOPs
|
||||
|
||||
# YOLO11n backbone
|
||||
backbone:
|
||||
# [from, repeats, module, args]
|
||||
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
|
||||
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
|
||||
- [-1, 2, C3k2, [256, False, 0.25]]
|
||||
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
|
||||
- [-1, 2, C3k2, [512, False, 0.25]]
|
||||
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
|
||||
- [-1, 2, C3k2, [512, True]]
|
||||
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
|
||||
- [-1, 2, C3k2, [1024, True]]
|
||||
- [-1, 2, C2PSA, [1024]] # 9
|
||||
|
||||
# YOLO11n head
|
||||
head:
|
||||
- [-1, 1, Classify, [nc]] # Classify
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user