qrpa 1.0.28__tar.gz → 1.0.29__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.

Potentially problematic release.


This version of qrpa might be problematic. Click here for more details.

Files changed (31) hide show
  1. {qrpa-1.0.28 → qrpa-1.0.29}/PKG-INFO +1 -1
  2. {qrpa-1.0.28 → qrpa-1.0.29}/pyproject.toml +1 -1
  3. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/__init__.py +1 -0
  4. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/feishu_bot_app.py +2 -2
  5. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/fun_excel.py +105 -37
  6. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/shein_excel.py +52 -14
  7. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/wxwork.py +318 -318
  8. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa.egg-info/PKG-INFO +1 -1
  9. {qrpa-1.0.28 → qrpa-1.0.29}/README.md +0 -0
  10. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/RateLimitedSender.py +0 -0
  11. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/db_migrator.py +0 -0
  12. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/fun_base.py +0 -0
  13. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/fun_file.py +0 -0
  14. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/fun_web.py +0 -0
  15. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/fun_win.py +0 -0
  16. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/shein_daily_report_model.py +0 -0
  17. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/shein_lib.py +0 -0
  18. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/shein_sqlite.py +0 -0
  19. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/shein_ziniao.py +0 -0
  20. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/temu_chrome.py +0 -0
  21. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/temu_excel.py +0 -0
  22. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/temu_lib.py +0 -0
  23. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/time_utils.py +0 -0
  24. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa/time_utils_example.py +0 -0
  25. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa.egg-info/SOURCES.txt +0 -0
  26. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa.egg-info/dependency_links.txt +0 -0
  27. {qrpa-1.0.28 → qrpa-1.0.29}/qrpa.egg-info/top_level.txt +0 -0
  28. {qrpa-1.0.28 → qrpa-1.0.29}/setup.cfg +0 -0
  29. {qrpa-1.0.28 → qrpa-1.0.29}/setup.py +0 -0
  30. {qrpa-1.0.28 → qrpa-1.0.29}/tests/test_db_migrator.py +0 -0
  31. {qrpa-1.0.28 → qrpa-1.0.29}/tests/test_wxwork.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qrpa
3
- Version: 1.0.28
3
+ Version: 1.0.29
4
4
  Summary: qsir's rpa library
5
5
  Author: QSir
6
6
  Author-email: QSir <1171725650@qq.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qrpa"
7
- version = "1.0.28"
7
+ version = "1.0.29"
8
8
  description = "qsir's rpa library"
9
9
  authors = [{ name = "QSir", email = "1171725650@qq.com" }]
10
10
  readme = "README.md"
@@ -1,4 +1,5 @@
1
1
  from .wxwork import WxWorkBot, WxWorkAppBot
2
+ from .feishu_bot_app import FeishuBot
2
3
  from .db_migrator import DatabaseMigrator, DatabaseConfig, RemoteConfig, create_default_migrator
3
4
 
4
5
  from .shein_ziniao import ZiniaoRunner
@@ -26,8 +26,8 @@ class FeishuBot:
26
26
  """获取飞书客户端,使用懒加载模式"""
27
27
  if self._client is None:
28
28
  self._client = lark.Client.builder() \
29
- .app_id(self.config.feishu_bot_app_id) \
30
- .app_secret(self.config.feishu_bot_app_secret) \
29
+ .app_id(self.config.feishu_bot.app_id) \
30
+ .app_secret(self.config.feishu_bot.app_secret) \
31
31
  .log_level(lark.LogLevel.INFO) \
32
32
  .build()
33
33
  return self._client
@@ -282,6 +282,7 @@ def delete_sheet_if_exists(wb, sheet_name):
282
282
  sheet_names = [s.name for s in wb.sheets]
283
283
  if sheet_name in sheet_names:
284
284
  wb.sheets[sheet_name].delete()
285
+ wb.save()
285
286
  print(f"已删除 Sheet: {sheet_name}")
286
287
  else:
287
288
  print(f"Sheet 不存在: {sheet_name}")
@@ -1871,18 +1872,18 @@ def format_to_text_v2_safe(sheet, columns=None, data_rows=None):
1871
1872
  """
1872
1873
  if columns is None or len(columns) == 0:
1873
1874
  return
1874
-
1875
+
1875
1876
  # 确保columns是列表
1876
1877
  if not isinstance(columns, list):
1877
1878
  columns = [columns]
1878
-
1879
+
1879
1880
  for col_name in columns:
1880
1881
  try:
1881
1882
  if isinstance(col_name, int):
1882
1883
  col_name = xw.utils.col_name(col_name)
1883
-
1884
+
1884
1885
  log(f'安全设置[{col_name}] 文本格式')
1885
-
1886
+
1886
1887
  # 如果指定了数据行数,只格式化有数据的范围
1887
1888
  if data_rows and data_rows > 0:
1888
1889
  # 格式化从第1行到数据行数的范围
@@ -1899,7 +1900,7 @@ def format_to_text_v2_safe(sheet, columns=None, data_rows=None):
1899
1900
  log(f'列 {col_name} 没有数据,跳过格式化')
1900
1901
  except:
1901
1902
  log(f'列 {col_name} 格式化失败,跳过')
1902
-
1903
+
1903
1904
  except Exception as e:
1904
1905
  log(f'设置列 {col_name} 文本格式失败: {e},继续处理其他列')
1905
1906
 
@@ -1914,14 +1915,14 @@ def pre_format_columns_safe(sheet, columns, data_rows):
1914
1915
  """
