pixelarraylib 1.0.0__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.
arraylib/aliyun/sts.py ADDED
@@ -0,0 +1,124 @@
1
+ import traceback
2
+ import json
3
+ import oss2
4
+ from alibabacloud_sts20150401.client import Client as StsClient
5
+ from alibabacloud_tea_openapi import models as open_api_models
6
+ from alibabacloud_sts20150401 import models as sts_20150401_models
7
+ from alibabacloud_tea_util import models as util_models
8
+ from arraylib.monitor.feishu import Feishu
9
+ from arraylib.db_utils.redis import RedisUtils
10
+
11
+ feishu_alert = Feishu("devtoolkit服务报警")
12
+
13
+
14
+ class STSUtils:
15
+ def __init__(
16
+ self, access_key_id, access_key_secret, role_arn, region_id, bucket_name, redis_utils
17
+ ):
18
+ assert isinstance(redis_utils, RedisUtils), "redis_utils must be a RedisUtils instance"
19
+ self.access_key_id = access_key_id
20
+ self.access_key_secret = access_key_secret
21
+ self.role_arn = role_arn
22
+ # 使用新版本的STS客户端
23
+ config = open_api_models.Config(
24
+ access_key_id=access_key_id,
25
+ access_key_secret=access_key_secret,
26
+ region_id=region_id,
27
+ )
28
+ self.client = StsClient(config)
29
+ self.redis_utils = redis_utils
30
+ self.bucket_name = bucket_name
31
+ self.oss_endpoint = f"https://oss-{region_id}.aliyuncs.com"
32
+ self.region_id = region_id
33
+
34
+ def get_sts_token(self, role_session_name, duration_seconds=3600):
35
+ """
36
+ description:
37
+ 获取STS临时访问凭证
38
+ parameters:
39
+ role_session_name(str): 角色会话名称
40
+ duration_seconds(int): 临时凭证有效期,单位为秒
41
+ return:
42
+ credentials(dict): 临时访问凭证
43
+ flag(bool): 是否成功
44
+ """
45
+ assert role_session_name in ["oss-session"]
46
+ try:
47
+ credentials = self.redis_utils.get(f"sts_auth_token_{role_session_name}")
48
+ if credentials:
49
+ return json.loads(credentials), True
50
+
51
+ assume_role_request = sts_20150401_models.AssumeRoleRequest(
52
+ role_arn=self.role_arn,
53
+ role_session_name=role_session_name,
54
+ duration_seconds=duration_seconds,
55
+ )
56
+
57
+ response = self.client.assume_role_with_options(
58
+ assume_role_request, util_models.RuntimeOptions()
59
+ )
60
+
61
+ credentials = response.to_map()["body"]["Credentials"]
62
+
63
+ access_key_id = credentials["AccessKeyId"]
64
+ access_key_secret = credentials["AccessKeySecret"]
65
+ security_token = credentials["SecurityToken"]
66
+
67
+ credentials = {
68
+ "access_key_id": access_key_id,
69
+ "access_key_secret": access_key_secret,
70
+ "security_token": security_token,
71
+ }
72
+ self.redis_utils.set(
73
+ f"sts_auth_token_{role_session_name}",
74
+ json.dumps(credentials),
75
+ expire_seconds=duration_seconds,
76
+ )
77
+ return credentials, True
78
+ except Exception as e:
79
+ feishu_alert.send(traceback.format_exc())
80
+ return {}, False
81
+
82
+ def get_oss_sts_client(self, endpoint, bucket_name):
83
+ """
84
+ description:
85
+ 获取基于STS的OSS客户端
86
+ return:
87
+ bucket(oss2.Bucket): OSS客户端
88
+ flag(bool): 是否成功
89
+ """
90
+ try:
91
+ credentials, flag = self.get_sts_token("oss-session")
92
+ if not credentials or not flag:
93
+ return None, False
94
+
95
+ auth = oss2.StsAuth(
96
+ credentials["access_key_id"],
97
+ credentials["access_key_secret"],
98
+ credentials["security_token"],
99
+ )
100
+ return oss2.Bucket(auth, endpoint, bucket_name), True
101
+ except Exception as e:
102
+ feishu_alert.send(traceback.format_exc())
103
+ return None, False
104
+
105
+ def generate_presigned_url(self, prefix, expires_in=60 * 60 * 24):
106
+ """
107
+ description:
108
+ 使用STS生成预签名URL
109
+ parameters:
110
+ prefix(str): 前缀
111
+ expires_in(int): 过期时间,默认24小时
112
+ return:
113
+ url(str): 预签名URL
114
+ """
115
+ try:
116
+ sts_oss_client, sts_flag = self.get_oss_sts_client(
117
+ self.oss_endpoint, self.bucket_name
118
+ )
119
+ if not sts_oss_client or not sts_flag:
120
+ return ""
121
+ return sts_oss_client.sign_url("GET", prefix, expires_in)
122
+ except Exception as e:
123
+ feishu_alert.send(traceback.format_exc())
124
+ return ""
@@ -0,0 +1,544 @@
1
+ import traceback
2
+ import pymysql
3
+ import aiomysql
4
+ import asyncio
5
+ from arraylib.monitor.feishu import Feishu
6
+ import time
7
+ from pymysql.err import OperationalError
8
+
9
+ feishu_alert = Feishu("devtoolkit服务报警")
10
+
11
+
12
+ class MysqlUtils:
13
+ def __init__(
14
+ self,
15
+ host,
16
+ database,
17
+ user,
18
+ password,
19
+ port,
20
+ max_retries=3,
21
+ ):
22
+ self.host = host
23
+ self.database = database
24
+ self.user = user
25
+ self.password = password
26
+ self.port = port
27
+ self.max_retries = max_retries
28
+ self.mysql = None
29
+ self._connect()
30
+
31
+ def _connect(self):
32
+ """建立数据库连接,支持重试"""
33
+ for attempt in range(self.max_retries):
34
+ try:
35
+ self.mysql = pymysql.connect(
36
+ host=self.host,
37
+ database=self.database,
38
+ user=self.user,
39
+ password=self.password,
40
+ port=self.port,
41
+ connect_timeout=60,
42
+ read_timeout=60,
43
+ write_timeout=60,
44
+ autocommit=True,
45
+ )
46
+ break # 连接成功,跳出重试循环
47
+ except OperationalError as e:
48
+ if attempt < self.max_retries - 1:
49
+ feishu_alert.send(
50
+ f"MySQL连接失败,正在重试 ({attempt + 1}/{self.max_retries}): {str(e)}"
51
+ )
52
+ time.sleep(2**attempt) # 指数退避
53
+ else:
54
+ feishu_alert.send(f"MySQL连接最终失败: {traceback.format_exc()}")
55
+ raise
56
+
57
+ def get_conn(self):
58
+ return self.mysql
59
+
60
+ def _ensure_connection(self):
61
+ """确保连接有效,如果断开则重新连接"""
62
+ try:
63
+ self.mysql.ping(reconnect=True)
64
+ except Exception:
65
+ self._connect()
66
+
67
+ def get_db_name(self):
68
+ """
69
+ description:
70
+ 获取当前数据库名称
71
+ return:
72
+ database_name(str): 数据库名称
73
+ """
74
+ res = self.query("SELECT DATABASE();")
75
+ database_name = res[0][0]
76
+ return database_name
77
+
78
+ def create_table(self, table_name, columns):
79
+ """
80
+ 创建表
81
+ parameters:
82
+ table_name(str): 表名
83
+ columns(list(tuple)): 列名和类型
84
+ returnType:
85
+ flag(bool): 是否成功
86
+ """
87
+ sql = f"""
88
+ CREATE TABLE {table_name} (
89
+ {','.join([f'{col_name} {type}' for col_name, type in columns])}
90
+ );
91
+ """
92
+ return self.execute_sql(sql)
93
+
94
+ def add_column(self, table_name, column_name, column_type):
95
+ """
96
+ description:
97
+ 添加列
98
+ parameters:
99
+ table_name(str): 表名
100
+ column_name(str): 列名
101
+ column_type(str): 列类型
102
+ return:
103
+ flag(bool): 是否成功
104
+ """
105
+ sql = f"""
106
+ ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type};
107
+ """
108
+ return self.execute_sql(sql)
109
+
110
+ def delete_column(self, table_name, column_name):
111
+ """
112
+ description:
113
+ 删除列
114
+ parameters:
115
+ table_name(str): 表名
116
+ column_name(str): 列名
117
+ return:
118
+ flag(bool): 是否成功
119
+ """
120
+ sql = f"""
121
+ ALTER TABLE {table_name} DROP COLUMN {column_name};
122
+ """
123
+ return self.execute_sql(sql)
124
+
125
+ def get_table_names(self):
126
+ """
127
+ description:
128
+ 获取当前数据库中的所有表名
129
+ return:
130
+ table_names(list): 表名列表
131
+ """
132
+ res = self.query("SHOW TABLES;")
133
+ table_names = [row[0] for row in res]
134
+ return table_names
135
+
136
+ def insert(self, table_name, rows, batch_size=100):
137
+ """
138
+ description:
139
+ 插入数据
140
+ parameters:
141
+ table_name(str): 表名
142
+ rows(list(dict)): 数据
143
+ batch_size(int): 每批插入的条数
144
+ return:
145
+ flag(bool): 是否成功
146
+ """
147
+ for i in range(0, len(rows), batch_size):
148
+ batch_rows = rows[i : i + batch_size]
149
+ values_str_list = []
150
+ for row in batch_rows:
151
+ values_str = ",".join([f"'{value}'" for value in row.values()])
152
+ values_str = f"({values_str})"
153
+ values_str_list.append(values_str)
154
+ sql = f"""
155
+ INSERT INTO {table_name} ({','.join(rows[0].keys())})
156
+ VALUES
157
+ {','.join(values_str_list)};
158
+ """
159
+ self.execute_sql(sql)
160
+ print(f"insert {i} / {len(rows)} rows to {table_name}")
161
+ return True
162
+
163
+ def insert_or_update(self, table_name, rows, key_columns, batch_size=100):
164
+ """
165
+ description:
166
+ 插入数据,如果指定的键已存在则更新
167
+ parameters:
168
+ table_name(str): 表名
169
+ rows(list(dict)): 数据
170
+ key_columns(list): 作为唯一标识的列名列表
171
+ batch_size(int): 每批插入的条数
172
+ return:
173
+ flag(bool): 是否成功
174
+ """
175
+ try:
176
+ for i in range(0, len(rows), batch_size):
177
+ batch_rows = rows[i : i + batch_size]
178
+ values_str_list = []
179
+ for row in batch_rows:
180
+ values_str = ",".join([f"'{value}'" for value in row.values()])
181
+ values_str = f"({values_str})"
182
+ values_str_list.append(values_str)
183
+
184
+ update_str = ",".join(
185
+ [f"{k}=VALUES({k})" for k in rows[0].keys() if k not in key_columns]
186
+ )
187
+ on_duplicate_key = (
188
+ f"ON DUPLICATE KEY UPDATE {update_str}" if update_str else ""
189
+ )
190
+
191
+ sql = f"""
192
+ INSERT INTO {table_name} ({','.join(rows[0].keys())})
193
+ VALUES
194
+ {','.join(values_str_list)}
195
+ {on_duplicate_key};
196
+ """
197
+ self.execute_sql(sql)
198
+ print(f"插入/更新 {i} / {len(rows)} 行到 {table_name}")
199
+ return True
200
+ except Exception as e:
201
+ feishu_alert.send(f"mysql-insert_or_update error {traceback.format_exc()}")
202
+ return False
203
+
204
+ def query(self, sql, convert_to_dict=False):
205
+ """
206
+ description:
207
+ 执行sql查询语句
208
+ parameters:
209
+ sql(str): sql语句
210
+ convert_to_dict(bool): 是否将查询结果转换为字典
211
+ return:
212
+ result(list): 查询结果
213
+ """
214
+ allowed_sql_prefixes = ("SELECT", "SHOW", "DESCRIBE", "EXPLAIN")
215
+ assert (
216
+ sql.lstrip().upper().startswith(allowed_sql_prefixes)
217
+ ), f"sql 必须以 {', '.join(allowed_sql_prefixes)} 开头"
218
+ try:
219
+ self._ensure_connection() # 确保连接有效
220
+ cursor = self.mysql.cursor()
221
+ cursor.execute(sql)
222
+ result = cursor.fetchall()
223
+ columns = [col[0] for col in cursor.description]
224
+ if convert_to_dict:
225
+ return [dict(zip(columns, row)) for row in result]
226
+ return list(result)
227
+ except Exception as e:
228
+ feishu_alert.send(f"mysql-query error {traceback.format_exc()}")
229
+ return []
230
+ finally:
231
+ cursor.close()
232
+
233
+ def execute_sql(self, sql):
234
+ """
235
+ description:
236
+ 执行sql语句
237
+ parameters:
238
+ sql(str): sql语句
239
+ return:
240
+ flag(bool): 是否成功
241
+ """
242
+ try:
243
+ self._ensure_connection() # 确保连接有效
244
+ cursor = self.mysql.cursor()
245
+ cursor.execute(sql)
246
+ self.mysql.commit()
247
+ return True
248
+ except Exception as e:
249
+ self.mysql.rollback()
250
+ feishu_alert.send(f"mysql-execute error {traceback.format_exc()}")
251
+ return False
252
+ finally:
253
+ cursor.close()
254
+
255
+ def clear_table(self, table_name):
256
+ sql = f"""
257
+ TRUNCATE TABLE {table_name};
258
+ """
259
+ return self.execute_sql(sql)
260
+
261
+ def delete_table(self, table_name):
262
+ sql = f"""
263
+ DROP TABLE {table_name};
264
+ """
265
+ return self.execute_sql(sql)
266
+
267
+
268
+ class MysqlUtilsAsync:
269
+ def __init__(
270
+ self,
271
+ host,
272
+ database,
273
+ user,
274
+ password,
275
+ port,
276
+ max_retries=3,
277
+ ):
278
+ self.host = host
279
+ self.database = database
280
+ self.user = user
281
+ self.password = password
282
+ self.port = port
283
+ self.max_retries = max_retries
284
+ self.async_mysql = None
285
+
286
+ async def get_async_conn(self):
287
+ """获取异步MySQL连接"""
288
+ if self.async_mysql is None:
289
+ await self._async_connect()
290
+ return self.async_mysql
291
+
292
+ async def _async_connect(self):
293
+ """建立异步数据库连接,支持重试"""
294
+ for attempt in range(self.max_retries):
295
+ try:
296
+ self.async_mysql = await aiomysql.connect(
297
+ host=self.host,
298
+ db=self.database,
299
+ user=self.user,
300
+ password=self.password,
301
+ port=self.port,
302
+ connect_timeout=60,
303
+ autocommit=True,
304
+ )
305
+ break # 连接成功,跳出重试循环
306
+ except Exception as e:
307
+ if attempt < self.max_retries - 1:
308
+ feishu_alert.send(
309
+ f"异步MySQL连接失败,正在重试 ({attempt + 1}/{self.max_retries}): {str(e)}"
310
+ )
311
+ await asyncio.sleep(2**attempt) # 指数退避
312
+ else:
313
+ feishu_alert.send(
314
+ f"异步MySQL连接最终失败: {traceback.format_exc()}"
315
+ )
316
+ raise
317
+
318
+ async def _async_ensure_connection(self):
319
+ """确保异步连接有效,如果断开则重新连接"""
320
+ try:
321
+ if self.async_mysql is None:
322
+ await self._async_connect()
323
+ else:
324
+ # aiomysql没有ping方法,直接尝试执行简单查询
325
+ cursor = await self.async_mysql.cursor()
326
+ await cursor.execute("SELECT 1")
327
+ await cursor.close()
328
+ except Exception:
329
+ await self._async_connect()
330
+
331
+ async def get_db_name(self):
332
+ """
333
+ description:
334
+ 异步获取当前数据库名称
335
+ return:
336
+ database_name(str): 数据库名称
337
+ """
338
+ res = await self.query("SELECT DATABASE();")
339
+ database_name = res[0][0]
340
+ return database_name
341
+
342
+ async def create_table(self, table_name, columns):
343
+ """
344
+ 异步创建表
345
+ parameters:
346
+ table_name(str): 表名
347
+ columns(list(tuple)): 列名和类型
348
+ returnType:
349
+ flag(bool): 是否成功
350
+ """
351
+ sql = f"""
352
+ CREATE TABLE {table_name} (
353
+ {','.join([f'{col_name} {type}' for col_name, type in columns])}
354
+ );
355
+ """
356
+ return await self.execute_sql(sql)
357
+
358
+ async def add_column(self, table_name, column_name, column_type):
359
+ """
360
+ description:
361
+ 异步添加列
362
+ parameters:
363
+ table_name(str): 表名
364
+ column_name(str): 列名
365
+ column_type(str): 列类型
366
+ return:
367
+ flag(bool): 是否成功
368
+ """
369
+ sql = f"""
370
+ ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type};
371
+ """
372
+ return await self.execute_sql(sql)
373
+
374
+ async def delete_column(self, table_name, column_name):
375
+ """
376
+ description:
377
+ 异步删除列
378
+ parameters:
379
+ table_name(str): 表名
380
+ column_name(str): 列名
381
+ return:
382
+ flag(bool): 是否成功
383
+ """
384
+ sql = f"""
385
+ ALTER TABLE {table_name} DROP COLUMN {column_name};
386
+ """
387
+ return await self.execute_sql(sql)
388
+
389
+ async def get_table_names(self):
390
+ """
391
+ description:
392
+ 异步获取当前数据库中的所有表名
393
+ return:
394
+ table_names(list): 表名列表
395
+ """
396
+ res = await self.query("SHOW TABLES;")
397
+ table_names = [row[0] for row in res]
398
+ return table_names
399
+
400
+ async def insert(self, table_name, rows, batch_size=100):
401
+ """
402
+ description:
403
+ 异步插入数据
404
+ parameters:
405
+ table_name(str): 表名
406
+ rows(list(dict)): 数据
407
+ batch_size(int): 每批插入的条数
408
+ return:
409
+ flag(bool): 是否成功
410
+ """
411
+ for i in range(0, len(rows), batch_size):
412
+ batch_rows = rows[i : i + batch_size]
413
+ values_str_list = []
414
+ for row in batch_rows:
415
+ values_str = ",".join([f"'{value}'" for value in row.values()])
416
+ values_str = f"({values_str})"
417
+ values_str_list.append(values_str)
418
+ sql = f"""
419
+ INSERT INTO {table_name} ({','.join(rows[0].keys())})
420
+ VALUES
421
+ {','.join(values_str_list)};
422
+ """
423
+ await self.execute_sql(sql)
424
+ print(f"insert {i} / {len(rows)} rows to {table_name}")
425
+ return True
426
+
427
+ async def insert_or_update(self, table_name, rows, key_columns, batch_size=100):
428
+ """
429
+ description:
430
+ 异步插入数据,如果指定的键已存在则更新
431
+ parameters:
432
+ table_name(str): 表名
433
+ rows(list(dict)): 数据
434
+ key_columns(list): 作为唯一标识的列名列表
435
+ batch_size(int): 每批插入的条数
436
+ return:
437
+ flag(bool): 是否成功
438
+ """
439
+ try:
440
+ for i in range(0, len(rows), batch_size):
441
+ batch_rows = rows[i : i + batch_size]
442
+ values_str_list = []
443
+ for row in batch_rows:
444
+ values_str = ",".join([f"'{value}'" for value in row.values()])
445
+ values_str = f"({values_str})"
446
+ values_str_list.append(values_str)
447
+
448
+ update_str = ",".join(
449
+ [f"{k}=VALUES({k})" for k in rows[0].keys() if k not in key_columns]
450
+ )
451
+ on_duplicate_key = (
452
+ f"ON DUPLICATE KEY UPDATE {update_str}" if update_str else ""
453
+ )
454
+
455
+ sql = f"""
456
+ INSERT INTO {table_name} ({','.join(rows[0].keys())})
457
+ VALUES
458
+ {','.join(values_str_list)}
459
+ {on_duplicate_key};
460
+ """
461
+ await self.execute_sql(sql)
462
+ print(f"插入/更新 {i} / {len(rows)} 行到 {table_name}")
463
+ return True
464
+ except Exception as e:
465
+ feishu_alert.send(
466
+ f"mysql-insert_or_update_async error {traceback.format_exc()}"
467
+ )
468
+ return False
469
+
470
+ async def query(self, sql, convert_to_dict=False):
471
+ """
472
+ description:
473
+ 异步执行sql查询语句
474
+ parameters:
475
+ sql(str): sql语句
476
+ convert_to_dict(bool): 是否将查询结果转换为字典
477
+ return:
478
+ result(list): 查询结果
479
+ """
480
+ allowed_sql_prefixes = ("SELECT", "SHOW", "DESCRIBE", "EXPLAIN")
481
+ assert (
482
+ sql.lstrip().upper().startswith(allowed_sql_prefixes)
483
+ ), f"sql 必须以 {', '.join(allowed_sql_prefixes)} 开头"
484
+ cursor = None
485
+ try:
486
+ await self._async_ensure_connection() # 确保连接有效
487
+ async_mysql = await self.get_async_conn()
488
+ cursor = await async_mysql.cursor()
489
+ await cursor.execute(sql)
490
+ result = await cursor.fetchall()
491
+ columns = [col[0] for col in cursor.description]
492
+ if convert_to_dict:
493
+ return [dict(zip(columns, row)) for row in result]
494
+ return list(result)
495
+ except Exception as e:
496
+ feishu_alert.send(f"mysql-query_async error {traceback.format_exc()}")
497
+ return []
498
+ finally:
499
+ if cursor:
500
+ await cursor.close()
501
+
502
+ async def execute_sql(self, sql):
503
+ """
504
+ description:
505
+ 异步执行sql语句
506
+ parameters:
507
+ sql(str): sql语句
508
+ return:
509
+ flag(bool): 是否成功
510
+ """
511
+ cursor = None
512
+ try:
513
+ await self._async_ensure_connection() # 确保连接有效
514
+ async_mysql = await self.get_async_conn()
515
+ cursor = await async_mysql.cursor()
516
+ await cursor.execute(sql)
517
+ await async_mysql.commit()
518
+ return True
519
+ except Exception as e:
520
+ async_mysql = await self.get_async_conn()
521
+ await async_mysql.rollback()
522
+ feishu_alert.send(f"mysql-execute_async error {traceback.format_exc()}")
523
+ return False
524
+ finally:
525
+ if cursor:
526
+ await cursor.close()
527
+
528
+ async def clear_table(self, table_name):
529
+ sql = f"""
530
+ TRUNCATE TABLE {table_name};
531
+ """
532
+ return await self.execute_sql(sql)
533
+
534
+ async def delete_table(self, table_name):
535
+ sql = f"""
536
+ DROP TABLE {table_name};
537
+ """
538
+ return await self.execute_sql(sql)
539
+
540
+ async def close(self):
541
+ """关闭异步MySQL连接"""
542
+ if self.async_mysql:
543
+ self.async_mysql.close()
544
+ await self.async_mysql.wait_closed()