Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f66e4d41c9 | |||
| e0cba105b6 | |||
| a8e764b102 |
@@ -0,0 +1,75 @@
|
|||||||
|
# Python 3.10 兼容性说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
本分支 (python3.10) 专门针对 Python 3.10 环境优化,确保所有代码和依赖都能在 Python 3.10 下正常运行。
|
||||||
|
|
||||||
|
## 兼容性调整
|
||||||
|
|
||||||
|
### 已验证的兼容特性
|
||||||
|
✅ 所有依赖包版本均支持 Python 3.10
|
||||||
|
✅ 无 Python 3.11 特有语法(如 `tomllib`、`asyncio.TaskGroup`、`typing.Self` 等)
|
||||||
|
✅ 类型注解完全兼容 Python 3.10
|
||||||
|
✅ `match` 关键字未用作 Python 3.10 的模式匹配语句(仅用作 `re.match()` 的变量名)
|
||||||
|
|
||||||
|
### 重要依赖说明
|
||||||
|
|
||||||
|
#### pysnmp 兼容问题解决
|
||||||
|
**问题**:`pysnmp==7.1.15` 存在以下兼容性问题:
|
||||||
|
- 使用已废弃的 `asyncio.coroutine` 装饰器(Python 3.11 中已移除)
|
||||||
|
- 与 Python 3.10+ 的 asyncio 不兼容
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
使用社区维护的分支 `pysnmp-lextudio==5.0.31`
|
||||||
|
- 完全向后兼容原有 pysnmp API
|
||||||
|
- 支持 Python 3.6+
|
||||||
|
- 积极维护,修复了 asyncio 兼容性问题
|
||||||
|
- 导入方式完全不变
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 无需修改代码,导入方式保持一致
|
||||||
|
from pysnmp.hlapi.asyncio import bulkCmd, getCmd, nextCmd
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pyasn1 版本冲突解决
|
||||||
|
**问题**:`pyasn1>=0.5.0` 移除了 `pyasn1.compat.octets` 模块,导致 pysnmp 导入失败。
|
||||||
|
同时 `python-jose==3.5.0` 强制要求 `pyasn1>=0.5.0`,形成版本冲突。
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- `pyasn1==0.4.8` - 锁定到包含 compat.octets 的版本
|
||||||
|
- `pyasn1-modules==0.2.8` - 配套版本
|
||||||
|
- `python-jose[cryptography]==3.3.0` - 兼容 pyasn1 0.4.8 的版本
|
||||||
|
|
||||||
|
### 依赖版本要求
|
||||||
|
- `pysnmp-lextudio==5.0.31` - SNMP 协议兼容替代
|
||||||
|
- `typing-extensions>=4.5.0` - 提供额外的类型支持
|
||||||
|
- 所有核心依赖(FastAPI、SQLAlchemy、Pydantic 等)均已验证兼容
|
||||||
|
|
||||||
|
## 已知的 Python 3.11+ 特性未使用
|
||||||
|
项目中未使用以下 Python 3.11+ 特有的功能:
|
||||||
|
- `tomllib` 标准库(如有需要可使用 `tomli` 第三方库)
|
||||||
|
- `asyncio.TaskGroup`
|
||||||
|
- `typing.Self`
|
||||||
|
- `ExceptionGroup` / `except*`
|
||||||
|
- `StrEnum` / `IntEnum` 的新特性
|
||||||
|
|
||||||
|
## 安装说明
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 升级提示
|
||||||
|
如果未来需要升级到 Python 3.11+,可以:
|
||||||
|
1. 使用 `tomllib` 替代可能的 TOML 解析需求
|
||||||
|
2. 考虑使用 `asyncio.TaskGroup` 改进异步代码结构
|
||||||
|
3. 使用 `typing.Self` 简化类型注解
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
可以使用以下命令验证 Python 版本兼容性:
|
||||||
|
```bash
|
||||||
|
python --version # 应为 3.10.x 或 3.11.x
|
||||||
|
python -c "import sys; assert sys.version_info >= (3, 10), 'Python 3.10+ required'"
|
||||||
|
|
||||||
|
# 验证 pysnmp 导入
|
||||||
|
python -c "from pysnmp.hlapi.asyncio import bulkCmd, getCmd, nextCmd; print('pysnmp OK')"
|
||||||
|
```
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -248,6 +248,7 @@ def get_network_devices(
|
|||||||
# 补充手写的关联字段(datetime 已经由 serialize_dt_fields 序列化)
|
# 补充手写的关联字段(datetime 已经由 serialize_dt_fields 序列化)
|
||||||
item["device_type"] = d.device_type.value if hasattr(d.device_type, 'value') else d.device_type
|
item["device_type"] = d.device_type.value if hasattr(d.device_type, 'value') else d.device_type
|
||||||
item["credential_name"] = cred_map.get(d.snmp_credential_id)
|
item["credential_name"] = cred_map.get(d.snmp_credential_id)
|
||||||
|
item["snmp_credential_id"] = d.snmp_credential_id # 显式添加,确保前端能拿到
|
||||||
item["last_polled_at"] = item.get("last_polled_at") # 已是 ISO 字符串
|
item["last_polled_at"] = item.get("last_polled_at") # 已是 ISO 字符串
|
||||||
item["last_successful_poll"] = item.get("last_successful_poll")
|
item["last_successful_poll"] = item.get("last_successful_poll")
|
||||||
item["arp_poll_interval"] = d.arp_poll_interval
|
item["arp_poll_interval"] = d.arp_poll_interval
|
||||||
@@ -325,6 +326,7 @@ def update_network_device(
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
ip_address: Optional[str] = None,
|
ip_address: Optional[str] = None,
|
||||||
snmp_credential_id: Optional[int] = None,
|
snmp_credential_id: Optional[int] = None,
|
||||||
|
clear_snmp_credential: bool = False, # 新增:专门的标志
|
||||||
port: Optional[int] = None,
|
port: Optional[int] = None,
|
||||||
device_type: Optional[str] = None,
|
device_type: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
@@ -350,7 +352,13 @@ def update_network_device(
|
|||||||
device.name = name
|
device.name = name
|
||||||
if ip_address:
|
if ip_address:
|
||||||
device.ip_address = ip_address
|
device.ip_address = ip_address
|
||||||
if snmp_credential_id is not None:
|
# 处理 SNMP 凭据:
|
||||||
|
# - clear_snmp_credential=True → 清除凭据
|
||||||
|
# - snmp_credential_id > 0 → 设置为该值
|
||||||
|
# - 否则(都不做任何操作
|
||||||
|
if clear_snmp_credential:
|
||||||
|
device.snmp_credential_id = None
|
||||||
|
elif snmp_credential_id is not None and snmp_credential_id > 0:
|
||||||
device.snmp_credential_id = snmp_credential_id
|
device.snmp_credential_id = snmp_credential_id
|
||||||
if port:
|
if port:
|
||||||
device.port = port
|
device.port = port
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,7 +4,7 @@ from datetime import datetime
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pysnmp.hlapi import (
|
from pysnmp.hlapi.asyncio import (
|
||||||
SnmpEngine, CommunityData, UsmUserData,
|
SnmpEngine, CommunityData, UsmUserData,
|
||||||
UdpTransportTarget, ContextData,
|
UdpTransportTarget, ContextData,
|
||||||
ObjectType, ObjectIdentity,
|
ObjectType, ObjectIdentity,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Python 3.10+ 兼容版本
|
||||||
fastapi==0.110.0
|
fastapi==0.110.0
|
||||||
uvicorn[standard]==0.27.1
|
uvicorn[standard]==0.27.1
|
||||||
sqlalchemy==2.0.28
|
sqlalchemy==2.0.28
|
||||||
@@ -9,9 +10,18 @@ celery==5.3.6
|
|||||||
redis==5.0.3
|
redis==5.0.3
|
||||||
python-multipart==0.0.9
|
python-multipart==0.0.9
|
||||||
alembic==1.13.1
|
alembic==1.13.1
|
||||||
pysnmp==7.1.15
|
# 注意: pysnmp 7.x 与 Python 3.10+/asyncio 存在兼容性问题
|
||||||
|
# 使用 lextudio 维护的分支(向后兼容,支持 Python 3.6+)
|
||||||
|
pysnmp-lextudio==5.0.31
|
||||||
|
# pyasn1 0.5.0+ 移除了 compat.octets 模块,与 pysnmp 不兼容
|
||||||
|
pyasn1==0.4.8
|
||||||
|
pyasn1-modules==0.2.8
|
||||||
scapy==2.5.0
|
scapy==2.5.0
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
passlib[bcrypt]==1.7.4
|
passlib[bcrypt]==1.7.4
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
python-jose[cryptography]
|
# python-jose 3.5.0 需要 pyasn1>=0.5.0,与 pysnmp 冲突
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
bcrypt==3.2.2
|
||||||
|
# typing-extensions 提供 Python 3.10+ 的向后兼容支持
|
||||||
|
typing-extensions>=4.5.0
|
||||||
|
|||||||
@@ -398,7 +398,13 @@ const saveDevice = async () => {
|
|||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
if (isDeviceEdit.value) {
|
if (isDeviceEdit.value) {
|
||||||
await snmpApi.updateDevice(deviceForm.id, deviceForm)
|
// 处理清除凭据:snmp_credential_id=null 时发送 clear_snmp_credential=true
|
||||||
|
const updateData = { ...deviceForm }
|
||||||
|
if (updateData.snmp_credential_id === null) {
|
||||||
|
delete updateData.snmp_credential_id
|
||||||
|
updateData.clear_snmp_credential = true
|
||||||
|
}
|
||||||
|
await snmpApi.updateDevice(deviceForm.id, updateData)
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
} else {
|
} else {
|
||||||
await snmpApi.createDevice(deviceForm)
|
await snmpApi.createDevice(deviceForm)
|
||||||
|
|||||||
Reference in New Issue
Block a user