1915
1916
  if not columns or not isinstance(columns, list):
1916
1917
  return
1917
-
1918
+
1918
1919
  for col_name in columns:
1919
1920
  try:
1920
1921
  if isinstance(col_name, int):
1921
1922
  col_name = xw.utils.col_name(col_name)
1922
-
1923
+
1923
1924
  log(f'预格式化列 [{col_name}] 为文本格式')
1924
-
1925
+
1925
1926
  # 方法1:先创建最小范围,避免整列操作
1926
1927
  try:
1927
1928
  # 创建足够大的范围来覆盖预期数据
@@ -1930,7 +1931,7 @@ def pre_format_columns_safe(sheet, columns, data_rows):
1930
1931
  log(f'预格式化成功: {range_str}')
1931
1932
  except Exception as e1:
1932
1933
  log(f'预格式化方法1失败: {e1}')
1933
-
1934
+
1934
1935
  # 方法2:逐行设置格式,更安全但稍慢
1935
1936
  try:
1936
1937
  for row in range(1, data_rows + 1):
@@ -1939,7 +1940,7 @@ def pre_format_columns_safe(sheet, columns, data_rows):
1939
1940
  log(f'逐行预格式化成功: {col_name}')
1940
1941
  except Exception as e2:
1941
1942
  log(f'逐行预格式化也失败: {e2}')
1942
-
1943
+
1943
1944
  except Exception as e:
1944
1945
  log(f'预格式化列 {col_name} 失败: {e},继续处理其他列')
1945
1946
 
@@ -1954,20 +1955,20 @@ def post_format_columns_safe(sheet, columns, data_rows):
1954
1955
  """
1955
1956
  if not columns or not isinstance(columns, list):
1956
1957
  return
1957
-
1958
+
1958
1959
  for col_name in columns:
1959
1960
  try:
1960
1961
  if isinstance(col_name, int):
1961
1962
  col_name = xw.utils.col_name(col_name)
1962
-
1963
+
1963
1964
  log(f'后格式化列 [{col_name}] 为文本格式')
1964
-
1965
+
1965
1966
  # 只对实际有数据的行进行格式化
1966
1967
  if data_rows > 0:
1967
1968
  range_str = f'{col_name}1:{col_name}{data_rows}'
1968
1969
  sheet.range(range_str).number_format = '@'
1969
1970
  log(f'后格式化成功: {range_str}')
1970
-
1971
+
1971
1972
  except Exception as e:
1972
1973
  log(f'后格式化列 {col_name} 失败: {e},继续处理其他列')
1973
1974
 
@@ -2545,18 +2546,77 @@ def format_excel_with_lock(excel_path, sheet_name, format_func, *args, **kwargs)
2545
2546
  # 经过观察 fortmat时 传入函数需要为类函数且第二个参数必须是 sheet
2546
2547
  def batch_excel_operations(excel_path, operations):
2547
2548
  """
2548
- 批量 Excel 操作函数,一次性打开 Excel 执行多个操作
2549
-
2549
+ 批量 Excel 操作函数,自动分批处理,避免一次操作过多sheet导致Excel COM错误
2550
+ 保持操作的原始顺序执行
2551
+
2550
2552
  Args:
2551
2553
  excel_path: Excel 文件路径
2552
2554
  operations: 操作列表,每个操作是 (sheet_name, operation_type, data, format_func) 的元组
2553
- operation_type: 'write' 'format'
2554
- data: 写入的数据(仅 write 操作需要)
2555
- format_func: 格式化函数(仅 format 操作需要)
2556
-
2555
+ operation_type: 'write', 'format', 'delete', 'move', 'active'
2556
+
2557
2557
  Returns:
2558
2558
  bool: 是否全部操作成功
