codosdk 1.0.54__tar.gz → 1.0.56__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codosdk-1.0.54 → codosdk-1.0.56}/PKG-INFO +1 -1
- codosdk-1.0.56/README.md +215 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/codosdk.egg-info/PKG-INFO +1 -1
- {codosdk-1.0.54 → codosdk-1.0.56}/setup.py +1 -1
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/apis/agent_apis.py +9 -0
- codosdk-1.0.56/websdk2/cache_context.py +150 -0
- codosdk-1.0.56/websdk2/mqhelper.py +435 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/sqlalchemy_pagination.py +2 -1
- codosdk-1.0.54/README.md +0 -58
- codosdk-1.0.54/websdk2/cache_context.py +0 -38
- codosdk-1.0.54/websdk2/mqhelper.py +0 -144
- {codosdk-1.0.54 → codosdk-1.0.56}/LICENSE +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/codosdk.egg-info/SOURCES.txt +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/codosdk.egg-info/dependency_links.txt +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/codosdk.egg-info/requires.txt +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/codosdk.egg-info/top_level.txt +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/opssdk/__init__.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/opssdk/utils/__init__.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/pyproject.toml +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/setup.cfg +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/__init__.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/api_set.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/apis/__init__.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/apis/admin_apis.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/apis/cmdb_apis.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/apis/kerrigan_apis.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/apis/mgv4_apis.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/apis/notice_apis.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/apis/task_apis.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/application.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/base_handler.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/cache.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/client.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/cloud/__init__.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/cloud/qcloud_api.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/cloud/ucloud_api.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/cloud_utils.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/configs.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/consts.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/crud_utils.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/db_context.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/error.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/fetch_coroutine.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/jwt_token.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/ldap.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/logger.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/model_utils.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/program.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/salt_api.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/tools.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/utils/__init__.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/utils/cc_crypto.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/utils/date_format.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/utils/pydantic_utils.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/utils/pydantic_utils_v2.py +0 -0
- {codosdk-1.0.54 → codosdk-1.0.56}/websdk2/web_logs.py +0 -0
codosdk-1.0.56/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# CODO SDK
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/)
|
|
4
|
+
[](https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
[](https://pypi.org/project/codosdk/)
|
|
6
|
+
|
|
7
|
+
CODO项目的官方Python SDK,提供企业级Web应用开发框架和运维工具集成。基于Tornado,集成数据库、缓存、消息队列、认证等核心组件。
|
|
8
|
+
|
|
9
|
+
## 核心特性
|
|
10
|
+
|
|
11
|
+
- **Tornado Web框架** - 异步请求处理,内置认证和权限管理
|
|
12
|
+
- **数据库支持** - SQLAlchemy ORM,支持主从库配置
|
|
13
|
+
- **Redis缓存** - 多连接池管理
|
|
14
|
+
- **消息队列** - RabbitMQ集成
|
|
15
|
+
- **多种认证** - JWT、LDAP、Session
|
|
16
|
+
- **API集成** - 预定义CODO各平台API接口
|
|
17
|
+
- **数据验证** - Pydantic模型验证
|
|
18
|
+
- **工具支持** - 各类常用工具封装
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 从PyPI安装
|
|
24
|
+
pip install codosdk
|
|
25
|
+
|
|
26
|
+
# 从GitHub安装最新版
|
|
27
|
+
pip install -U git+https://github.com/ss1917/ops_sdk.git
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 快速开始
|
|
31
|
+
|
|
32
|
+
### 创建Web应用
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from websdk2.application import Application
|
|
36
|
+
from websdk2.base_handler import BaseHandler
|
|
37
|
+
|
|
38
|
+
class HelloHandler(BaseHandler):
|
|
39
|
+
def get(self):
|
|
40
|
+
self.write({'code': 0, 'msg': 'Hello CODO!'})
|
|
41
|
+
|
|
42
|
+
handlers = [
|
|
43
|
+
(r'/api/hello/', HelloHandler, {'handle_name': '问候接口', 'method': ['GET']}),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
app = Application(handlers)
|
|
47
|
+
app.start_server()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 使用数据库
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from websdk2.db_context import DBContextV2 as DBContext
|
|
54
|
+
from sqlalchemy import Column, Integer, String
|
|
55
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
56
|
+
|
|
57
|
+
Base = declarative_base()
|
|
58
|
+
|
|
59
|
+
class User(Base):
|
|
60
|
+
__tablename__ = 'users'
|
|
61
|
+
id = Column(Integer, primary_key=True)
|
|
62
|
+
username = Column(String(50), unique=True)
|
|
63
|
+
|
|
64
|
+
# 查询
|
|
65
|
+
with DBContext('r') as session:
|
|
66
|
+
user = session.query(User).filter(User.username == 'admin').first()
|
|
67
|
+
|
|
68
|
+
# 创建/更新
|
|
69
|
+
with DBContext('w') as session:
|
|
70
|
+
user = User(username='new_user')
|
|
71
|
+
session.add(user)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 使用缓存
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from websdk2.cache_context import cache_conn
|
|
78
|
+
|
|
79
|
+
redis_conn = cache_conn()
|
|
80
|
+
redis_conn.set('key', 'value', ex=3600)
|
|
81
|
+
value = redis_conn.get('key')
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### JWT认证
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from websdk2.jwt_token import AuthToken
|
|
88
|
+
|
|
89
|
+
auth = AuthToken()
|
|
90
|
+
|
|
91
|
+
# 生成Token
|
|
92
|
+
token = auth.encode_auth_token(
|
|
93
|
+
user_id='123',
|
|
94
|
+
username='admin',
|
|
95
|
+
exp_time=7 # 7天过期
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# 验证Token
|
|
99
|
+
user_info = auth.decode_auth_token(token)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 消息队列
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from websdk2.mqhelper import MessageQueueBase
|
|
106
|
+
import json
|
|
107
|
+
|
|
108
|
+
# 发送消息(with语句自动管理连接)
|
|
109
|
+
msg = json.dumps({'task_id': '123', 'status': 'running'})
|
|
110
|
+
with MessageQueueBase('bpm_task_log', 'direct', 'the_log') as mq:
|
|
111
|
+
mq.publish_message(msg)
|
|
112
|
+
|
|
113
|
+
# 消费消息(继承MessageQueueBase)
|
|
114
|
+
class TaskConsumer(MessageQueueBase):
|
|
115
|
+
def __init__(self):
|
|
116
|
+
super().__init__(
|
|
117
|
+
exchange='bpm_task_log',
|
|
118
|
+
exchange_type='direct',
|
|
119
|
+
routing_key='the_log',
|
|
120
|
+
queue_name='task_queue'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def on_message(self, body):
|
|
124
|
+
"""处理接收到的消息"""
|
|
125
|
+
print(f"收到消息: {body}")
|
|
126
|
+
|
|
127
|
+
# 启动消费
|
|
128
|
+
consumer = TaskConsumer()
|
|
129
|
+
consumer.start_consuming()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 配置示例
|
|
133
|
+
|
|
134
|
+
### 数据库配置
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from websdk2.consts import const
|
|
138
|
+
|
|
139
|
+
db_config = {
|
|
140
|
+
const.DB_CONFIG_ITEM: {
|
|
141
|
+
const.DEFAULT_DB_KEY: {
|
|
142
|
+
const.DBHOST_KEY: 'localhost',
|
|
143
|
+
const.DBPORT_KEY: 3306,
|
|
144
|
+
const.DBUSER_KEY: 'root',
|
|
145
|
+
const.DBPWD_KEY: 'password',
|
|
146
|
+
const.DBNAME_KEY: 'codo_db',
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Redis配置
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from websdk2.consts import const
|
|
156
|
+
redis_config = {
|
|
157
|
+
const.REDIS_CONFIG_ITEM: {
|
|
158
|
+
const.DEFAULT_RD_KEY: {
|
|
159
|
+
const.RD_HOST_KEY: 'localhost',
|
|
160
|
+
const.RD_PORT_KEY: 6379,
|
|
161
|
+
const.RD_DB_KEY: 0,
|
|
162
|
+
const.RD_PASSWORD_KEY: 'password',
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## 项目结构
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
ops_sdk/
|
|
172
|
+
├── websdk2/ # Web开发SDK(主模块)
|
|
173
|
+
│ ├── apis/ # CODO平台API集合
|
|
174
|
+
│ │ ├── admin_apis.py # 用户管理API
|
|
175
|
+
│ │ ├── mgv4_apis.py # 后台管理API
|
|
176
|
+
│ │ ├── cmdb_apis.py # CMDB配置API
|
|
177
|
+
│ │ ├── agent_apis.py # Agent代理API
|
|
178
|
+
│ │ ├── task_apis.py # 任务调度API
|
|
179
|
+
│ │ ├── kerrigan_apis.py # 配置文件管理API
|
|
180
|
+
│ │ └── notice_apis.py # 通知告警API
|
|
181
|
+
│ │
|
|
182
|
+
│ ├── cloud/ # 云厂商SDK
|
|
183
|
+
│ │
|
|
184
|
+
│ ├── utils/ # 工具集
|
|
185
|
+
│ │ ├── pydantic_utils.py # Pydantic数据验证
|
|
186
|
+
│ │ ├── date_format.py # 日期格式化
|
|
187
|
+
│ │ └── cc_crypto.py # 加密解密
|
|
188
|
+
│ │
|
|
189
|
+
│ ├── application.py # Tornado应用定制
|
|
190
|
+
│ ├── base_handler.py # 请求处理基类(认证、授权)
|
|
191
|
+
│ ├── db_context.py # 数据库连接管理
|
|
192
|
+
│ ├── cache.py / cache_context.py # Redis缓存管理
|
|
193
|
+
│ ├── crud_utils.py # CRUD工具(分页、验证)
|
|
194
|
+
│ ├── sqlalchemy_pagination.py # ORM分页组件
|
|
195
|
+
│ ├── model_utils.py # 模型转换工具
|
|
196
|
+
│ ├── jwt_token.py # JWT认证
|
|
197
|
+
│ ├── mqhelper.py # RabbitMQ消息队列
|
|
198
|
+
│ ├── client.py # API调用客户端
|
|
199
|
+
│ ├── ldap.py # LDAP认证
|
|
200
|
+
│ ├── configs.py # 配置管理
|
|
201
|
+
│ ├── consts.py # 常量定义
|
|
202
|
+
│ ├── error.py # 自定义异常
|
|
203
|
+
│ ├── logger.py # 日志配置
|
|
204
|
+
│ └── ... # 其他工具模块
|
|
205
|
+
│
|
|
206
|
+
├── opssdk/ # 运维SDK(原始模块)
|
|
207
|
+
│ └── utils/ # 运维工具集
|
|
208
|
+
│
|
|
209
|
+
├── setup.py # 包配置
|
|
210
|
+
└── pyproject.toml # 项目配置
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
Everything is [GPL v3.0](https://www.gnu.org/licenses/gpl-3.0.html).
|
|
@@ -9,7 +9,7 @@ desc : CODO SDK
|
|
|
9
9
|
import sys
|
|
10
10
|
from setuptools import setup, find_packages
|
|
11
11
|
|
|
12
|
-
VERSION = '1.0.
|
|
12
|
+
VERSION = '1.0.56'
|
|
13
13
|
|
|
14
14
|
if sys.version_info < (2, 7) or (3, 0) <= sys.version_info < (3, 6):
|
|
15
15
|
print('This program requires at least Python 2.7 or 3.6 to run.')
|
|
@@ -18,6 +18,7 @@ class AgentAPIS:
|
|
|
18
18
|
|
|
19
19
|
sync_files_register_url_v2 = f"{route_prefix}/v1/manager/agent/share_filev2/register"
|
|
20
20
|
sync_files_cancel_url_v2 = f"{route_prefix}/v1/manager/agent/share_filev2/cancel"
|
|
21
|
+
get_agent_task_result_url = f"{route_prefix}/v1/manager/agent/task/result'"
|
|
21
22
|
|
|
22
23
|
get_agent_list = dict(method='GET',
|
|
23
24
|
url=agent_list_url,
|
|
@@ -77,3 +78,11 @@ class AgentAPIS:
|
|
|
77
78
|
field_help={
|
|
78
79
|
},
|
|
79
80
|
description='云原生任务查询')
|
|
81
|
+
|
|
82
|
+
get_agent_task_result = dict(method='GET',
|
|
83
|
+
url=get_agent_task_result_url,
|
|
84
|
+
params={},
|
|
85
|
+
field_help={
|
|
86
|
+
'task_id': "提供任务ID"
|
|
87
|
+
},
|
|
88
|
+
description='云原生任务查询')
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Contact : 191715030@qq.com
|
|
5
|
+
Author : shenshuo
|
|
6
|
+
Date : 2018/11/26
|
|
7
|
+
Desc : Redis连接
|
|
8
|
+
"""
|
|
9
|
+
import threading
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
import redis
|
|
13
|
+
from .consts import const
|
|
14
|
+
from .configs import configs
|
|
15
|
+
|
|
16
|
+
_cache_conns: Dict[str, redis.Redis] = {}
|
|
17
|
+
_init_lock = threading.Lock()
|
|
18
|
+
|
|
19
|
+
# 固定默认值
|
|
20
|
+
_MAX_CONNECTIONS: int = 200
|
|
21
|
+
_POOL_TIMEOUT: float = 5.0
|
|
22
|
+
_SOCKET_CONNECT_TIMEOUT: float = 2.0
|
|
23
|
+
_SOCKET_TIMEOUT: float = 3.0
|
|
24
|
+
_RETRY_ON_TIMEOUT: bool = True
|
|
25
|
+
_HEALTH_CHECK_INTERVAL: int = 30
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _validate_basic_types(key: Optional[str], db: Optional[int]) -> None:
|
|
29
|
+
if key is not None and not isinstance(key, str):
|
|
30
|
+
raise TypeError(f"key 必须为 str 或 None,当前类型为 {type(key).__name__}")
|
|
31
|
+
if db is not None and not isinstance(db, int):
|
|
32
|
+
raise TypeError(f"db 必须为 int 或 None,当前类型为 {type(db).__name__}")
|
|
33
|
+
# 可选:校验 db 合法范围(通常 0-15,取决于服务端配置)
|
|
34
|
+
if isinstance(db, int) and db < 0:
|
|
35
|
+
raise ValueError(f"db 不能为负数,当前值为 {db}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _extract_and_validate_cfg(key: str, db: Optional[int]) -> tuple[str, int, Optional[str], bool, bool, int]:
|
|
39
|
+
# 读取配置字典并进行存在性校验
|
|
40
|
+
redis_configs = configs[const.REDIS_CONFIG_ITEM]
|
|
41
|
+
if key not in redis_configs:
|
|
42
|
+
raise ValueError(f"Redis配置键 '{key}' 不存在")
|
|
43
|
+
cfg = redis_configs[key]
|
|
44
|
+
|
|
45
|
+
# 逐项提取并校验类型
|
|
46
|
+
host = cfg[const.RD_HOST_KEY]
|
|
47
|
+
port = cfg[const.RD_PORT_KEY]
|
|
48
|
+
password = cfg.get(const.RD_PASSWORD_KEY)
|
|
49
|
+
auth = cfg.get(const.RD_AUTH_KEY, bool(password))
|
|
50
|
+
decode_responses = cfg.get(const.RD_DECODE_RESPONSES, False)
|
|
51
|
+
db_num = db if db is not None else cfg[const.RD_DB_KEY]
|
|
52
|
+
|
|
53
|
+
if not isinstance(host, str) or not host:
|
|
54
|
+
raise TypeError(f"RD_HOST 必须为非空字符串,当前: {host!r}")
|
|
55
|
+
if not isinstance(port, int) or port <= 0:
|
|
56
|
+
raise TypeError(f"RD_PORT 必须为正整数,当前: {port!r}")
|
|
57
|
+
if password is not None and not isinstance(password, str):
|
|
58
|
+
raise TypeError(f"RD_PASSWORD 必须为 str 或 None,当前: {type(password).__name__}")
|
|
59
|
+
if not isinstance(auth, bool):
|
|
60
|
+
raise TypeError(f"RD_AUTH 必须为 bool,当前: {type(auth).__name__}")
|
|
61
|
+
if not isinstance(decode_responses, bool):
|
|
62
|
+
raise TypeError(f"RD_DECODE_RESPONSES 必须为 bool,当前: {type(decode_responses).__name__}")
|
|
63
|
+
if not isinstance(db_num, int) or db_num < 0:
|
|
64
|
+
raise TypeError(f"RD_DB 必须为非负整数,当前: {db_num!r}")
|
|
65
|
+
|
|
66
|
+
return host, port, password, auth, decode_responses, db_num
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _build_conn(
|
|
70
|
+
host: str,
|
|
71
|
+
port: int,
|
|
72
|
+
db_num: int,
|
|
73
|
+
password: Optional[str],
|
|
74
|
+
auth: bool,
|
|
75
|
+
decode_responses: bool,
|
|
76
|
+
cache_key: str,
|
|
77
|
+
) -> redis.Redis:
|
|
78
|
+
# 快速路径(无锁)
|
|
79
|
+
conn = _cache_conns.get(cache_key)
|
|
80
|
+
if conn is not None:
|
|
81
|
+
return conn
|
|
82
|
+
|
|
83
|
+
# 慢路径:加锁 + 双重检查
|
|
84
|
+
with _init_lock:
|
|
85
|
+
conn = _cache_conns.get(cache_key)
|
|
86
|
+
if conn is not None:
|
|
87
|
+
return conn
|
|
88
|
+
|
|
89
|
+
# 认证处理
|
|
90
|
+
if isinstance(password, str) and password == "":
|
|
91
|
+
password = None
|
|
92
|
+
if not auth and password:
|
|
93
|
+
auth = True
|
|
94
|
+
if auth and not password:
|
|
95
|
+
raise ValueError("Redis要求认证但未提供有效密码")
|
|
96
|
+
|
|
97
|
+
# 阻塞连接池 + 固定默认值
|
|
98
|
+
pool = redis.BlockingConnectionPool(
|
|
99
|
+
host=host,
|
|
100
|
+
port=port,
|
|
101
|
+
db=db_num,
|
|
102
|
+
password=password if auth else None,
|
|
103
|
+
decode_responses=decode_responses,
|
|
104
|
+
max_connections=_MAX_CONNECTIONS,
|
|
105
|
+
timeout=_POOL_TIMEOUT,
|
|
106
|
+
socket_connect_timeout=_SOCKET_CONNECT_TIMEOUT,
|
|
107
|
+
socket_timeout=_SOCKET_TIMEOUT,
|
|
108
|
+
retry_on_timeout=_RETRY_ON_TIMEOUT,
|
|
109
|
+
health_check_interval=_HEALTH_CHECK_INTERVAL,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
conn = redis.Redis(connection_pool=pool)
|
|
113
|
+
_cache_conns[cache_key] = conn
|
|
114
|
+
return conn
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cache_conn(key: Optional[str] = None, db: Optional[int] = None) -> redis.Redis:
|
|
118
|
+
"""
|
|
119
|
+
获取 Redis 客户端(遵循配置中的 RD_DECODE_RESPONSES)
|
|
120
|
+
- 老代码兼容:如果配置是 False,则返回 bytes;True 则返回 str
|
|
121
|
+
"""
|
|
122
|
+
_validate_basic_types(key, db)
|
|
123
|
+
|
|
124
|
+
# 默认 key
|
|
125
|
+
if not key:
|
|
126
|
+
from .consts import const as _const
|
|
127
|
+
key = _const.DEFAULT_RD_KEY
|
|
128
|
+
|
|
129
|
+
host, port, password, auth, decode_responses, db_num = _extract_and_validate_cfg(key, db)
|
|
130
|
+
cache_key = f"{key}:{db_num}"
|
|
131
|
+
return _build_conn(host, port, db_num, password, auth, decode_responses, cache_key)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cache_conn_decode(key: Optional[str] = None, db: Optional[int] = None) -> redis.Redis:
|
|
135
|
+
"""
|
|
136
|
+
获取 Redis 客户端(强制 decode_responses=True)
|
|
137
|
+
- 所有返回值为 str
|
|
138
|
+
- 与 cache_conn 共存,使用独立的缓存键后缀,避免类型混用
|
|
139
|
+
"""
|
|
140
|
+
_validate_basic_types(key, db)
|
|
141
|
+
|
|
142
|
+
# 默认 key
|
|
143
|
+
if not key:
|
|
144
|
+
from .consts import const as _const
|
|
145
|
+
key = _const.DEFAULT_RD_KEY
|
|
146
|
+
|
|
147
|
+
host, port, password, auth, _decode_responses_cfg, db_num = _extract_and_validate_cfg(key, db)
|
|
148
|
+
cache_key = f"{key}:{db_num}:decode"
|
|
149
|
+
# 强制字符串返回
|
|
150
|
+
return _build_conn(host, port, db_num, password, auth, True, cache_key)
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8-*-
|
|
3
|
+
"""
|
|
4
|
+
Author : ming
|
|
5
|
+
date : 2017/3/3 下午9:31
|
|
6
|
+
role : rabbitMQ 操作类
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import traceback
|
|
11
|
+
import pika
|
|
12
|
+
import time
|
|
13
|
+
import json
|
|
14
|
+
from typing import Optional, Tuple, Union, List
|
|
15
|
+
|
|
16
|
+
from pika.exceptions import AMQPConnectionError, ChannelClosedByBroker
|
|
17
|
+
from .consts import const
|
|
18
|
+
from .configs import configs
|
|
19
|
+
from .error import ConfigError
|
|
20
|
+
|
|
21
|
+
# 配置pika日志级别
|
|
22
|
+
logger = logging.getLogger('pika')
|
|
23
|
+
logger.setLevel(logging.ERROR)
|
|
24
|
+
|
|
25
|
+
# 屏蔽pika诊断日志噪音
|
|
26
|
+
pika_diagnostic_logger = logging.getLogger('pika.diagnostic_utils')
|
|
27
|
+
pika_diagnostic_logger.setLevel(logging.CRITICAL)
|
|
28
|
+
|
|
29
|
+
# 屏蔽pika适配器日志噪音
|
|
30
|
+
pika_adapter_logger = logging.getLogger('pika.adapters')
|
|
31
|
+
pika_adapter_logger.setLevel(logging.CRITICAL)
|
|
32
|
+
JsonOrBytes = Union[bytes, bytearray, str, dict, list]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MessageQueueBase:
|
|
36
|
+
def __init__(self, exchange, exchange_type, routing_key="", routing_keys=None,
|
|
37
|
+
queue_name="", no_ack=False, mq_key=const.DEFAULT_MQ_KEY):
|
|
38
|
+
|
|
39
|
+
mq_config = configs[const.MQ_CONFIG_ITEM].get(mq_key)
|
|
40
|
+
if not mq_config:
|
|
41
|
+
raise ConfigError(f"MQ config {mq_key} not found")
|
|
42
|
+
|
|
43
|
+
required_keys = [const.MQ_ADDR, const.MQ_PORT, const.MQ_VHOST, const.MQ_USER, const.MQ_PWD]
|
|
44
|
+
for k in required_keys:
|
|
45
|
+
if k not in mq_config:
|
|
46
|
+
raise ConfigError(k)
|
|
47
|
+
|
|
48
|
+
self.addr = mq_config[const.MQ_ADDR]
|
|
49
|
+
self.port = int(mq_config[const.MQ_PORT])
|
|
50
|
+
self.vhost = mq_config[const.MQ_VHOST]
|
|
51
|
+
self.user = mq_config[const.MQ_USER]
|
|
52
|
+
self.pwd = mq_config[const.MQ_PWD]
|
|
53
|
+
|
|
54
|
+
self.exchange = exchange
|
|
55
|
+
self.exchange_type = exchange_type
|
|
56
|
+
self.routing_key = routing_key
|
|
57
|
+
self.routing_keys = routing_keys or []
|
|
58
|
+
self.queue_name = queue_name
|
|
59
|
+
self.no_ack = no_ack
|
|
60
|
+
|
|
61
|
+
self.connection = None
|
|
62
|
+
self.channel = None
|
|
63
|
+
|
|
64
|
+
# ========== Context Manager ==========
|
|
65
|
+
def __enter__(self):
|
|
66
|
+
self.channel = self.create_channel()
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
70
|
+
self.close()
|
|
71
|
+
|
|
72
|
+
# ========== Connection / Channel ==========
|
|
73
|
+
def create_channel(self):
|
|
74
|
+
credentials = pika.PlainCredentials(self.user, self.pwd)
|
|
75
|
+
self.connection = pika.BlockingConnection(
|
|
76
|
+
pika.ConnectionParameters(self.addr, self.port, self.vhost, credentials=credentials)
|
|
77
|
+
)
|
|
78
|
+
self.channel = self.connection.channel()
|
|
79
|
+
return self.channel
|
|
80
|
+
|
|
81
|
+
def close(self):
|
|
82
|
+
if self.connection and not self.connection.is_closed:
|
|
83
|
+
self.connection.close()
|
|
84
|
+
|
|
85
|
+
def new_channel(self):
|
|
86
|
+
self.channel = self.create_channel()
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
# ========== Consume ==========
|
|
90
|
+
def start_consuming(self, exchange_durable=False):
|
|
91
|
+
ch = self.create_channel()
|
|
92
|
+
self._declare_exchange(ch, exchange_durable)
|
|
93
|
+
queue_name = self._declare_and_bind_queue(ch)
|
|
94
|
+
|
|
95
|
+
ch.basic_qos(prefetch_count=1)
|
|
96
|
+
ch.basic_consume(queue=queue_name, on_message_callback=self.call_back, auto_ack=self.no_ack)
|
|
97
|
+
logger.info(f"[*] Queue {queue_name} started.")
|
|
98
|
+
ch.start_consuming()
|
|
99
|
+
|
|
100
|
+
def call_back(self, ch, method, properties, body):
|
|
101
|
+
try:
|
|
102
|
+
logger.info("Received message")
|
|
103
|
+
self.on_message(body)
|
|
104
|
+
if not self.no_ack:
|
|
105
|
+
ch.basic_ack(delivery_tag=method.delivery_tag)
|
|
106
|
+
except Exception:
|
|
107
|
+
logger.error(traceback.format_exc())
|
|
108
|
+
if not self.no_ack:
|
|
109
|
+
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
|
|
110
|
+
|
|
111
|
+
def on_message(self, body):
|
|
112
|
+
"""子类实现具体逻辑"""
|
|
113
|
+
raise NotImplementedError
|
|
114
|
+
|
|
115
|
+
# ========== Publish ==========
|
|
116
|
+
def publish_message(self, body, durable=True, exchange_durable=False):
|
|
117
|
+
if not self.channel:
|
|
118
|
+
self.create_channel()
|
|
119
|
+
|
|
120
|
+
self._declare_exchange(self.channel, exchange_durable)
|
|
121
|
+
queue_name = self._declare_and_bind_queue(self.channel)
|
|
122
|
+
|
|
123
|
+
props = pika.BasicProperties(delivery_mode=2) if durable else None
|
|
124
|
+
self.channel.basic_publish(
|
|
125
|
+
exchange=self.exchange,
|
|
126
|
+
routing_key=self.routing_key,
|
|
127
|
+
body=body,
|
|
128
|
+
properties=props,
|
|
129
|
+
)
|
|
130
|
+
logger.info(f"Publish message success: {body} -> queue {queue_name}")
|
|
131
|
+
|
|
132
|
+
# ========== Private Helpers ==========
|
|
133
|
+
def _declare_exchange(self, ch, exchange_durable):
|
|
134
|
+
ch.exchange_declare(exchange=self.exchange,
|
|
135
|
+
exchange_type=self.exchange_type,
|
|
136
|
+
durable=exchange_durable)
|
|
137
|
+
|
|
138
|
+
def _declare_and_bind_queue(self, ch):
|
|
139
|
+
if self.queue_name:
|
|
140
|
+
result = ch.queue_declare(queue=self.queue_name, durable=True)
|
|
141
|
+
else:
|
|
142
|
+
result = ch.queue_declare(queue="", exclusive=True, auto_delete=True)
|
|
143
|
+
|
|
144
|
+
queue_name = result.method.queue
|
|
145
|
+
if self.routing_keys:
|
|
146
|
+
for binding_key in self.routing_keys:
|
|
147
|
+
ch.queue_bind(exchange=self.exchange, queue=queue_name, routing_key=binding_key)
|
|
148
|
+
else:
|
|
149
|
+
ch.queue_bind(exchange=self.exchange, queue=queue_name, routing_key=self.routing_key)
|
|
150
|
+
return queue_name
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class MessageQueueBaseV2:
|
|
154
|
+
"""
|
|
155
|
+
消息队列基类:集成消费者和生产者功能。
|
|
156
|
+
线程模型:推荐每个线程创建独立实例,不要跨线程共享(BlockingConnection 非线程安全)。
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(self, exchange: str, exchange_type: str = "direct", routing_key: str = "",
|
|
160
|
+
routing_keys: Optional[List[str]] = None, queue_name: str = "",
|
|
161
|
+
no_ack: bool = False, mq_key: str = const.DEFAULT_MQ_KEY):
|
|
162
|
+
|
|
163
|
+
# 读取配置
|
|
164
|
+
mq_config = configs[const.MQ_CONFIG_ITEM].get(mq_key)
|
|
165
|
+
if not mq_config:
|
|
166
|
+
raise ConfigError(f"MQ config {mq_key} not found")
|
|
167
|
+
|
|
168
|
+
required_keys = [const.MQ_ADDR, const.MQ_PORT, const.MQ_VHOST, const.MQ_USER, const.MQ_PWD]
|
|
169
|
+
for k in required_keys:
|
|
170
|
+
if k not in mq_config:
|
|
171
|
+
raise ConfigError(k)
|
|
172
|
+
|
|
173
|
+
self.addr = mq_config[const.MQ_ADDR]
|
|
174
|
+
self.port = int(mq_config[const.MQ_PORT])
|
|
175
|
+
self.vhost = mq_config[const.MQ_VHOST]
|
|
176
|
+
self.user = mq_config[const.MQ_USER]
|
|
177
|
+
self.pwd = mq_config[const.MQ_PWD]
|
|
178
|
+
|
|
179
|
+
# 拓扑与消费参数
|
|
180
|
+
self.exchange = exchange
|
|
181
|
+
self.exchange_type = exchange_type
|
|
182
|
+
self.routing_key = routing_key
|
|
183
|
+
self.routing_keys = routing_keys or []
|
|
184
|
+
self.queue_name = queue_name
|
|
185
|
+
self.no_ack = no_ack
|
|
186
|
+
|
|
187
|
+
# 连接/通道
|
|
188
|
+
self.connection: Optional[pika.BlockingConnection] = None
|
|
189
|
+
self.channel: Optional[pika.adapters.blocking_connection.BlockingChannel] = None
|
|
190
|
+
|
|
191
|
+
# 状态
|
|
192
|
+
self._returned = []
|
|
193
|
+
self._callbacks_ready = False
|
|
194
|
+
self._blocked = False
|
|
195
|
+
|
|
196
|
+
# 统一的连接参数(避免重复创建)
|
|
197
|
+
self._conn_params = pika.ConnectionParameters(
|
|
198
|
+
host=self.addr,
|
|
199
|
+
port=self.port,
|
|
200
|
+
virtual_host=self.vhost,
|
|
201
|
+
credentials=pika.PlainCredentials(self.user, self.pwd),
|
|
202
|
+
heartbeat=30,
|
|
203
|
+
blocked_connection_timeout=300,
|
|
204
|
+
connection_attempts=3,
|
|
205
|
+
retry_delay=2.0,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# ========== Context Manager ==========
|
|
209
|
+
def __enter__(self):
|
|
210
|
+
self._ensure_conn_chan(need_confirm=False)
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
214
|
+
self.close()
|
|
215
|
+
|
|
216
|
+
# ========== 连接/通道管理 ==========
|
|
217
|
+
def _ensure_conn_chan(self, need_confirm: bool = False) -> None:
|
|
218
|
+
"""
|
|
219
|
+
确保连接和通道可用,并按需注册回调和确认机制。
|
|
220
|
+
|
|
221
|
+
参数:
|
|
222
|
+
- need_confirm: 是否需要发布确认(消费时 False,生产时 True)
|
|
223
|
+
"""
|
|
224
|
+
# 确保连接可用
|
|
225
|
+
if not (self.connection and self.connection.is_open):
|
|
226
|
+
self.connection = pika.BlockingConnection(self._conn_params)
|
|
227
|
+
self.connection.add_on_connection_blocked_callback(
|
|
228
|
+
lambda c, r: self._on_blocked(r)
|
|
229
|
+
)
|
|
230
|
+
self.connection.add_on_connection_unblocked_callback(
|
|
231
|
+
lambda c: self._on_unblocked()
|
|
232
|
+
)
|
|
233
|
+
self._callbacks_ready = False
|
|
234
|
+
|
|
235
|
+
# 确保通道可用
|
|
236
|
+
if not (self.channel and self.channel.is_open):
|
|
237
|
+
self.channel = self.connection.channel()
|
|
238
|
+
self._callbacks_ready = False
|
|
239
|
+
|
|
240
|
+
# 按需注册发布确认和 return 回调(仅注册一次)
|
|
241
|
+
if need_confirm and not self._callbacks_ready:
|
|
242
|
+
self.channel.confirm_delivery()
|
|
243
|
+
self._returned.clear()
|
|
244
|
+
self.channel.add_on_return_callback(
|
|
245
|
+
lambda ch, m, p, b: self._returned.append((m, p, b))
|
|
246
|
+
)
|
|
247
|
+
self._callbacks_ready = True
|
|
248
|
+
|
|
249
|
+
def _on_blocked(self, reason: str):
|
|
250
|
+
"""broker 阻塞回调(内存/磁盘告警)"""
|
|
251
|
+
self._blocked = True
|
|
252
|
+
logger.warning(f"Connection blocked: {reason}")
|
|
253
|
+
|
|
254
|
+
def _on_unblocked(self):
|
|
255
|
+
"""broker 解除阻塞回调"""
|
|
256
|
+
self._blocked = False
|
|
257
|
+
logger.info("Connection unblocked")
|
|
258
|
+
|
|
259
|
+
def _close_resources(self) -> None:
|
|
260
|
+
"""关闭连接和通道(先关通道,再关连接)"""
|
|
261
|
+
# 关闭通道
|
|
262
|
+
if self.channel:
|
|
263
|
+
try:
|
|
264
|
+
if self.channel.is_open:
|
|
265
|
+
self.channel.close()
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
finally:
|
|
269
|
+
self.channel = None
|
|
270
|
+
|
|
271
|
+
# 关闭连接
|
|
272
|
+
if self.connection:
|
|
273
|
+
try:
|
|
274
|
+
if self.connection.is_open:
|
|
275
|
+
self.connection.close()
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
finally:
|
|
279
|
+
self.connection = None
|
|
280
|
+
|
|
281
|
+
# 重置状态
|
|
282
|
+
self._callbacks_ready = False
|
|
283
|
+
self._returned.clear()
|
|
284
|
+
|
|
285
|
+
def close(self):
|
|
286
|
+
"""关闭连接和通道"""
|
|
287
|
+
self._close_resources()
|
|
288
|
+
|
|
289
|
+
def _reset_connection(self) -> None:
|
|
290
|
+
"""异常时重置连接(同 close)"""
|
|
291
|
+
self._close_resources()
|
|
292
|
+
|
|
293
|
+
# ========== 消费 ==========
|
|
294
|
+
def start_consuming(self, exchange_durable: bool = False):
|
|
295
|
+
"""开始消费消息"""
|
|
296
|
+
self._ensure_conn_chan(need_confirm=False)
|
|
297
|
+
self._declare_exchange(self.channel, exchange_durable)
|
|
298
|
+
queue_name = self._declare_and_bind_queue(self.channel)
|
|
299
|
+
|
|
300
|
+
self.channel.basic_qos(prefetch_count=1)
|
|
301
|
+
self.channel.basic_consume(queue=queue_name, on_message_callback=self._callback,
|
|
302
|
+
auto_ack=self.no_ack)
|
|
303
|
+
logger.info(f"[*] Queue {queue_name} started")
|
|
304
|
+
self.channel.start_consuming()
|
|
305
|
+
|
|
306
|
+
def _callback(self, ch, method, properties, body):
|
|
307
|
+
"""消息回调处理"""
|
|
308
|
+
try:
|
|
309
|
+
self.on_message(body)
|
|
310
|
+
if not self.no_ack:
|
|
311
|
+
ch.basic_ack(delivery_tag=method.delivery_tag)
|
|
312
|
+
except Exception:
|
|
313
|
+
logger.error("on_message error:\n%s", traceback.format_exc())
|
|
314
|
+
if not self.no_ack:
|
|
315
|
+
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
|
|
316
|
+
|
|
317
|
+
def on_message(self, body: bytes):
|
|
318
|
+
"""子类实现具体逻辑"""
|
|
319
|
+
raise NotImplementedError
|
|
320
|
+
|
|
321
|
+
# ========== 生产 ==========
|
|
322
|
+
@staticmethod
|
|
323
|
+
def _encode_body(body: JsonOrBytes, json_encode: bool) -> Tuple[bytes, str]:
|
|
324
|
+
"""
|
|
325
|
+
编码消息体并推断 content_type。
|
|
326
|
+
|
|
327
|
+
返回:(payload, content_type)
|
|
328
|
+
"""
|
|
329
|
+
if isinstance(body, (bytes, bytearray)):
|
|
330
|
+
return bytes(body), "application/octet-stream"
|
|
331
|
+
if isinstance(body, str):
|
|
332
|
+
return body.encode("utf-8"), "text/plain"
|
|
333
|
+
if json_encode:
|
|
334
|
+
return json.dumps(body, ensure_ascii=False).encode("utf-8"), "application/json"
|
|
335
|
+
raise TypeError(f"body type {type(body)} not supported")
|
|
336
|
+
|
|
337
|
+
def publish(self, exchange: str, routing_key: str, body: JsonOrBytes,
|
|
338
|
+
durable: bool = True, json_encode: bool = True, mandatory: bool = True,
|
|
339
|
+
max_retries: int = 6) -> None:
|
|
340
|
+
"""
|
|
341
|
+
发送消息(可靠性:发布确认 + mandatory + return 机制)。
|
|
342
|
+
|
|
343
|
+
参数:
|
|
344
|
+
- exchange: 交换机名
|
|
345
|
+
- routing_key: 路由键
|
|
346
|
+
- body: 消息体 (str/bytes/dict/list)
|
|
347
|
+
- durable: 是否持久化(默认 True)
|
|
348
|
+
- json_encode: 是否 JSON 编码(默认 True)
|
|
349
|
+
- mandatory: 是否强制要求有接收方(默认 True,无队列绑定时返回)
|
|
350
|
+
- max_retries: 最大重试次数(默认 6)
|
|
351
|
+
"""
|
|
352
|
+
# 检查 broker 是否阻塞
|
|
353
|
+
if self._blocked:
|
|
354
|
+
logger.warning("Broker is blocked, retrying may be needed")
|
|
355
|
+
|
|
356
|
+
# 编码消息
|
|
357
|
+
payload, actual_content_type = self._encode_body(body, json_encode)
|
|
358
|
+
props = pika.BasicProperties(
|
|
359
|
+
delivery_mode=2 if durable else 1, # 2=持久化, 1=临时
|
|
360
|
+
content_type=actual_content_type,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
last_err = None
|
|
364
|
+
for attempt in range(1, max_retries + 1):
|
|
365
|
+
try:
|
|
366
|
+
# 确保连接和通道可用(需要确认机制)
|
|
367
|
+
self._ensure_conn_chan(need_confirm=True)
|
|
368
|
+
|
|
369
|
+
# 清空返回列表
|
|
370
|
+
self._returned.clear()
|
|
371
|
+
|
|
372
|
+
# 发送消息
|
|
373
|
+
self.channel.basic_publish(
|
|
374
|
+
exchange=exchange,
|
|
375
|
+
routing_key=routing_key,
|
|
376
|
+
body=payload,
|
|
377
|
+
properties=props,
|
|
378
|
+
mandatory=mandatory
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# 检查消息是否被返回(无队列绑定)
|
|
382
|
+
if self._returned:
|
|
383
|
+
raise RuntimeError(f"Message unroutable: no binding for routing_key={routing_key}")
|
|
384
|
+
|
|
385
|
+
# logger.info(f"Message published: {exchange}/{routing_key}")
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
except (AMQPConnectionError, ChannelClosedByBroker) as e:
|
|
389
|
+
# 网络错误 → 重试
|
|
390
|
+
last_err = e
|
|
391
|
+
logger.warning(f"Connection error on attempt {attempt}/{max_retries}: {e}")
|
|
392
|
+
self._reset_connection()
|
|
393
|
+
if attempt < max_retries:
|
|
394
|
+
time.sleep(min(2 ** attempt, 5)) # 指数退避
|
|
395
|
+
|
|
396
|
+
except RuntimeError as e:
|
|
397
|
+
# 业务错误(不可路由等)→ 不重试
|
|
398
|
+
logger.error(f"Publish error: {e}")
|
|
399
|
+
raise
|
|
400
|
+
|
|
401
|
+
except Exception as e:
|
|
402
|
+
# 其他异常 → 重试
|
|
403
|
+
last_err = e
|
|
404
|
+
logger.error(f"Unexpected error on attempt {attempt}/{max_retries}: {e}")
|
|
405
|
+
self._reset_connection()
|
|
406
|
+
if attempt < max_retries:
|
|
407
|
+
time.sleep(min(2 ** attempt, 5))
|
|
408
|
+
|
|
409
|
+
raise last_err or RuntimeError("Unknown publish error")
|
|
410
|
+
|
|
411
|
+
# ========== 私有辅助方法 ==========
|
|
412
|
+
def _declare_exchange(self, ch, exchange_durable: bool):
|
|
413
|
+
"""声明交换机"""
|
|
414
|
+
ch.exchange_declare(exchange=self.exchange,
|
|
415
|
+
exchange_type=self.exchange_type,
|
|
416
|
+
durable=exchange_durable)
|
|
417
|
+
|
|
418
|
+
def _declare_and_bind_queue(self, ch):
|
|
419
|
+
"""声明队列并绑定"""
|
|
420
|
+
# 声明队列
|
|
421
|
+
if self.queue_name:
|
|
422
|
+
result = ch.queue_declare(queue=self.queue_name, durable=True)
|
|
423
|
+
else:
|
|
424
|
+
result = ch.queue_declare(queue="", exclusive=True, auto_delete=True)
|
|
425
|
+
|
|
426
|
+
queue_name = result.method.queue
|
|
427
|
+
|
|
428
|
+
# 绑定队列
|
|
429
|
+
if self.routing_keys:
|
|
430
|
+
for rk in self.routing_keys:
|
|
431
|
+
ch.queue_bind(exchange=self.exchange, queue=queue_name, routing_key=rk)
|
|
432
|
+
else:
|
|
433
|
+
ch.queue_bind(exchange=self.exchange, queue=queue_name, routing_key=self.routing_key)
|
|
434
|
+
|
|
435
|
+
return queue_name
|
|
@@ -48,6 +48,7 @@ def paginate(query, order_by: str = None, **query_params):
|
|
|
48
48
|
else:
|
|
49
49
|
items = query.all() if page_size >= 200 else query.limit(page_size).offset((page - 1) * page_size).all()
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
# 计算总数:去掉排序避免 filesort
|
|
52
|
+
total = query.order_by(None).count()
|
|
52
53
|
if not items_not_to_list: items = queryset_to_list(items)
|
|
53
54
|
return Page(items, page, page_size, total)
|
codosdk-1.0.54/README.md
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
## 安装
|
|
2
|
-
|
|
3
|
-
### python3安装
|
|
4
|
-
|
|
5
|
-
[python链接](https://www.python.org/)
|
|
6
|
-
|
|
7
|
-
##### python3.9以上
|
|
8
|
-
|
|
9
|
-
##### SDK 安装
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
$ pip3 install -U git+https://github.com/ss1917/ops_sdk.git
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## 结构
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
├── README.md 项目readme
|
|
19
|
-
└── opssdk
|
|
20
|
-
├── logs 日志模块
|
|
21
|
-
├── install 安装模块
|
|
22
|
-
├── get_info 配置获取
|
|
23
|
-
└── operate 运维操作
|
|
24
|
-
├── check 系统参数检查和获取
|
|
25
|
-
├── mysql mysql 操作
|
|
26
|
-
├── mail 发送邮件
|
|
27
|
-
└── centralization 集中化管理工具 salt
|
|
28
|
-
├── websdk2 web开发使用
|
|
29
|
-
├── application.py tornado application
|
|
30
|
-
├── base_handler.py tornado 基类
|
|
31
|
-
├── cache.py 处理redis缓存
|
|
32
|
-
├── configs.py 配置文件管理
|
|
33
|
-
├── consts.py 常量
|
|
34
|
-
├── db_context.py MySQL 处理类
|
|
35
|
-
├── error.py 异常
|
|
36
|
-
├── crud_utils.py API CRUD类
|
|
37
|
-
├── model_utils.py 数据库模型处理类 sqlalchemy_pagination
|
|
38
|
-
├── sqlalchemy_pagination.py 分页
|
|
39
|
-
├── fetch_coroutine.py
|
|
40
|
-
├── jwt_token.py JWT处理
|
|
41
|
-
├── mqhelper.py MQ 处理类
|
|
42
|
-
├── program.py
|
|
43
|
-
├── salt_api.py salt 处理类 可以移到工具类
|
|
44
|
-
├── ldap.py LDAP 处理
|
|
45
|
-
├── sms.py 发送短信 可以移到工具类
|
|
46
|
-
├── tools.py 工具类
|
|
47
|
-
├── clent.py API调用客户端封装
|
|
48
|
-
└── apis API集合
|
|
49
|
-
├── mgv4_apis 后台API集合
|
|
50
|
-
├── cmdb_apis 配置平台API
|
|
51
|
-
├── agent_apis Agent
|
|
52
|
-
├── kerrigan_apis 配置文件管理
|
|
53
|
-
└── notice_apis 待补充
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## License
|
|
57
|
-
|
|
58
|
-
Everything is [GPL v3.0](https://www.gnu.org/licenses/gpl-3.0.html).
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
Contact : 191715030@qq.com
|
|
5
|
-
Author : shenshuo
|
|
6
|
-
Date : 2018/11/26
|
|
7
|
-
Desc :
|
|
8
|
-
"""
|
|
9
|
-
import redis
|
|
10
|
-
from .consts import const
|
|
11
|
-
from .configs import configs
|
|
12
|
-
|
|
13
|
-
cache_conns = {}
|
|
14
|
-
|
|
15
|
-
def cache_conn(key=None, db=None):
|
|
16
|
-
redis_configs = configs[const.REDIS_CONFIG_ITEM]
|
|
17
|
-
if not key:
|
|
18
|
-
key = const.DEFAULT_RD_KEY
|
|
19
|
-
for config_key, redis_config in redis_configs.items():
|
|
20
|
-
auth = redis_config[const.RD_AUTH_KEY]
|
|
21
|
-
host = redis_config[const.RD_HOST_KEY]
|
|
22
|
-
port = redis_config[const.RD_PORT_KEY]
|
|
23
|
-
password = redis_config[const.RD_PASSWORD_KEY]
|
|
24
|
-
if db:
|
|
25
|
-
db = db
|
|
26
|
-
else:
|
|
27
|
-
db = redis_config[const.RD_DB_KEY]
|
|
28
|
-
return_utf8 = False
|
|
29
|
-
if const.RD_DECODE_RESPONSES in redis_config:
|
|
30
|
-
return_utf8 = redis_config[const.RD_DECODE_RESPONSES]
|
|
31
|
-
|
|
32
|
-
if auth:
|
|
33
|
-
redis_pool = redis.ConnectionPool(host=host, port=port, db=db, password=password,
|
|
34
|
-
decode_responses=return_utf8)
|
|
35
|
-
else:
|
|
36
|
-
redis_pool = redis.ConnectionPool(host=host, port=port, db=db, decode_responses=return_utf8)
|
|
37
|
-
cache_conns[config_key] = redis.StrictRedis(connection_pool=redis_pool)
|
|
38
|
-
return cache_conns[key]
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# !/usr/bin/env python
|
|
2
|
-
# -*-coding:utf-8-*-
|
|
3
|
-
"""
|
|
4
|
-
Author : ming
|
|
5
|
-
date : 2017/3/3 下午9:31
|
|
6
|
-
role : rabbitMQ 操作类
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import logging
|
|
10
|
-
import traceback
|
|
11
|
-
import pika
|
|
12
|
-
from .consts import const
|
|
13
|
-
from .configs import configs
|
|
14
|
-
from .error import ConfigError
|
|
15
|
-
|
|
16
|
-
# 配置pika日志级别
|
|
17
|
-
logger = logging.getLogger('pika')
|
|
18
|
-
logger.setLevel(logging.ERROR)
|
|
19
|
-
|
|
20
|
-
# 屏蔽pika诊断日志噪音
|
|
21
|
-
pika_diagnostic_logger = logging.getLogger('pika.diagnostic_utils')
|
|
22
|
-
pika_diagnostic_logger.setLevel(logging.CRITICAL)
|
|
23
|
-
|
|
24
|
-
# 屏蔽pika适配器日志噪音
|
|
25
|
-
pika_adapter_logger = logging.getLogger('pika.adapters')
|
|
26
|
-
pika_adapter_logger.setLevel(logging.CRITICAL)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class MessageQueueBase:
|
|
30
|
-
def __init__(self, exchange, exchange_type, routing_key="", routing_keys=None,
|
|
31
|
-
queue_name="", no_ack=False, mq_key=const.DEFAULT_MQ_KEY):
|
|
32
|
-
|
|
33
|
-
mq_config = configs[const.MQ_CONFIG_ITEM].get(mq_key)
|
|
34
|
-
if not mq_config:
|
|
35
|
-
raise ConfigError(f"MQ config {mq_key} not found")
|
|
36
|
-
|
|
37
|
-
required_keys = [const.MQ_ADDR, const.MQ_PORT, const.MQ_VHOST, const.MQ_USER, const.MQ_PWD]
|
|
38
|
-
for k in required_keys:
|
|
39
|
-
if k not in mq_config:
|
|
40
|
-
raise ConfigError(k)
|
|
41
|
-
|
|
42
|
-
self.addr = mq_config[const.MQ_ADDR]
|
|
43
|
-
self.port = int(mq_config[const.MQ_PORT])
|
|
44
|
-
self.vhost = mq_config[const.MQ_VHOST]
|
|
45
|
-
self.user = mq_config[const.MQ_USER]
|
|
46
|
-
self.pwd = mq_config[const.MQ_PWD]
|
|
47
|
-
|
|
48
|
-
self.exchange = exchange
|
|
49
|
-
self.exchange_type = exchange_type
|
|
50
|
-
self.routing_key = routing_key
|
|
51
|
-
self.routing_keys = routing_keys or []
|
|
52
|
-
self.queue_name = queue_name
|
|
53
|
-
self.no_ack = no_ack
|
|
54
|
-
|
|
55
|
-
self.connection = None
|
|
56
|
-
self.channel = None
|
|
57
|
-
|
|
58
|
-
# ========== Context Manager ==========
|
|
59
|
-
def __enter__(self):
|
|
60
|
-
self.channel = self.create_channel()
|
|
61
|
-
return self
|
|
62
|
-
|
|
63
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
64
|
-
self.close()
|
|
65
|
-
|
|
66
|
-
# ========== Connection / Channel ==========
|
|
67
|
-
def create_channel(self):
|
|
68
|
-
credentials = pika.PlainCredentials(self.user, self.pwd)
|
|
69
|
-
self.connection = pika.BlockingConnection(
|
|
70
|
-
pika.ConnectionParameters(self.addr, self.port, self.vhost, credentials=credentials)
|
|
71
|
-
)
|
|
72
|
-
self.channel = self.connection.channel()
|
|
73
|
-
return self.channel
|
|
74
|
-
|
|
75
|
-
def close(self):
|
|
76
|
-
if self.connection and not self.connection.is_closed:
|
|
77
|
-
self.connection.close()
|
|
78
|
-
|
|
79
|
-
def new_channel(self):
|
|
80
|
-
self.channel = self.create_channel()
|
|
81
|
-
return self
|
|
82
|
-
|
|
83
|
-
# ========== Consume ==========
|
|
84
|
-
def start_consuming(self, exchange_durable=False):
|
|
85
|
-
ch = self.create_channel()
|
|
86
|
-
self._declare_exchange(ch, exchange_durable)
|
|
87
|
-
queue_name = self._declare_and_bind_queue(ch)
|
|
88
|
-
|
|
89
|
-
ch.basic_qos(prefetch_count=1)
|
|
90
|
-
ch.basic_consume(queue=queue_name, on_message_callback=self.call_back, auto_ack=self.no_ack)
|
|
91
|
-
logger.info(f"[*] Queue {queue_name} started.")
|
|
92
|
-
ch.start_consuming()
|
|
93
|
-
|
|
94
|
-
def call_back(self, ch, method, properties, body):
|
|
95
|
-
try:
|
|
96
|
-
logger.info("Received message")
|
|
97
|
-
self.on_message(body)
|
|
98
|
-
if not self.no_ack:
|
|
99
|
-
ch.basic_ack(delivery_tag=method.delivery_tag)
|
|
100
|
-
except Exception:
|
|
101
|
-
logger.error(traceback.format_exc())
|
|
102
|
-
if not self.no_ack:
|
|
103
|
-
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
|
|
104
|
-
|
|
105
|
-
def on_message(self, body):
|
|
106
|
-
"""子类实现具体逻辑"""
|
|
107
|
-
raise NotImplementedError
|
|
108
|
-
|
|
109
|
-
# ========== Publish ==========
|
|
110
|
-
def publish_message(self, body, durable=True, exchange_durable=False):
|
|
111
|
-
if not self.channel:
|
|
112
|
-
self.create_channel()
|
|
113
|
-
|
|
114
|
-
self._declare_exchange(self.channel, exchange_durable)
|
|
115
|
-
queue_name = self._declare_and_bind_queue(self.channel)
|
|
116
|
-
|
|
117
|
-
props = pika.BasicProperties(delivery_mode=2) if durable else None
|
|
118
|
-
self.channel.basic_publish(
|
|
119
|
-
exchange=self.exchange,
|
|
120
|
-
routing_key=self.routing_key,
|
|
121
|
-
body=body,
|
|
122
|
-
properties=props,
|
|
123
|
-
)
|
|
124
|
-
logger.info(f"Publish message success: {body} -> queue {queue_name}")
|
|
125
|
-
|
|
126
|
-
# ========== Private Helpers ==========
|
|
127
|
-
def _declare_exchange(self, ch, exchange_durable):
|
|
128
|
-
ch.exchange_declare(exchange=self.exchange,
|
|
129
|
-
exchange_type=self.exchange_type,
|
|
130
|
-
durable=exchange_durable)
|
|
131
|
-
|
|
132
|
-
def _declare_and_bind_queue(self, ch):
|
|
133
|
-
if self.queue_name:
|
|
134
|
-
result = ch.queue_declare(queue=self.queue_name, durable=True)
|
|
135
|
-
else:
|
|
136
|
-
result = ch.queue_declare(queue="", exclusive=True, auto_delete=True)
|
|
137
|
-
|
|
138
|
-
queue_name = result.method.queue
|
|
139
|
-
if self.routing_keys:
|
|
140
|
-
for binding_key in self.routing_keys:
|
|
141
|
-
ch.queue_bind(exchange=self.exchange, queue=queue_name, routing_key=binding_key)
|
|
142
|
-
else:
|
|
143
|
-
ch.queue_bind(exchange=self.exchange, queue=queue_name, routing_key=self.routing_key)
|
|
144
|
-
return queue_name
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|