nosp 0.7.5__py3-none-any.whl

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.
nosp/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .monitor import *
2
+ from .core import *
nosp/cache.py ADDED
@@ -0,0 +1,104 @@
1
+ import sys
2
+ from typing import Optional, Callable, Any, Dict, TypeVar, Generic
3
+
4
+ from loguru import logger
5
+
6
+ from nosp.config import RedisConfig
7
+ from nosp.database import Redis
8
+ from nosp.utils import get_md5
9
+
10
+
11
+ class CacheBaseModel:
12
+ def __init__(self):
13
+ self._on_change: Optional[Callable[[str, str], None]] = None
14
+ self._on_get: Optional[Callable[[str], str]] = None
15
+ self._live = False
16
+
17
+ def __init_subclass__(cls):
18
+ cls._on_change: Optional[Callable[[str, str], None]] = None
19
+ cls._on_get: Optional[Callable[[str], str]] = None
20
+ cls._live = False
21
+
22
+ def on_change(self, callback: Callable[[str, str], None]) -> None:
23
+ """注册变更回调"""
24
+ self._on_change = callback
25
+
26
+ def on_get(self, callback: Callable[[str], str]) -> None:
27
+ """注册Get回调"""
28
+ self._on_get = callback
29
+
30
+ def __setattr__(self, key: str, value: Any):
31
+ super().__setattr__(key, value)
32
+ if not key.startswith("_") and hasattr(self, '_on_change') and self._on_change is not None:
33
+ self._on_change(key, str(value))
34
+
35
+ def __getattribute__(self, key: str) -> Any:
36
+ if key.startswith("_"):
37
+ return super().__getattribute__(key)
38
+
39
+ if hasattr(self, '_live') and self._live and hasattr(self, '_on_get') and self._on_get is not None:
40
+ try:
41
+ result = self._on_get(key)
42
+ if result is None and super().__getattribute__(key) is not None:
43
+ return super().__getattribute__(key)
44
+ return self._on_get(key)
45
+ except Exception as e:
46
+ logger.warning(f"on_get({key}) failed: {e}")
47
+
48
+ return super().__getattribute__(key)
49
+
50
+ def set(self, data: Dict[str, Any]):
51
+ annotations = self.__class__.__annotations__
52
+ for k, v in data.items():
53
+ if k in annotations:
54
+ setattr(self, k, v)
55
+
56
+ def get(self) -> Dict[str, str]:
57
+ return {
58
+ k: str(getattr(self, k))
59
+ for k in self._class__.__annotations__
60
+ if hasattr(self, k)
61
+ }
62
+
63
+
64
+ T = TypeVar('T', bound=CacheBaseModel)
65
+
66
+
67
+ class Cache(Generic[T]):
68
+ def __init__(self, cfg: RedisConfig, db=15, data_type=None, live=False, key=None):
69
+ self.key = key if key else get_md5(sys.modules.get("__main__").__file__)
70
+ self.redis = Redis.simple(cfg, db=db)
71
+ self.data: Optional[T] = None
72
+ if data_type is not None:
73
+ self.data: Optional[T] = data_type()
74
+ self._load()
75
+ self.data._live = live
76
+
77
+ def _save_to_redis(self, key, value):
78
+ """私有方法:保存数据到 Redis"""
79
+ try:
80
+ self.redis.r.hset(self.key, key, value)
81
+ except Exception as e:
82
+ logger.warning(f"Save failed: {e}")
83
+
84
+ def _on_get(self, key: str) -> str:
85
+ return self.redis.r.hget(self.key, key)
86
+
87
+ def _load(self):
88
+ if self.data is None:
89
+ return None
90
+ data = self.redis.r.hgetall(self.key)
91
+ if data and isinstance(data, dict):
92
+ try:
93
+ self.data.set(data)
94
+ except Exception as e:
95
+ logger.warning(f"Load failed: {e}")
96
+ self.data.on_change(self._save_to_redis)
97
+ self.data.on_get(self._on_get)
98
+ return None
99
+
100
+ def get(self, key: str) -> str:
101
+ return self.redis.r.hget(self.key, key)
102
+
103
+ def set(self, key: str, value):
104
+ return self.redis.r.hset(self.key, key, value)
nosp/config.py ADDED
@@ -0,0 +1,34 @@
1
+ class MysqlConfig(object):
2
+ def __init__(self, host: str, port: int, username: str, password: str, db: str, charset: str = 'utf8mb4'):
3
+ self.host = host
4
+ self.port = port
5
+ self.username = username
6
+ self.password = password
7
+ self.db = db
8
+ self.charset = charset
9
+
10
+
11
+ class RedisConfig(object):
12
+ def __init__(self, host: str, port: int, password: str, db: int):
13
+ self.host = host
14
+ self.port = port
15
+ self.password = password
16
+ self.db = db
17
+
18
+
19
+ class RabbitMQConfig(object):
20
+ def __init__(self, host: str, port: int, username: str, password: str, virtual_host: str = '/'):
21
+ self.host = host
22
+ self.port = port
23
+ self.username = username
24
+ self.password = password
25
+ self.virtual_host = virtual_host
26
+
27
+
28
+ class OssConfig(object):
29
+ def __init__(self, access_key: str, access_secret: str, endpoint: str, bucket_name: str, region: str = None):
30
+ self.access_key = access_key
31
+ self.access_secret = access_secret
32
+ self.endpoint = endpoint
33
+ self.bucket_name = bucket_name
34
+ self.region = region
nosp/core.py ADDED
@@ -0,0 +1,41 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+
6
+ def load_path():
7
+ """
8
+ 向上查找 [config.py or pyproject.toml] 所在目录,并将其添加到 sys.path 中。
9
+ """
10
+ current_dir = Path(sys.modules.get("__main__").__file__).resolve().parent
11
+ root_dir = None
12
+ for parent in [current_dir, *current_dir.parents]:
13
+ if (parent / "config.py").exists() or (parent / "pyproject.toml").exists():
14
+ root_dir = parent
15
+ break
16
+ if root_dir is not None:
17
+ root_path = str(root_dir)
18
+ if sys.path[0] != root_path:
19
+ sys.path.insert(0, root_path)
20
+ print(f"[load_path] 添加路径到 sys.path: {root_path}")
21
+ else:
22
+ print("[load_path] 未找到 config.py 或 pyproject.toml,未修改 sys.path。")
23
+
24
+
25
+
26
+ def load_env(filepath='.env'):
27
+ """
28
+ 从指定路径读取 .env 文件,并将变量加载到 os.environ 中。
29
+ :param filepath: .env 文件路径,默认为当前目录下的 .env
30
+ """
31
+ if not os.path.isfile(filepath):
32
+ return
33
+
34
+ with open(filepath, encoding='utf-8') as file:
35
+ for line in file:
36
+ line = line.strip()
37
+ if not line or line.startswith('#'):
38
+ continue
39
+ if '=' in line:
40
+ key, value = line.split('=', 1)
41
+ os.environ[key] = value.strip('"\' ')
nosp/database.py ADDED
@@ -0,0 +1,451 @@
1
+ """
2
+ @author: noybzy
3
+ @time: 2024/9/19 上午1:02
4
+ @file: database.py
5
+ @describe: 数据库操作 mysql,redis
6
+ @updated: 2025/2/14
7
+ """
8
+ import json
9
+ import time
10
+ from typing import Union, TYPE_CHECKING, Optional
11
+
12
+ import pymysql
13
+ import pymysql.cursors
14
+ from dbutils.pooled_db import PooledDB
15
+ from loguru import logger
16
+
17
+ from .config import MysqlConfig, RedisConfig
18
+ from .lazy import LazyLoader
19
+
20
+ if TYPE_CHECKING:
21
+ import redis
22
+ else:
23
+ redis = LazyLoader("redis")
24
+
25
+
26
+ class ConnManager:
27
+ def __init__(self, sql_pool, kv=False):
28
+ self.kv = kv
29
+ self.sql_pool = sql_pool
30
+ self.conn: Union[pymysql.connections.Connection, None] = None
31
+ self.cursor: Union[pymysql.cursors.Cursor, None] = None
32
+
33
+ def __enter__(self) -> 'ConnManager':
34
+ self.conn = self.sql_pool.connection()
35
+ if self.kv:
36
+ self.cursor = self.conn.cursor(cursor=pymysql.cursors.DictCursor)
37
+ else:
38
+ self.cursor = self.conn.cursor()
39
+ return self
40
+
41
+ def __exit__(self, exc_type, exc_val, exc_tb):
42
+ if self.cursor:
43
+ self.cursor.close()
44
+ if self.conn:
45
+ self.conn.close()
46
+
47
+
48
+ class Mysql(object):
49
+ def __init__(self, host: str = '127.0.0.1', username: str = 'root', password: str = '123456', db: str = 'test',
50
+ drop_column: list = None, pool_num: int = 4, max_pool: int = None, port: int = 3306, monitor=None,
51
+ test=False):
52
+ # monitor 历史遗留问题,后续计划删掉
53
+ if drop_column is None:
54
+ drop_column = ["id", "updated", 'created', 'isonline', 'islocal']
55
+ self.host = host
56
+ self.port = port
57
+ self.username = username
58
+ self.password = password
59
+ self.db = db
60
+ self.drop_column = drop_column
61
+ self.pool_num = pool_num
62
+ self.sql_pool = PooledDB(
63
+ creator=pymysql, # 使用链接数据库的模块
64
+ maxconnections=max_pool, # 连接池允许的最大连接数,0和None表示不限制连接数
65
+ mincached=self.pool_num, # 初始化时,连接池中至少创建的空闲的连接,0表示不创建
66
+ # maxcached=5, # 连接池中空闲的最多连接数,0和None表示不限制
67
+ # maxshared=3, # 连接池中最多共享的连接数量,0和None表示全部共享
68
+ blocking=True, # 连接池中如果没有可用连接后,阻塞等待,而不是报错
69
+ # maxusage=None, # 一个连接最多被重复使用的次数,None表示无限制
70
+ # setsession=[], # 开始会话前执行的命令列表
71
+ # ping=0, # ping MySQL服务端,检查服务是否可用
72
+ host=self.host,
73
+ port=self.port,
74
+ user=self.username,
75
+ password=self.password,
76
+ database=self.db,
77
+ charset='utf8mb4'
78
+ )
79
+ self.sql_pool.connection()
80
+ self.table_name_list = self.get_db_name() # 获取所有表名
81
+ self.column_list = {}
82
+ self.test = test
83
+
84
+ @staticmethod
85
+ def simple(config: MysqlConfig, pool_num=4, db: str = None, monitor=None, max_pool: int = None,
86
+ test=False) -> 'Mysql':
87
+ """
88
+ 创建Mysql
89
+ :param max_pool: 最大连接数
90
+ :param config: 连接配置
91
+ :param pool_num: 线程池数量
92
+ :param db: 数据库
93
+ :param monitor: 监控
94
+ :return:
95
+ """
96
+
97
+ if db is None:
98
+ db = config.db
99
+ mysql = Mysql(
100
+ host=config.host,
101
+ port=config.port,
102
+ username=config.username,
103
+ password=config.password,
104
+ db=db,
105
+ pool_num=pool_num,
106
+ max_pool=max_pool,
107
+ test=test
108
+ )
109
+ return mysql
110
+
111
+ def get_db_name(self, db: str = None) -> list:
112
+ """
113
+ 获取指定库的所有表
114
+ :return:
115
+ """
116
+ if db is None:
117
+ db = self.db
118
+
119
+ sql = "select table_name from information_schema.tables where table_schema='{}'".format(db)
120
+ with ConnManager(self.sql_pool) as cm:
121
+ cm.cursor.execute(sql)
122
+ db_list = cm.cursor.fetchall()
123
+ db_list = [i[0] for i in db_list]
124
+ return db_list
125
+
126
+ def get_table_column_list(self, table_name: str) -> dict:
127
+ """
128
+ 获取指定表的所有字段
129
+ :param table_name: 表名
130
+ :return:
131
+ """
132
+ column_list = self.column_list.get(table_name)
133
+ if column_list:
134
+ return column_list
135
+ else:
136
+ sql = 'select column_name,data_type from information_schema.columns where table_name=%s and table_schema=%s'
137
+ with ConnManager(self.sql_pool) as cm:
138
+ cm.cursor.execute(sql, (table_name, self.db))
139
+ column_list = cm.cursor.fetchall()
140
+ insert_columns = {}
141
+ for i in column_list:
142
+ if i[0] in self.drop_column:
143
+ continue
144
+ insert_columns[i[0]] = i[1]
145
+ self.column_list[table_name] = insert_columns
146
+ return insert_columns
147
+
148
+ def execute_sql(self, sql, params=None, kv=True) -> Union[list, int]:
149
+ """
150
+ 执行sql语句
151
+ :param sql: sql代码
152
+ :param params: 参数
153
+ :param kv: 查询结果是否转成字典
154
+ :return: 返回查询结果 | 受影响行数
155
+ """
156
+ try:
157
+ with ConnManager(self.sql_pool, kv=kv) as cm:
158
+ cm.cursor.execute(sql, params)
159
+ if sql.strip().lower().startswith("select"):
160
+ result = cm.cursor.fetchall() # 查询语句返回所有结果
161
+ return result
162
+ else:
163
+ # 非查询语句,返回受影响的行数
164
+ cm.conn.commit()
165
+ return cm.cursor.rowcount
166
+ except Exception as e:
167
+ logger.error(f'执行sql失败:{e.__class__.__name__}|{e.args}>>{sql}, {params}')
168
+ raise e
169
+
170
+ @staticmethod
171
+ def wrapper_value(key, value, insert_columns):
172
+ if value is None:
173
+ if insert_columns.get(key) in ('datetime', 'date', 'time', 'year'):
174
+ return None
175
+ else:
176
+ return ''
177
+ else:
178
+ if isinstance(value, bool):
179
+ return value
180
+ else:
181
+ return str(value)
182
+
183
+ def insert_data(self, item: dict, table_name: str, replace: bool = False) -> int:
184
+ """
185
+ 插入单条数据(默认表里的字段不允许为空[除 'datetime', 'date', 'time', 'year' 字段],['bigint','int', 'decimal' 字段为None时取得默认值] )
186
+ :param item: 数据
187
+ :param table_name: 表名
188
+ :param replace: 是否使用replace,默认insert
189
+ :return: 返回成功的行数
190
+ """
191
+ if item:
192
+ insert_columns = self.get_table_column_list(table_name)
193
+ # 过滤字段,并移除 'int' 或 'decimal' 类型为 None 的字段 (采用数据库默认值)
194
+ filtered_item = {
195
+ k: v for k, v in item.items() if
196
+ k in insert_columns.keys() and not (
197
+ (v is None or v == '') and insert_columns[k] in ('bigint', 'int', 'decimal', 'tinyint'))
198
+ }
199
+ if not filtered_item:
200
+ logger.warning(f"没有找到可插入的字段: {item}")
201
+ return 0
202
+
203
+ insert_columns_new = ['`%s`' % i for i in filtered_item.keys()]
204
+ # 选择 SQL 操作:INSERT 或 REPLACE
205
+ operation = "REPLACE" if replace else "INSERT"
206
+ columns = ', '.join(insert_columns_new)
207
+ placeholders = ', '.join(['%s'] * len(filtered_item))
208
+ sql = f"{operation} INTO {table_name} ({columns}) VALUES ({placeholders})"
209
+
210
+ values_list = []
211
+ for key in filtered_item.keys():
212
+ value = filtered_item.get(key)
213
+ values_list.append(self.wrapper_value(key, value, insert_columns))
214
+ values = tuple(values_list)
215
+
216
+ try:
217
+ with ConnManager(self.sql_pool) as cm:
218
+ cm.cursor.execute(sql, values)
219
+ if self.test:
220
+ cm.conn.rollback()
221
+ logger.warning(f'[TEST MODE] {operation} {table_name} would affect rows: {cm.cursor.rowcount}')
222
+ return 0
223
+ cm.conn.commit() # 提交事务
224
+ logger.success(f'{operation} {table_name} rows:{cm.cursor.rowcount} successfully')
225
+ rowcount = cm.cursor.rowcount
226
+ return rowcount
227
+ except pymysql.IntegrityError as e:
228
+ error_code, error_message = e.args # 获取错误码和错误信息
229
+ if error_code == 1062:
230
+ # 提取唯一键冲突的详细信息
231
+ duplicate_key = error_message.split("for key")[1].strip().replace('\'', '').replace('\"',
232
+ '') if "for key" in error_message else "Unknown key"
233
+ duplicate_value = item.get(duplicate_key.split('.')[-1])
234
+ logger.warning(f"IntegrityError: {duplicate_key}: {duplicate_value}")
235
+ else:
236
+ logger.warning(f"IntegrityError: {e.args}")
237
+ return 0
238
+ except pymysql.ProgrammingError as e:
239
+ logger.error(f'sql 语法错误 {e.__class__.__name__},sql:{sql}')
240
+ raise e
241
+ except Exception as e:
242
+ logger.error(f'insert error {e.__class__.__name__}')
243
+ raise e
244
+
245
+ else:
246
+ logger.warning('插入为空')
247
+
248
+ def insert_data_many(self, item_list: list[dict], table_name: str, batch_size: int = 500, replace: bool = False,
249
+ ignore: bool = False) -> int:
250
+ """
251
+ 批量插入数据(默认表里的字段不允许为空[除 'datetime', 'date', 'time', 'year' 字段],注意['bigint','int', 'decimal' 字段为None时请手动设置默认值] )
252
+ :param item_list: 数据列表
253
+ :param table_name: 表名
254
+ :param batch_size: 批量插入大小
255
+ :param replace: 是否使用replace,默认insert
256
+ :param ignore: 是否忽略掉插入错误
257
+ :return:
258
+ """
259
+ # 批量插入不过滤插入参数
260
+
261
+ if not item_list:
262
+ logger.warning("插入的数据列表为空")
263
+ return 0
264
+
265
+ # 获取表的列名
266
+ insert_columns = self.get_table_column_list(table_name)
267
+ if not insert_columns:
268
+ logger.warning(f"表 {table_name} 的列名列表为空")
269
+ return 0
270
+
271
+ insert_columns_new = ['`%s`' % i for i in insert_columns.keys()]
272
+ # 生成批量插入的 SQL 和数据
273
+ operation = "REPLACE" if replace else "INSERT"
274
+ if ignore:
275
+ operation = operation + ' ' + 'ignore '
276
+ columns = ', '.join(insert_columns_new)
277
+ placeholders = ', '.join(['%s'] * len(insert_columns))
278
+ sql = f"{operation} INTO {table_name} ({columns}) VALUES ({placeholders})"
279
+
280
+ rows = []
281
+ for item in item_list:
282
+ values_list = []
283
+ for key in insert_columns.keys():
284
+ value = item.get(key)
285
+ values_list.append(self.wrapper_value(key, value, insert_columns))
286
+ rows.append(tuple(values_list))
287
+ total_inserted = 0
288
+ try:
289
+ with ConnManager(self.sql_pool) as cm:
290
+ # 批量插入,每次插入 batch_size 条记录
291
+ for i in range(0, len(rows), batch_size):
292
+ batch = rows[i:i + batch_size]
293
+ try:
294
+ cm.cursor.executemany(sql, batch) # 批量插入
295
+ cm.conn.commit() # 提交事务
296
+ inserted_rows = cm.cursor.rowcount
297
+ total_inserted += inserted_rows
298
+ logger.success(f"insert {inserted_rows}/{len(batch)} rows into {table_name} successfully.")
299
+ except pymysql.IntegrityError as e:
300
+ # 捕获批量插入时的唯一键冲突错误
301
+ logger.warning(f"IntegrityError during batch insert,尝试单条插入")
302
+ cm.conn.rollback()
303
+ for item in item_list[i:i + batch_size]:
304
+ try:
305
+ # 逐条插入调用 insert_data 方法
306
+ self.insert_data(item, table_name, replace)
307
+ except Exception as individual_e:
308
+ logger.error(f"insert error during individual inserts: {individual_e}")
309
+ raise individual_e
310
+ except pymysql.ProgrammingError as e:
311
+ logger.error(f"SQL 语法错误: {e}, SQL: {sql}")
312
+ raise e
313
+ except Exception as e:
314
+ logger.error(f"Insert error: {e.__class__.__name__} - {str(e)}")
315
+ raise e
316
+ return total_inserted
317
+
318
+
319
+ class Message(object):
320
+
321
+ def __init__(self, r, queue_name, queue_name_tmp, data):
322
+ self.r = r
323
+ self.queue_name = queue_name
324
+ self.queue_name_tmp = queue_name_tmp
325
+ self.data = data
326
+
327
+ def ack(self):
328
+ self.r.lrem(self.queue_name_tmp, 0, self.data)
329
+
330
+ def nack(self):
331
+ """
332
+ 原子性的否定确认 - 通过Lua脚本保证操作原子性
333
+ """
334
+ lua_script = """
335
+ if redis.call('LREM', KEYS[2], 0, ARGV[1]) > 0 then
336
+ return redis.call('LPUSH', KEYS[1], ARGV[1])
337
+ else
338
+ return 0
339
+ end
340
+ """
341
+ result = self.r.eval(lua_script, 2, self.queue_name, self.queue_name_tmp, self.data)
342
+ if result == 0:
343
+ raise ValueError(f"消息 {self.data} 未在临时队列 {self.queue_name_tmp} 中找到")
344
+
345
+
346
+ class Redis(object):
347
+ def __init__(self, host: str, port: int, password: str, db: int, decode_responses: bool = False):
348
+ self.host = host
349
+ self.port = port
350
+ self.password = password
351
+ self.db = db
352
+ self.r = redis.StrictRedis(host=host, port=port, password=password, db=db, decode_responses=decode_responses)
353
+
354
+ @staticmethod
355
+ def simple(config: RedisConfig, db: int = None, decode_responses: bool = True) -> 'Redis':
356
+ """
357
+ 使用配置创建Redis对象
358
+ :param config: 配置
359
+ :param db: 数据库
360
+ :param decode_responses: 是否自动解析字节
361
+ :return:
362
+ """
363
+ if not db:
364
+ db = config.db
365
+ return Redis(
366
+ host=config.host,
367
+ port=config.port,
368
+ password=config.password,
369
+ db=db,
370
+ decode_responses=decode_responses
371
+ )
372
+
373
+ def flush(self, dst: str):
374
+ """
375
+ 使用Lua脚本原子性转移整个列表内容
376
+ """
377
+ lua_script = """
378
+ local items = redis.call('LRANGE', KEYS[1], 0, -1)
379
+ if #items > 0 then
380
+ redis.call('DEL', KEYS[1])
381
+ return redis.call('LPUSH', KEYS[2], unpack(items))
382
+ end
383
+ return 0
384
+ """
385
+ src = f'{dst}_tmp'
386
+ self.r.eval(lua_script, 2, src, dst)
387
+
388
+ def pop_message(self, queue_name: str, wait: bool = False) -> Optional[Message]:
389
+ temp_queue_name = f'{queue_name}_tmp'
390
+ if not wait:
391
+ data = self.r.rpoplpush(queue_name, temp_queue_name)
392
+ else:
393
+ data = self.r.brpoplpush(queue_name, temp_queue_name)
394
+
395
+ if data:
396
+ return Message(self.r, queue_name, temp_queue_name, data)
397
+
398
+ def push(self, queue_name: str, data: Union[dict, str]):
399
+ if isinstance(data, dict):
400
+ data_str = json.dumps(data, ensure_ascii=False)
401
+ self.r.lpush(queue_name, data_str)
402
+ else:
403
+ self.r.lpush(queue_name, data)
404
+
405
+ def pop(self, queue_name: str, decode_json: bool = False) -> Union[dict, str]:
406
+ data_str = self.r.rpop(queue_name)
407
+ if decode_json:
408
+ return json.loads(data_str)
409
+ return data_str
410
+
411
+ def delay_push(self, queue_name: str, data: Union[dict, str], delay: int = 125):
412
+ """
413
+ 延迟添加数据到队列(配合delay_pop用)
414
+ :param queue_name: 队列名称
415
+ :param data: 数据
416
+ :param delay: 延迟执行时间(单位:秒)
417
+ """
418
+ if isinstance(data, dict):
419
+ data_str = json.dumps(data, ensure_ascii=False)
420
+ else:
421
+ data_str = data
422
+ execute_time = time.time() + delay # 计算任务的执行时间戳
423
+ self.r.zadd(queue_name, {data_str: execute_time})
424
+
425
+ def delay_pop(self, queue_name: str, delay=125, wait: bool = False) -> Union[str, None]:
426
+ """
427
+ 延迟获取队列数据
428
+ :param queue_name: 队列名称
429
+ :param delay: 延迟时间
430
+ :param wait: 是否等待获取
431
+ :return:
432
+ """
433
+ while True:
434
+ current_time = time.time()
435
+ task_info = self.r.zpopmin(queue_name, 1)
436
+ if task_info:
437
+ task, score = task_info[0]
438
+ if score <= current_time:
439
+ new_time = current_time + delay
440
+ self.r.zadd(queue_name, {task: new_time})
441
+ return task
442
+ else:
443
+ self.r.zadd(queue_name, {task: score})
444
+ if wait:
445
+ logger.debug(f'等待队列 {queue_name}')
446
+ time.sleep(5)
447
+ continue
448
+ else:
449
+ return None
450
+ else:
451
+ return None
nosp/err.py ADDED
@@ -0,0 +1,5 @@
1
+ class CustomException(Exception):
2
+ def __init__(self, status, msg=None, data=None):
3
+ self.status = status
4
+ self.msg = msg
5
+ self.data = data