2559
2559
  """
2560
+ if not operations:
2561
+ return True
2562
+
2563
+ # 批处理大小设置:每批最多处理8个操作
2564
+ MAX_OPERATIONS_PER_BATCH = 8
2565
+
2566
+ try:
2567
+ # 计算需要分几批
2568
+ total_batches = (len(operations) + MAX_OPERATIONS_PER_BATCH - 1) // MAX_OPERATIONS_PER_BATCH
2569
+ log(f"分{total_batches}批执行{len(operations)}个操作,每批最多{MAX_OPERATIONS_PER_BATCH}个,保持原始顺序")
2570
+
2571
+ # 按顺序分批执行
2572
+ for batch_idx in range(total_batches):
2573
+ start_idx = batch_idx * MAX_OPERATIONS_PER_BATCH
2574
+ end_idx = min(start_idx + MAX_OPERATIONS_PER_BATCH, len(operations))
2575
+ batch_operations = operations[start_idx:end_idx]
2576
+
2577
+ log(f"执行第{batch_idx + 1}/{total_batches}批操作({start_idx + 1}-{end_idx}),共{len(batch_operations)}个操作")
2578
+
2579
+ # 重试机制
2580
+ max_retries = 3
2581
+ for retry in range(max_retries):
2582
+ try:
2583
+ # 强制垃圾回收
2584
+ import gc
2585
+ gc.collect()
2586
+
2587
+ if _execute_operations_batch(excel_path, batch_operations):
2588
+ log(f"第{batch_idx + 1}批操作成功")
2589
+ break
2590
+ else:
2591
+ log(f"第{batch_idx + 1}批操作失败,重试 {retry + 1}/{max_retries}")
2592
+ if retry == max_retries - 1:
2593
+ log(f"第{batch_idx + 1}批操作最终失败")
2594
+ return False
2595
+ import time
2596
+ time.sleep(3)
2597
+ except Exception as e:
2598
+ log(f"第{batch_idx + 1}批操作异常: {e}")
2599
+ if retry == max_retries - 1:
2600
+ return False
2601
+ import time
2602
+ time.sleep(3)
2603
+
2604
+ # 批次间延迟
2605
+ if batch_idx < total_batches - 1:
2606
+ import time
2607
+ time.sleep(1)
2608
+
2609
+ log(f"所有批量操作完成: {excel_path}")
2610
+ return True
2611
+
2612
+ except Exception as e:
2613
+ log(f"批量操作过程异常: {e}")
2614
+ return False
2615
+
2616
+ def _execute_operations_batch(excel_path, operations):
2617
+ """
2618
+ 执行单个批次的操作
2619
+ """
2560
2620
  app, wb, sheet = open_excel_with_lock(excel_path)
2561
2621
  if not app or not wb:
2562
2622
  log(f"无法打开 Excel 文件: {excel_path}")
@@ -2564,23 +2624,38 @@ def batch_excel_operations(excel_path, operations):
2564
2624
 
2565
2625
  try:
2566
2626
  for sheet_name, operation_type, *args in operations:
2567
- # 获取或创建工作表
2627
+ # 根据操作类型决定是否需要获取或创建工作表
2628
+ sheet = None
2629
+
2630
+ # 删除操作不需要获取sheet对象
2631
+ if operation_type == 'delete':
2632
+ log(f'删除sheet: {sheet_name}')
2633
+ delete_sheet_if_exists(wb, sheet_name)
2634
+ continue
2635
+
2636
+ # 其他操作需要获取或创建工作表
2568
2637
  if isinstance(sheet_name, str):
2569
2638
  sheet_names = [s.name.strip().lower() for s in wb.sheets]
2570
2639
  if sheet_name.strip().lower() in sheet_names:
2571
2640
  sheet = wb.sheets[sheet_name]
2572
2641
  else:
2573
- sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
2642
+ # 只有在需要操作sheet内容时才创建
2643
+ if operation_type in ['write', 'format']:
2644
+ sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
2645
+ else:
2646
+ log(f"警告: 操作 {operation_type} 需要的sheet {sheet_name} 不存在,跳过此操作")
2647
+ continue
2574
2648
  else:
2575
2649
  sheet = wb.sheets[sheet_name]
2576
2650
 
2577
- sheet.activate()
2651
+ if sheet:
2652
+ sheet.activate()
2578
2653
 
2579
2654
  if operation_type == 'write':
2580
2655
  data, format_to_text_colunm = args[0], args[1:] if len(args) > 1 else None
2581
2656
  # 清空工作表
2582
2657
  sheet.clear()
2583
-
2658
+
2584
2659
  # 先设置文本格式,再写入数据(确保格式生效)
2585
2660
  if format_to_text_colunm and format_to_text_colunm[0]:
2586
2661
  try:
@@ -2588,11 +2663,11 @@ def batch_excel_operations(excel_path, operations):
2588
2663
  pre_format_columns_safe(sheet, format_to_text_colunm[0], len(data))
2589
2664
  except Exception as e:
2590
2665
  log(f"预格式化失败: {e},继续执行")
2591
-
2666
+
2592
2667
  # 写入数据
2668
+ log(f"批量操作,写入数据到: {sheet_name}")
2593
2669
  sheet.range('A1').value = data
2594
- log(f"批量操作:写入数据到 {sheet_name}")
2595
-
2670
+
2596
2671
  # 写入后再次确认格式(双重保险)
2597
2672
  if format_to_text_colunm and format_to_text_colunm[0]:
2598
2673
  try:
@@ -2602,31 +2677,24 @@ def batch_excel_operations(excel_path, operations):
2602
2677
 
2603
2678
  elif operation_type == 'format':
2604
2679
  format_func, format_args = args[0], args[1:] if len(args) > 1 else ()
2605
- log('格式化入参', *format_args)
2606
2680
  # 执行格式化
2607
2681
  format_func(sheet, *format_args)
2608
- log(f"批量操作:格式化工作表 {sheet_name}")
2609
-
2610
- elif operation_type == 'delete':
2611
- pass
2612
- delete_sheet_if_exists(wb, sheet_name)
2613
2682
 
2614
2683
  elif operation_type == 'move':
2615
- pass
2684
+ log(f'移动sheet: {sheet_name}')
2616
2685
  position = args[0]
2617
2686
  move_sheet_to_position(wb, sheet_name, position)
2618
2687
 
2619
2688
  elif operation_type == 'active':
2620
- pass
2689
+ log(f'激活sheet: {sheet_name}')
2621
2690
  sheet.activate()
2622
2691
 
2623
2692
  # 保存所有更改
2624
2693
  wb.save()
2625
- log(f"批量操作完成: {excel_path}")
2626
2694
  return True
2627
2695
 
2628
2696
  except Exception as e:
2629
- log(f"批量操作失败: {e}")
2697
+ log(f"单批次操作失败: {e}")
2630
2698
  return False
2631
2699
  finally:
2632
2700
  # 释放锁但不关闭 Excel(保持复用)
@@ -1426,6 +1426,13 @@ class SheinExcel:
1426
1426
  ))
1427
1427
  batch_excel_operations(excel_path, operations)
1428
1428
 
1429
+ # 添加月度sheet操作 - 自定义操作函数
1430
+ def write_monthly_data(self, sheet, data, name):
1431
+ # 写入数据到A5位置(月度数据从A列开始)
1432
+ sheet.range('A5').value = data
1433
+ # 设置标题
1434
+ sheet.range('A1').value = f'{name}SHEIN{TimeUtils.get_current_month()}月店铺数据'
1435
+
1429
1436
  def write_sales_data(self):
1430
1437
  yesterday = TimeUtils.get_yesterday()
1431
1438
  model = SheinStoreSalesDetailManager(self.config.database_url)
@@ -1464,7 +1471,7 @@ class SheinExcel:
1464
1471
  sheet_name_first = 'SHEIN销售部每日店铺情况'
1465
1472
 
1466
1473
  # 准备批量操作列表
1467
- operations = []
1474
+ base_operations = []
1468
1475
 
1469
1476
  # 添加每日汇总sheet的操作 - 自定义操作函数
1470
1477
  def write_daily_data(sheet):
@@ -1475,13 +1482,16 @@ class SheinExcel:
1475
1482
  # 设置日期和合并
1476
1483
  sheet.range('A4').value = f'{TimeUtils.format_date_cross_platform(yesterday)}\n({TimeUtils.get_chinese_weekday(yesterday)})'
1477
1484
 
1478
- operations.append((sheet_name_first, 'format', write_daily_data))
1479
- operations.append((sheet_name_first, 'format', self._format_daily_summary_sheet, yesterday, len(data_day)))
1480
- operations.append(('Sheet1', 'delete'))
1481
- operations.append((sheet_name_first, 'move', 1))
1485
+ base_operations.append((sheet_name_first, 'format', write_daily_data))
1486
+ base_operations.append((sheet_name_first, 'format', self._format_daily_summary_sheet, yesterday, len(data_day)))
1487
+ base_operations.append((sheet_name_first, 'move', 1))
1488
+ base_operations.append(('Sheet1', 'delete'))
1482
1489
 
1483
1490
  # 获取店铺列表并准备月度数据
1484
1491
  store_list = model.get_distinct_store_sales_list()
1492
+
1493
+ # 准备所有店铺的数据
1494
+ store_operations_data = []
1485
1495
  for store in store_list:
1486
1496
  store_username = store[0]
1487
1497
  store_name = dict_store_name.get(store_username)
@@ -1512,18 +1522,22 @@ class SheinExcel:
1512
1522
  # store_data.append(record.remark) # 月度数据不包含备注列,保持19列
1513
1523
  data_month.append(store_data)
1514
1524
 
1515
- # 添加月度sheet操作 - 自定义操作函数
1516
- def write_monthly_data(sheet, data=data_month, name=store_name):
1517
- # 写入数据到A5位置(月度数据从A列开始)
1518
- sheet.range('A5').value = data
1519
- # 设置标题
1520
- sheet.range('A1').value = f'{name}SHEIN{TimeUtils.get_current_month()}月店铺数据'
1525
+ store_operations_data.append((store_name, data_month))
1526
+
1527
+ # 构建所有操作列表
1528
+ operations = base_operations.copy()
1521
1529
 
1522
- operations.append((store_name, 'format', write_monthly_data))
1523
- operations.append((store_name, 'format', self._format_store_monthly_sheet, store_name, len(data_month)))
1530
+ # 添加店铺操作
1531
+ for store_name, data_month in store_operations_data:
1532
+ # 清理店铺名称
1533
+ clean_store_name = self._clean_sheet_name(store_name)
1534
+ operations.append((clean_store_name, 'format', self.write_monthly_data, data_month, clean_store_name))
1535
+ operations.append((clean_store_name, 'format', self._format_store_monthly_sheet, clean_store_name, len(data_month)))
1524
1536
 
1525
- # 执行批量操作
1537
+ # 添加最后激活操作
1526
1538
  operations.append((sheet_name_first, 'active'))
1539
+
1540
+ # 执行批量操作(内部会自动分批处理)
1527
1541
  success = batch_excel_operations(excel_path, operations)
1528
1542
 
1529
1543
  if success:
@@ -1533,6 +1547,30 @@ class SheinExcel:
1533
1547
  else:
1534
1548
  log(f"销售数据写入失败: {excel_path}")
1535
1549
 
1550
+ def _clean_sheet_name(self, name):
1551
+ """
1552
+ 清理工作表名称,移除Excel不支持的字符
1553
+ """
1554
+ if not name:
1555
+ return "DefaultSheet"
1556
+
1557
+ # Excel工作表名称限制:不能包含 [ ] : * ? / \ 字符,且长度不超过31字符
1558
+ invalid_chars = ['[', ']', ':', '*', '?', '/', '\\']
1559
+ clean_name = name
1560
+
1561
+ for char in invalid_chars:
1562
+ clean_name = clean_name.replace(char, '_')
1563
+
1564
+ # 限制长度为31字符
1565
+ if len(clean_name) > 31:
1566
+ clean_name = clean_name[:28] + "..."
1567
+
1568
+ # 确保不为空
1569
+ if not clean_name.strip():
1570
+ clean_name = "Sheet"
1571
+
1572
+ return clean_name
1573
+
1536
1574
  def _format_daily_summary_sheet(self, sheet, yesterday, data_length):
1537
1575
  """格式化每日汇总sheet"""
1538
1576
  las_row = data_length + 4 # 数据从第5行开始,4行header
@@ -1,318 +1,318 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- -------------------------------------------------
4
- @version : v1.0
5
- @author : qsir
6
- @contact : qsir@vxnote.com
7
- @software : PyCharm
8
- @filename : wxwork.py
9
- @create time: 2025/08/03
10
- @modify time: 2025/08/03
11
- @describe :
12
- -------------------------------------------------
13
- """
14
- import json
15
- import os
16
- import hashlib
17
- import base64
18
- import requests
19
- from requests_toolbelt import MultipartEncoder
20
- from datetime import datetime
21
-
22
- # 通过企微群机器人发送消息
23
- class WxWorkBot:
24
- def __init__(self, key):
25
- self.key = key
26
-
27
- def upload_media(self, filepath):
28
- """
29
- 上传临时素材,给企微群里发文件消息时需要先将文件上传至企微临时素材中
30
- :param filepath:
31
- :return: 临时素材的media_id
32
- """
33
- try:
34
- headers = {
35
- 'Content-Type': 'multipart/form-data',
36
- }
37
- with open(filepath, 'rb') as f:
38
- files = {
39
- 'media': (os.path.basename(filepath), f.read())
40
- }
41
- response = requests.post(
42
- f'https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={self.key}&type=file',
43
- headers=headers, files=files)
44
- response_text = json.loads(response.text)
45
- if str(response_text.get('errcode')) != '0':
46
- raise Exception(response_text)
47
- if response.status_code == 200:
48
- result = json.loads(response.text)
49
- return result['media_id']
50
- else:
51
- print("HTTP Error:", response.status_code)
52
- return None
53
- except Exception as err:
54
- raise Exception("upload_media error", err)
55
-
56
- def send_file(self, file_path):
57
- if not os.path.exists(file_path):
58
- print('文件不存在: ', file_path)
59
- return
60
- """
61
- 发送文件到群里
62
- :param file_path:
63
- :return:
64
- """
65
- media_id = self.upload_media(file_path)
66
- data = {
67
- "msgtype": "file",
68
- "file" : {
69
- "media_id": media_id
70
- }
71
- }
72
- return self.send_msg(data)
73
-
74
- def send_text(self, content, mentioned_list=None, mentioned_mobile_list=None):
75
- """
76
- 发送文本消息
77
- :param content:
78
- :param mentioned_list: 需要@的人userid
79
- :param mentioned_mobile_list: 需要@的人手机号
80
- :return:
81
- """
82
- data = {
83
- "msgtype": "text",
84
- "text" : {
85
- "content": content
86
- }
87
- }
88
- if mentioned_list is not None and mentioned_list:
89
- data['text'].update({"mentioned_list": mentioned_list})
90
- if mentioned_mobile_list is not None and mentioned_mobile_list:
91
- data['text'].update({"mentioned_mobile_list": mentioned_mobile_list})
92
-
93
- self.send_msg(data)
94
-
95
- def send_markdown(self, content):
96
- """
97
- 发送Markdown消息
98
- :param content:
99
- :return:
100
- """
101
- data = {
102
- "msgtype" : "markdown",
103
- "markdown": {
104
- "content": content
105
- }
106
- }
107
- self.send_msg(data)
108
-
109
- def send_notify(self, title, sub_title_list, data_list):
110
- """
111
- 发送Markdown消息
112
- :param content:
113
- :return:
114
- """
115
-
116
- current_date = datetime.now().strftime("%Y-%m-%d")
117
- header = f"{current_date} {title}\n\n"
118
-
119
- arr_color = ['warning', 'info', 'warning']
120
- arr_sub_header = [f"<font color='{arr_color[index]}'>{title}</font>" for index, title in enumerate(sub_title_list)]
121
- sub_header = "\t".join(arr_sub_header) + "\n\n"
122
-
123
- # 获取每个元素的行索引和列索引
124
- arr_content = [
125
- [
126
- f'{value}' if col_idx == 0 else f"<font color='{arr_color[col_idx - 1]}'>{value}</font>"
127
- for col_idx, value in enumerate(row)
128
- ] # 每行的元素组成一个子列表
129
- for row_idx, row in enumerate(data_list) # 外层循环控制行
130
- ]
131
- # 将二维数组转换为字符串
132
- content = "\n".join(
133
- # 对每行的元素列表使用 join(),用 \t 连接
134
- "\t".join(row) for row in arr_content
135
- )
136
-
137
- data = {
138
- "msgtype" : "markdown",
139
- "markdown": {
140
- "content": header + sub_header + content
141
- }
142
- }
143
- self.send_msg(data)
144
-
145
- def send_img(self, img_path):
146
- """
147
- 发送图片消息
148
- 图片(base64编码前)最大不能超过2M,支持JPG,PNG格式
149
- :param img_path:
150
- :return:
151
- """
152
- data = {
153
- "msgtype": "image",
154
- "image" : {
155
- "base64": self.img_to_base64(img_path),
156
- "md5" : self.img_to_md5(img_path)
157
- }
158
- }
159
- self.send_msg(data)
160
-
161
- def send_news(self, title, description, url, picurl):
162
- """
163
- 发送图文消息
164
- :param title: 标题
165
- :param description: 描述
166
- :param url: 跳转URL
167
- :param picurl: 图文图片地址
168
- :return:
169
- """
170
- data = {
171
- "msgtype": "news",
172
- "news" : {
173
- "articles": [
174
- {
175
- "title" : title,
176
- "description": description,
177
- "url" : url,
178
- "picurl" : picurl
179
- }
180
- ]
181
- }
182
- }
183
- self.send_msg(data)
184
-
185
- def send_msg(self, data):
186
- """
187
- 发送机器人通用消息到企微群
188
- :param data: 消息内容json数据
189
- :return:
190
- """
191
- try:
192
- header = {
193
- "Content-Type": "application/json"
194
- }
195
- response = requests.post(f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={self.key}", headers=header, data=json.dumps(data))
196
- response_text = json.loads(response.text)
197
- if str(response_text.get('errcode')) != '0':
198
- raise Exception(response_text)
199
- if response.status_code == 200:
200
- result = json.loads(response.text)
201
- return result
202
- else:
203
- print("HTTP Error:", response.status_code)
204
- return None
205
- except Exception as err:
206
- raise Exception("Send Chat Message error", err)
207
-
208
- def img_to_md5(self, img_path):
209
- # 读取图片文件并计算MD5值
210
- with open(img_path, 'rb') as image_file:
211
- image_data = image_file.read()
212
- return hashlib.md5(image_data).hexdigest()
213
-
214
- def img_to_base64(self, img_path):
215
- # 读取图片文件并转换为Base64编码
216
- with open(img_path, 'rb') as image_file:
217
- image_data = image_file.read()
218
- return base64.b64encode(image_data).decode('utf-8')
219
-
220
- # 通过企微应用发送消息
221
- class WxWorkAppBot:
222
- def __init__(self, corpid, corpsecret, agentid):
223
- self.corpid = corpid
224
- self.corpsecret = corpsecret
225
- self.agentid = agentid
226
- self.access_token = self._getToken()
227
-
228
- def _getToken(self):
229
- try:
230
- if all([self.corpid, self.corpsecret]):
231
- url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corpid}&corpsecret={corpsecret}".format(
232
- corpid=self.corpid, corpsecret=self.corpsecret)
233
- response = requests.get(url)
234
- if response.status_code == 200:
235
- result = json.loads(response.text)
236
- return result['access_token']
237
- else:
238
- print("HTTP Error:", response.status_code)
239
- return None
240
- except Exception as err:
241
- raise Exception("get WeChat access Token error", err)
242
-
243
- def _send_msg(self, data):
244
- self._check_token()
245
- try:
246
- send_url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}".format(
247
- access_token=self.access_token)
248
- response = requests.post(send_url, json.dumps(data))
249
- if response.status_code == 200:
250
- result = json.loads(response.text)
251
- return result
252
- else:
253
- print("HTTP Error:", response.status_code)
254
- return None
255
- except Exception as err:
256
- raise Exception("send WeChat Message error", err)
257
-
258
- def _check_token(self):
259
- if self.access_token is None:
260
- self._getToken()
261
-
262
- def send_msg(self, data):
263
- return self._send_msg(data)
264
-
265
- def upload_media(self, filetype, filepath, filename):
266
- """
267
- 上传临时素材到企微并获取media_id
268
- :param filetype: 图片(image)、语音(voice)、视频(video),普通文件(file)
269
- :param filepath:
270
- :param filename:
271
- :return: media_id
272
- """
273
- try:
274
- self._check_token()
275
- post_file_url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type={filetype}".format(
276
- filetype=filetype,
277
- access_token=self.access_token)
278
-
279
- m = MultipartEncoder(
280
- fields={filename: (filename, open(filepath + filename, 'rb'), 'text/plain')},
281
- )
282
- response = requests.post(url=post_file_url, data=m, headers={'Content-Type': m.content_type})
283
- if response.status_code == 200:
284
- result = json.loads(response.text)
285
- return result['media_id']
286
- else:
287
- print("HTTP Error:", response.status_code)
288
- return None
289
- except Exception as err:
290
- raise Exception("upload media error", err)
291
-
292
- def get_media(self, media_id):
293
- """
294
- 获取临时素材
295
- :param media_id:
296
- :return: 返回二进制形式
297
- """
298
- try:
299
- self._check_token()
300
- url = "https://qyapi.weixin.qq.com/cgi-bin/media/get"
301
- params = {
302
- "access_token": self.access_token,
303
- "media_id" : media_id
304
- }
305
- response = requests.get(url=url, params=params)
306
- if response.status_code == 200:
307
- content_type = response.headers.get('Content-Type')
308
- if content_type == 'application/json':
309
- response_data = json.loads(response.text)
310
- print("Error:", response_data.get("errmsg"))
311
- return None
312
- else:
313
- return response.content
314
- else:
315
- print("HTTP Error:", response.status_code)
316
- return None
317
- except Exception as err:
318
- raise Exception("get media error", err)
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ -------------------------------------------------
4
+ @version : v1.0
5
+ @author : qsir
6
+ @contact : qsir@vxnote.com
7
+ @software : PyCharm
8
+ @filename : wxwork.py
9
+ @create time: 2025/08/03
10
+ @modify time: 2025/08/03
11
+ @describe :
12
+ -------------------------------------------------
13
+ """
14
+ import json
15
+ import os
16
+ import hashlib
17
+ import base64
18
+ import requests
19
+ from requests_toolbelt import MultipartEncoder
20
+ from datetime import datetime
21
+
22
+ # 通过企微群机器人发送消息
23
+ class WxWorkBot:
24
+ def __init__(self, key):
25
+ self.key = key
26
+
27
+ def upload_media(self, filepath):
28
+ """
29
+ 上传临时素材,给企微群里发文件消息时需要先将文件上传至企微临时素材中
30
+ :param filepath:
31
+ :return: 临时素材的media_id
32
+ """
33
+ try:
34
+ headers = {
35
+ 'Content-Type': 'multipart/form-data',
36
+ }
37
+ with open(filepath, 'rb') as f:
38
+ files = {
39
+ 'media': (os.path.basename(filepath), f.read())
40
+ }
41
+ response = requests.post(
42
+ f'https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={self.key}&type=file',
43
+ headers=headers, files=files)
44
+ response_text = json.loads(response.text)
45
+ if str(response_text.get('errcode')) != '0':
46
+ raise Exception(response_text)
47
+ if response.status_code == 200:
48
+ result = json.loads(response.text)
49
+ return result['media_id']
50
+ else:
51
+ print("HTTP Error:", response.status_code)
52
+ return None
53
+ except Exception as err:
54
+ raise Exception("upload_media error", err)
55
+
56
+ def send_file(self, file_path):
57
+ if not os.path.exists(file_path):
58
+ print('文件不存在: ', file_path)
59
+ return
60
+ """
61
+ 发送文件到群里
62
+ :param file_path:
63
+ :return:
64
+ """
65
+ media_id = self.upload_media(file_path)
66
+ data = {
67
+ "msgtype": "file",
68
+ "file" : {
69
+ "media_id": media_id
70
+ }
71
+ }
72
+ return self.send_msg(data)
73
+
74
+ def send_text(self, content, mentioned_list=None, mentioned_mobile_list=None):
75
+ """
76
+ 发送文本消息
77
+ :param content:
78
+ :param mentioned_list: 需要@的人userid
79
+ :param mentioned_mobile_list: 需要@的人手机号
80
+ :return:
81
+ """
82
+ data = {
83
+ "msgtype": "text",
84
+ "text" : {
85
+ "content": content
86
+ }
87
+ }
88
+ if mentioned_list is not None and mentioned_list:
89
+ data['text'].update({"mentioned_list": mentioned_list})
90
+ if mentioned_mobile_list is not None and mentioned_mobile_list:
91
+ data['text'].update({"mentioned_mobile_list": mentioned_mobile_list})
92
+
93
+ self.send_msg(data)
94
+
95
+ def send_markdown(self, content):
96
+ """
97
+ 发送Markdown消息
98
+ :param content:
99
+ :return:
100
+ """
101
+ data = {
102
+ "msgtype" : "markdown",
103
+ "markdown": {
104
+ "content": content
105
+ }
106
+ }
107
+ self.send_msg(data)
108
+
109
+ def send_notify(self, title, sub_title_list, data_list):
110
+ """
111
+ 发送Markdown消息
112
+ :param content:
113
+ :return:
114
+ """
115
+
116
+ current_date = datetime.now().strftime("%Y-%m-%d")
117
+ header = f"{current_date} {title}\n\n"
118
+
119
+ arr_color = ['warning', 'info', 'warning']
120
+ arr_sub_header = [f"<font color='{arr_color[index]}'>{title}</font>" for index, title in enumerate(sub_title_list)]
121
+ sub_header = "\t".join(arr_sub_header) + "\n\n"
122
+
123
+ # 获取每个元素的行索引和列索引
124
+ arr_content = [
125
+ [
126
+ f'{value}' if col_idx == 0 else f"<font color='{arr_color[col_idx - 1]}'>{value}</font>"
127
+ for col_idx, value in enumerate(row)
128
+ ] # 每行的元素组成一个子列表
129
+ for row_idx, row in enumerate(data_list) # 外层循环控制行
130
+ ]
131
+ # 将二维数组转换为字符串
132
+ content = "\n".join(
133
+ # 对每行的元素列表使用 join(),用 \t 连接
134
+ "\t".join(row) for row in arr_content
135
+ )
136
+
137
+ data = {
138
+ "msgtype" : "markdown",
139
+ "markdown": {
140
+ "content": header + sub_header + content
141
+ }
142
+ }
143
+ self.send_msg(data)
144
+
145
+ def send_img(self, img_path):
146
+ """
147
+ 发送图片消息
148
+ 图片(base64编码前)最大不能超过2M,支持JPG,PNG格式
149
+ :param img_path:
150
+ :return:
151
+ """
152
+ data = {
153
+ "msgtype": "image",
154
+ "image" : {
155
+ "base64": self.img_to_base64(img_path),
156
+ "md5" : self.img_to_md5(img_path)
157
+ }
158
+ }
159
+ self.send_msg(data)
160
+
161
+ def send_news(self, title, description, url, picurl):
162
+ """
163
+ 发送图文消息
164
+ :param title: 标题
165
+ :param description: 描述
166
+ :param url: 跳转URL
167
+ :param picurl: 图文图片地址
168
+ :return:
169
+ """
170
+ data = {
171
+ "msgtype": "news",
172
+ "news" : {
173
+ "articles": [
174
+ {
175
+ "title" : title,
176
+ "description": description,
177
+ "url" : url,
178
+ "picurl" : picurl
179
+ }
180
+ ]
181
+ }
182
+ }
183
+ self.send_msg(data)
184
+
185
+ def send_msg(self, data):
186
+ """
187
+ 发送机器人通用消息到企微群
188
+ :param data: 消息内容json数据
189
+ :return:
190
+ """
191
+ try:
192
+ header = {
193
+ "Content-Type": "application/json"
194
+ }
195
+ response = requests.post(f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={self.key}", headers=header, data=json.dumps(data))
196
+ response_text = json.loads(response.text)
197
+ if str(response_text.get('errcode')) != '0':
198
+ raise Exception(response_text)
199
+ if response.status_code == 200:
200
+ result = json.loads(response.text)
201
+ return result
202
+ else:
203
+ print("HTTP Error:", response.status_code)
204
+ return None
205
+ except Exception as err:
206
+ raise Exception("Send Chat Message error", err)
207
+
208
+ def img_to_md5(self, img_path):
209
+ # 读取图片文件并计算MD5值
210
+ with open(img_path, 'rb') as image_file:
211
+ image_data = image_file.read()
212
+ return hashlib.md5(image_data).hexdigest()
213
+
214
+ def img_to_base64(self, img_path):
215
+ # 读取图片文件并转换为Base64编码
216
+ with open(img_path, 'rb') as image_file:
217
+ image_data = image_file.read()
218
+ return base64.b64encode(image_data).decode('utf-8')
219
+
220
+ # 通过企微应用发送消息
221
+ class WxWorkAppBot:
222
+ def __init__(self, corpid, corpsecret, agentid):
223
+ self.corpid = corpid
224
+ self.corpsecret = corpsecret
225
+ self.agentid = agentid
226
+ self.access_token = self._getToken()
227
+
228
+ def _getToken(self):
229
+ try:
230
+ if all([self.corpid, self.corpsecret]):
231
+ url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corpid}&corpsecret={corpsecret}".format(
232
+ corpid=self.corpid, corpsecret=self.corpsecret)
233
+ response = requests.get(url)
234
+ if response.status_code == 200:
235
+ result = json.loads(response.text)
236
+ return result['access_token']
237
+ else:
238
+ print("HTTP Error:", response.status_code)
239
+ return None
240
+ except Exception as err:
241
+ raise Exception("get WeChat access Token error", err)
242
+
243
+ def _send_msg(self, data):
244
+ self._check_token()
245
+ try:
246
+ send_url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}".format(
247
+ access_token=self.access_token)
248
+ response = requests.post(send_url, json.dumps(data))
249
+ if response.status_code == 200:
250
+ result = json.loads(response.text)
251
+ return result
252
+ else:
253
+ print("HTTP Error:", response.status_code)
254
+ return None
255
+ except Exception as err:
256
+ raise Exception("send WeChat Message error", err)
257
+
258
+ def _check_token(self):
259
+ if self.access_token is None:
260
+ self._getToken()
261
+
262
+ def send_msg(self, data):
263
+ return self._send_msg(data)
264
+
265
+ def upload_media(self, filetype, filepath, filename):
266
+ """
267
+ 上传临时素材到企微并获取media_id
268
+ :param filetype: 图片(image)、语音(voice)、视频(video),普通文件(file)
269
+ :param filepath:
270
+ :param filename:
271
+ :return: media_id
272
+ """
273
+ try:
274
+ self._check_token()
275
+ post_file_url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type={filetype}".format(
276
+ filetype=filetype,
277
+ access_token=self.access_token)
278
+
279
+ m = MultipartEncoder(
280
+ fields={filename: (filename, open(filepath + filename, 'rb'), 'text/plain')},
281
+ )
282
+ response = requests.post(url=post_file_url, data=m, headers={'Content-Type': m.content_type})
283
+ if response.status_code == 200:
284
+ result = json.loads(response.text)
285
+ return result['media_id']
286
+ else:
287
+ print("HTTP Error:", response.status_code)
288
+ return None
289
+ except Exception as err:
290
+ raise Exception("upload media error", err)
291
+
292
+ def get_media(self, media_id):
293
+ """
294
+ 获取临时素材
295
+ :param media_id:
296
+ :return: 返回二进制形式
297
+ """
298
+ try:
299
+ self._check_token()
300
+ url = "https://qyapi.weixin.qq.com/cgi-bin/media/get"
301
+ params = {
302
+ "access_token": self.access_token,
303
+ "media_id" : media_id
304
+ }
305
+ response = requests.get(url=url, params=params)
306
+ if response.status_code == 200:
307
+ content_type = response.headers.get('Content-Type')
308
+ if content_type == 'application/json':
309
+ response_data = json.loads(response.text)
310
+ print("Error:", response_data.get("errmsg"))
311
+ return None
312
+ else:
313
+ return response.content
314
+ else:
315
+ print("HTTP Error:", response.status_code)
316
+ return None
317
+ except Exception as err:
318
+ raise Exception("get media error", err)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qrpa
3
- Version: 1.0.28
3
+ Version: 1.0.29
4
4
  Summary: qsir's rpa library
5
5
  Author: QSir
6
6
  Author-email: QSir <1171725650@qq.com>
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