Functions-d 1.0.0__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.
@@ -0,0 +1,882 @@
1
+ # -*- coding:utf-8 -*-
2
+ import datetime
3
+ import inspect
4
+ import json
5
+ import logging
6
+ import os
7
+ import shutil
8
+ import time
9
+ import urllib.request
10
+ import traceback
11
+ import pandas as pd
12
+ import WeComMsg
13
+ import xlwings as xw
14
+ import yagmail
15
+ import smtplib
16
+ from email.mime.multipart import MIMEMultipart
17
+ from email.mime.text import MIMEText
18
+ from email.mime.application import MIMEApplication
19
+ import HiveClients
20
+ import HiveClient
21
+ from PIL import ImageGrab, Image
22
+ from bs4 import BeautifulSoup
23
+ import re
24
+ import xlsxwriter
25
+ import numpy as np
26
+ # 新增的库(Edge浏览器需要)
27
+ from pypinyin import lazy_pinyin
28
+ from selenium import webdriver
29
+ from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
30
+ from selenium import webdriver
31
+ from selenium.webdriver.edge.service import Service
32
+ from selenium.webdriver.edge.options import Options
33
+ from webdriver_manager.microsoft import EdgeChromiumDriverManager
34
+ import platform
35
+ import requests
36
+
37
+
38
+ class DataProcessingAndMessaging:
39
+ def __init__(self):
40
+ # 获取调用者的堆栈信息
41
+ caller_frame = inspect.stack()[1]
42
+ # 获取调用者的文件名
43
+ caller_filename = caller_frame.filename
44
+ # 获取主脚本的基本名称(不包含路径和后缀)
45
+ log_file = os.path.splitext(os.path.basename(caller_filename))[0] + ".log"
46
+ # 初始化日志记录
47
+ logging.basicConfig(filename=log_file, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
48
+ self.logger = logging.getLogger()
49
+
50
+ self.logger.setLevel(logging.INFO)
51
+ # 避免重复添加处理器
52
+ if not self.logger.handlers:
53
+ # 文件处理器(保存到日志文件)
54
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
55
+ file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
56
+ # 控制台处理器(方便开发调试)
57
+ console_handler = logging.StreamHandler()
58
+ console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
59
+ # 添加处理器
60
+ self.logger.addHandler(file_handler)
61
+ self.logger.addHandler(console_handler)
62
+
63
+
64
+ self.logger.info("初始化 DataProcessingAndMessaging 类")
65
+ # print("初始化 DataProcessingAndMessaging 类")
66
+ self.start_time = None
67
+ self.current_script_name = None
68
+ self.log_filename = None
69
+ self.current_script_names = None
70
+ self.current_path = None
71
+ self.path = None
72
+
73
+ self.corpid = "wxd4e113eb4c0136b9"
74
+ self.corpsecret = "PMfPOv2Qqq0iXZAdWHF7WdaW4kkWUZcwyGE4NZtve3k"
75
+ self.agentid = "1000026"
76
+
77
+
78
+
79
+ def init_edge_driver(self, headless=True):
80
+ """初始化Edge驱动(彻底移除architecture参数,用环境变量指定32位)"""
81
+ # 1. 强制指定32位驱动(适配旧版本webdriver-manager)
82
+ os.environ['WDM_ARCH'] = 'x86' # 关键:通过环境变量指定32位,无需architecture参数
83
+
84
+ # 2. 创建Edge浏览器选项
85
+ edge_options = Options()
86
+ edge_options.add_argument('--disable-gpu')
87
+ edge_options.add_argument('--no-sandbox')
88
+ edge_options.add_argument('--ignore-certificate-errors')
89
+
90
+ if headless:
91
+ edge_options.add_argument('--headless=new')
92
+ edge_options.add_argument('--window-size=1920,1080')
93
+
94
+ # 3. 初始化驱动(不传入任何architecture参数)
95
+ service = Service(EdgeChromiumDriverManager().install()) # 此处必须移除architecture参数
96
+
97
+ # 4. 启动浏览器
98
+ driver = webdriver.Edge(service=service, options=edge_options)
99
+ self.logger.info("Edge浏览器初始化成功(适配旧版本webdriver-manager)")
100
+ return driver
101
+
102
+ def Start_Get_filepath_and_filename(self):
103
+ self.start_time = time.time()
104
+ # 获取调用者的堆栈信息
105
+ caller_frame = inspect.stack()[1]
106
+ # 获取调用者的文件名
107
+ self.current_script_name = caller_frame.filename
108
+ self.log_filename = os.path.splitext(self.current_script_name)[0] + ".log"
109
+ self.current_script_names = os.path.basename(self.current_script_name)
110
+ self.current_path = os.path.dirname(os.path.abspath(self.current_script_name))
111
+ # 修改 here,确保 path 指向主脚本所在的目录
112
+ self.path = self.current_path + os.sep # 直接使用 current_path
113
+ print(f"当前时间:{self.get_date_and_time('%Y-%m-%d %H:%M:%S', 0)}")
114
+ print(f"开始执行脚本:{self.current_script_names}")
115
+ self.logger.info(f"开始执行脚本:{self.current_script_names}")
116
+
117
+ def End_operation(self):
118
+ print(f"脚本:{self.current_script_names} 执行成功")
119
+ self.logger.info(f"脚本:{self.current_script_names} 执行成功")
120
+ end_time = time.time() # 结束运行时间
121
+ elapsed_time = round(end_time - self.start_time, 0)
122
+ print(f"运行时间:{elapsed_time} 秒")
123
+ self.logger.info(f"运行时间:{elapsed_time} 秒")
124
+ self.logger.info('\n' * 10)
125
+
126
+ def uxin_wx(self, name, message, mentioned_list=None):
127
+ # corpid = "wxd4e113eb4c0136b9"
128
+ # corpsecret = "PMfPOv2Qqq0iXZAdWHF7WdaW4kkWUZcwyGE4NZtve3k"
129
+ # agentid = "1000026"
130
+ sender = WeComMsg.WeChatWorkSender(self.corpid, self.corpsecret, self.agentid)
131
+ try:
132
+ # 记录发送对象和消息类型
133
+ target_type = "群聊(Webhook)" if name.startswith("https://") else "用户"
134
+ self.logger.info(f"开始向{target_type}发送消息,目标:{name}")
135
+
136
+ if name.startswith("https://"): # 群聊Webhook
137
+ if isinstance(message, str) and message.endswith(('.xlsx', '.docx', '.pdf', '.txt')) and os.path.isfile(
138
+ message):
139
+ # 发送群聊文件
140
+ file_name = os.path.basename(message)
141
+ file_size = os.path.getsize(message) / 1024
142
+ self.logger.info(f"发送群聊文件消息:文件名={file_name},大小={file_size:.2f}KB")
143
+ result = sender.send_file_to_group(name, message)
144
+ # 提取并记录message_id
145
+ msg_id = result.get('msgid', '未知') # 群聊消息用msgid
146
+ self.logger.info(f"群聊文件消息发送结果:{'成功' if result.get('errcode') == 0 else '失败'},"
147
+ f"错误信息:{result.get('errmsg')},消息ID:{msg_id}")
148
+
149
+ elif isinstance(message, str):
150
+ # 发送群聊文本
151
+ at_info = f",@对象:{mentioned_list}" if mentioned_list else ""
152
+ self.logger.info(f"发送群聊文本消息:内容={message}{at_info}")
153
+ result = sender.send_text_to_group(name, message, mentioned_list=mentioned_list)
154
+ # 提取并记录message_id
155
+ msg_id = result.get('msgid', '未知')
156
+ self.logger.info(f"群聊文本消息发送结果:{'成功' if result.get('errcode') == 0 else '失败'},"
157
+ f"错误信息:{result.get('errmsg')},消息ID:{msg_id}")
158
+
159
+ else:
160
+ err_msg = "不支持的群聊消息类型"
161
+ print(err_msg)
162
+ self.logger.warning(err_msg)
163
+ return
164
+
165
+ else: # 个人用户
166
+ if isinstance(message, str) and message.endswith(('.jpg', '.jpeg', '.png', '.gif')) and os.path.isfile(
167
+ message):
168
+ # 发送个人图片
169
+ img_name = os.path.basename(message)
170
+ img_size = os.path.getsize(message) / 1024
171
+ self.logger.info(f"发送个人图片消息:图片名={img_name},大小={img_size:.2f}KB")
172
+ result = sender.send_image([name], message)
173
+ # 提取并记录message_id
174
+ msg_id = result.get('msgid', '未知') # 个人消息用msgid
175
+ self.logger.info(f"个人图片消息发送结果:{'成功' if result.get('errcode') == 0 else '失败'},"
176
+ f"错误信息:{result.get('errmsg')},消息ID:{msg_id}")
177
+
178
+ elif isinstance(message, str) and message.endswith(
179
+ ('.xlsx', '.docx', '.pdf', '.txt', 'xls', 'csv')) and os.path.isfile(message):
180
+ # 发送个人文件
181
+ file_name = os.path.basename(message)
182
+ file_size = os.path.getsize(message) / 1024
183
+ self.logger.info(f"发送个人文件消息:文件名={file_name},大小={file_size:.2f}KB")
184
+ result = sender.send_file([name], message)
185
+ # 提取并记录message_id
186
+ msg_id = result.get('msgid', '未知')
187
+ self.logger.info(f"个人文件消息发送结果:{'成功' if result.get('errcode') == 0 else '失败'},"
188
+ f"错误信息:{result.get('errmsg')},消息ID:{msg_id}")
189
+
190
+ elif isinstance(message, str):
191
+ # 发送个人文本
192
+ self.logger.info(f"发送个人文本消息:内容={message}")
193
+ result = sender.send_text([name], message)
194
+ # 提取并记录message_id
195
+ msg_id = result.get('msgid', '未知')
196
+ self.logger.info(f"个人文本消息发送结果:{'成功' if result.get('errcode') == 0 else '失败'},"
197
+ f"错误信息:{result.get('errmsg')},消息ID:{msg_id}")
198
+
199
+ else:
200
+ err_msg = "不支持的个人消息类型"
201
+ print(err_msg)
202
+ self.logger.warning(err_msg)
203
+ return
204
+
205
+ # 控制台输出结果
206
+ if result.get('errcode') == 0:
207
+ print(f"给 {name} 的消息发送成功,消息ID:{result.get('msgid', '未知')}")
208
+ else:
209
+ print(f"给 {name} 的消息发送失败,错误码:{result.get('errcode')},"
210
+ f"错误信息:{result.get('errmsg')},消息ID:{result.get('msgid', '未知')}")
211
+
212
+ except Exception as e:
213
+ self.logger.error(f"消息发送失败,报错信息: {e}", exc_info=True)
214
+ print(f"发送失败,报错信息: {e}")
215
+
216
+ def recall_message(self, msgid):
217
+ try:
218
+ self.logger.info(f"开始撤回消息,msgid: {msgid}")
219
+ sender = wecom.WeChatWorkSender(self.corpid, self.corpsecret, self.agentid)
220
+ result = sender.recall_message(msgid)
221
+
222
+ # 记录撤回结果
223
+ if result.get('errcode') == 0:
224
+ self.logger.info(f"消息撤回成功,msgid: {msgid}")
225
+ print(f"消息撤回成功,msgid: {msgid}")
226
+ else:
227
+ err_msg = f"消息撤回失败,错误码: {result.get('errcode')}, 错误信息: {result.get('errmsg')}"
228
+ self.logger.warning(err_msg)
229
+ print(err_msg)
230
+
231
+ return result
232
+ except Exception as e:
233
+ err_msg = f"撤回消息时发生错误: {str(e)}"
234
+ self.logger.error(err_msg, exc_info=True)
235
+ print(err_msg)
236
+ return
237
+
238
+ def Get_update_time(self, data_table):
239
+ url4 = f'http://cptools.xin.com/hive/getLastUpdateTime?table={data_table}'
240
+ res = urllib.request.Request(url4)
241
+ response = urllib.request.urlopen(res)
242
+ html = response.read()
243
+ soup = BeautifulSoup(html, "lxml")
244
+ someData = soup.select("p")
245
+ json_data = json.loads(someData[0].text)
246
+ d_time = json_data['data']
247
+ d_code = json_data['code']
248
+ d_message = json_data['message']
249
+ # print(d_time, d_code, d_message)
250
+ utc_time = datetime.datetime.utcfromtimestamp(int(d_time))
251
+ beijing_time = utc_time + datetime.timedelta(hours=8)
252
+ self.logger.info(f'更新时间:{beijing_time}')
253
+ print(f'更新时间:{beijing_time}')
254
+ return beijing_time
255
+
256
+ def extract_main_table_from_sql(self, sql_query):
257
+ lines = sql_query.split('\n')
258
+ from_line = None
259
+ for line in lines:
260
+ if line.strip().lower().startswith('from'):
261
+ parts = line.strip().split('from', 1)
262
+ if len(parts) > 1:
263
+ from_table_info = parts[1].strip()
264
+ # 去除可能存在的别名
265
+ table_name = from_table_info.split(' ')[0]
266
+ from_line = table_name
267
+ break
268
+ self.logger.info(f'数据表:{from_line}')
269
+ print(f'数据表:{from_line}')
270
+ return from_line
271
+
272
+ def replace_day(self, sqls, day_num):
273
+ today = datetime.date.today()
274
+ oneday = datetime.timedelta(days=day_num)
275
+ yesterday = str(today - oneday)
276
+ yesterday = yesterday.replace('-', '')
277
+ yesterday_m = yesterday[0:6]
278
+ sqls = sqls.replace('$dt_ymd', yesterday)
279
+ sqls = sqls.replace('$dt_ym', yesterday_m)
280
+ return sqls
281
+
282
+ def get_date_and_time(self, format_type, days):
283
+ today = datetime.datetime.today()
284
+ target_date = today - datetime.timedelta(days=days)
285
+ result = target_date.strftime(format_type)
286
+ return result
287
+
288
+ def sende_email(self, name, contact_name, title, rec, file, cc=False, bcc=None):
289
+ yag = yagmail.SMTP(user='cc_yingxiao@xin.com', password='cw46pfeznNQx', host='mail.xin.com', port='587',
290
+ smtp_ssl=False, smtp_starttls=True)
291
+ contents = f'{name} 好:\n \n ' \
292
+ f'附件为{title},请查收!\n \n' \
293
+ f'如有疑问请联系{contact_name},谢谢~'
294
+ if cc and bcc:
295
+ yag.send(rec, title, contents, file, cc, bcc)
296
+ elif cc:
297
+ yag.send(rec, title, contents, file, cc)
298
+ elif bcc:
299
+ yag.send(rec, title, contents, file, bcc)
300
+ else:
301
+ yag.send(rec, title, contents, file)
302
+ self.logger.info(f'邮件主题:{title} \n邮件附件:{file} 发送完成')
303
+ print(f'邮件主题:{title} \n邮件附件:{file} 发送完成')
304
+
305
+
306
+ def run_sql(self, path, sql_name, channel=False, sql_content=None):
307
+ """
308
+ 执行SQL(支持直接传入SQL内容或从文件读取)
309
+
310
+ :param path: SQL文件所在路径
311
+ :param sql_name: SQL文件名(当sql_content为None时有效)
312
+ :param channel: 是否需要替换日期变量
313
+ :param sql_content: 直接传入的SQL内容(可选,优先级高于文件读取)
314
+ :return: 查询结果(非DDL操作时)
315
+ """
316
+ if path is None:
317
+ raise ValueError("path cannot be None")
318
+
319
+ # 优先使用传入的SQL内容,否则从文件读取
320
+ if sql_content is None:
321
+ sql_file_path = os.path.join(path, sql_name)
322
+ # self.logger.info(f"准备执行SQL文件:{sql_file_path}")
323
+ # print(f"准备执行SQL文件:{sql_file_path}")
324
+
325
+ # 读取SQL文件
326
+ try:
327
+ with open(sql_file_path, encoding='utf-8') as sql_file:
328
+ sql = sql_file.read()
329
+ self.logger.info(f"SQL文件内容读取成功,长度:{len(sql)}字符")
330
+ except FileNotFoundError:
331
+ error_msg = f"SQL文件不存在:{sql_file_path}"
332
+ self.logger.error(error_msg)
333
+ raise FileNotFoundError(error_msg)
334
+ except Exception as e:
335
+ error_msg = f"读取SQL文件失败:{str(e)}"
336
+ self.logger.error(error_msg)
337
+ raise
338
+ else:
339
+ sql = sql_content
340
+ sql_file_path = f"[直接传入的SQL内容]"
341
+ self.logger.info(f"使用直接传入的SQL内容,长度:{len(sql)}字符")
342
+
343
+ try:
344
+ # 替换日期变量(如果需要)
345
+ if channel:
346
+ original_sql = sql # 保存原始SQL用于日志
347
+ sql = self.replace_day(sql, 0)
348
+ self.logger.info(f"已替换SQL中的日期变量(channel=True)")
349
+ self.logger.info(f"替换前SQL:\n{original_sql}\n替换后SQL:\n{sql}")
350
+
351
+ # 连接Hive并执行
352
+ self.logger.info(f"开始连接Hive服务器(地址:172.20.2.190:10023)")
353
+ hive_client = HiveClient.HiveClient('172.20.2.190', 10023, 'cc_yingxiao', 'e147bbed39c810e32f7842cf5f59b9ae')
354
+ self.logger.info(f"Hive连接成功,开始执行SQL:{sql_file_path}")
355
+ print(f"插入表格:开始执行 sql:{sql_file_path}" if channel else f"开始执行 sql:{sql_file_path}")
356
+
357
+ # 执行SQL(区分DDL和查询)
358
+ if channel:
359
+ # 执行DDL操作(无返回数据)
360
+ hive_client.ddls(sql)
361
+ self.logger.info(f"SQL执行成功(DDL):{sql_file_path}")
362
+ print(f"插入表格:{sql_file_path} 执行完成")
363
+ return None
364
+ else:
365
+ # 执行查询(返回数据)
366
+ data = hive_client.pdre(sql)
367
+
368
+ # 计算返回数据行数
369
+ if isinstance(data, pd.DataFrame):
370
+ row_count = len(data) if not data.empty else 0
371
+ else:
372
+ row_count = len(data) if data else 0
373
+ self.logger.info(f"SQL执行成功(查询):{sql_file_path},返回数据行数:{row_count}")
374
+
375
+ # 记录查询结果(前10行+后10行)
376
+ if row_count > 0:
377
+ if isinstance(data, pd.DataFrame):
378
+ data_str = data.to_string()
379
+ else:
380
+ data_str = str(data)
381
+
382
+ data_lines = data_str.split('\n')
383
+ if len(data_lines) <= 20:
384
+ self.logger.info(f"Hive查询结果完整数据:\n{data_str}")
385
+ else:
386
+ head_lines = '\n'.join(data_lines[:10])
387
+ tail_lines = '\n'.join(data_lines[-10:])
388
+ self.logger.info(
389
+ f"Hive查询结果(共{row_count}行,仅显示前10行和后10行):\n"
390
+ f"前10行:\n{head_lines}\n...\n后10行:\n{tail_lines}"
391
+ )
392
+
393
+ print(f"{sql_file_path} 执行完成,返回数据行数:{row_count}")
394
+ return data
395
+
396
+ except Exception as e:
397
+ # 错误处理逻辑
398
+ error_summary = f"SQL执行失败(来源:{sql_file_path}):{str(e)}"
399
+ self.logger.error(error_summary)
400
+ full_error_details = repr(e)
401
+ self.logger.error(f"Hive原始错误详情(完整内容):\n{full_error_details}")
402
+ stack_trace = traceback.format_exc()
403
+ self.logger.error(f"错误堆栈信息:\n{stack_trace}")
404
+ print(f"SQL执行失败:{error_summary}")
405
+ print(f"Hive原始错误详情:\n{full_error_details}")
406
+ raise
407
+
408
+ def run_sql_2(self, path, sql_name, channel=False, sql_content=None):
409
+ """
410
+ 执行SQL(支持直接传入SQL内容或从文件读取)- 优化版Hive连接
411
+ 功能与run_sql完全一致,仅调整HiveClient初始化方式(适配默认数据库、简化参数)
412
+
413
+ :param path: SQL文件所在路径
414
+ :param sql_name: SQL文件名(当sql_content为None时有效)
415
+ :param channel: 是否需要替换日期变量($dt_ymd/$dt_ym)
416
+ :param sql_content: 直接传入的SQL内容(可选,优先级高于文件读取)
417
+ :return: 查询结果(非DDL操作时返回DataFrame,DDL操作返回None)
418
+ """
419
+ if path is None:
420
+ raise ValueError("path cannot be None")
421
+
422
+ # 1. 处理SQL来源(优先用传入的sql_content,否则读文件)
423
+ if sql_content is None:
424
+ sql_file_path = os.path.join(path, sql_name)
425
+ self.logger.info(f"[run_sql_2] 准备执行SQL文件:{sql_file_path}")
426
+ print(f"[run_sql_2] 准备执行SQL文件:{sql_file_path}")
427
+
428
+ # 读取SQL文件(处理文件不存在/读取错误)
429
+ try:
430
+ with open(sql_file_path, encoding='utf-8') as sql_file:
431
+ sql = sql_file.read()
432
+ self.logger.info(f"[run_sql_2] SQL文件内容读取成功,长度:{len(sql)}字符")
433
+ except FileNotFoundError:
434
+ error_msg = f"[run_sql_2] SQL文件不存在:{sql_file_path}"
435
+ self.logger.error(error_msg)
436
+ raise FileNotFoundError(error_msg)
437
+ except Exception as e:
438
+ error_msg = f"[run_sql_2] 读取SQL文件失败:{str(e)}"
439
+ self.logger.error(error_msg)
440
+ raise
441
+ else:
442
+ sql = sql_content
443
+ sql_file_path = f"[直接传入的SQL内容]"
444
+ self.logger.info(f"[run_sql_2] 使用直接传入的SQL内容,长度:{len(sql)}字符")
445
+
446
+ try:
447
+ # 2. 替换日期变量(channel=True时生效,兼容原逻辑)
448
+ if channel:
449
+ original_sql = sql # 保存原始SQL用于日志回溯
450
+ sql = self.replace_day(sql, 0)
451
+ self.logger.info(f"[run_sql_2] 已替换SQL中的日期变量($dt_ymd→昨日日期,$dt_ym→昨日年月)")
452
+ self.logger.debug(f"[run_sql_2] 替换前SQL:\n{original_sql}\n替换后SQL:\n{sql}")
453
+
454
+ # 3. 初始化Hive连接(优化点:使用默认数据库dw_cc,简化参数传递)
455
+ self.logger.info(f"[run_sql_2] 开始连接Hive服务器(地址:172.20.2.190:10023,默认数据库:default)")
456
+ # 调用优化后的HiveClient(已设置database='dw_cc'默认值,无需显式传库名)
457
+ hive_client = HiveClients.HiveClients(
458
+ host='172.20.2.190',
459
+ port=10023,
460
+ username='cc_yingxiao',
461
+ password='e147bbed39c810e32f7842cf5f59b9ae',
462
+ auth='LDAP', # 显式指定认证方式,避免默认值不一致
463
+ database = 'default'
464
+ )
465
+ self.logger.info(f"[run_sql_2] Hive连接成功,开始执行SQL:{sql_file_path}")
466
+ # 控制台打印与原run_sql一致的提示
467
+ print(f"[run_sql_2] 插入表格:开始执行 sql:{sql_file_path}" if channel
468
+ else f"[run_sql_2] 开始执行 sql:{sql_file_path}")
469
+
470
+ # 4. 执行SQL(区分DDL和查询,逻辑与原run_sql完全一致)
471
+ if channel:
472
+ # 执行DDL操作(建表/插入等无返回数据的操作)
473
+ hive_client.ddls(sql)
474
+ self.logger.info(f"[run_sql_2] SQL执行成功(DDL):{sql_file_path}")
475
+ print(f"[run_sql_2] 插入表格:{sql_file_path} 执行完成")
476
+ return None
477
+ else:
478
+ # 执行查询操作(返回DataFrame,含中文编码处理)
479
+ data = hive_client.pdre(sql)
480
+
481
+ # 统计返回数据行数(兼容DataFrame和空结果)
482
+ if isinstance(data, pd.DataFrame):
483
+ row_count = len(data) if not data.empty else 0
484
+ else:
485
+ row_count = len(data) if data else 0
486
+ self.logger.info(f"[run_sql_2] SQL执行成功(查询):{sql_file_path},返回数据行数:{row_count}")
487
+
488
+ # 日志记录查询结果(前10行+后10行,避免大数据量日志冗余)
489
+ if row_count > 0:
490
+ data_str = data.to_string() if isinstance(data, pd.DataFrame) else str(data)
491
+ data_lines = data_str.split('\n')
492
+ if len(data_lines) <= 20:
493
+ self.logger.info(f"[run_sql_2] Hive查询结果完整数据:\n{data_str}")
494
+ else:
495
+ head_lines = '\n'.join(data_lines[:10])
496
+ tail_lines = '\n'.join(data_lines[-10:])
497
+ self.logger.info(
498
+ f"[run_sql_2] Hive查询结果(共{row_count}行,仅显示前10行和后10行):\n"
499
+ f"前10行:\n{head_lines}\n...\n后10行:\n{tail_lines}"
500
+ )
501
+
502
+ # 控制台输出与原run_sql一致的结果提示
503
+ print(f"[run_sql_2] {sql_file_path} 执行完成,返回数据行数:{row_count}")
504
+ return data
505
+
506
+ # 5. 错误处理(保留原run_sql的完整日志记录逻辑)
507
+ except Exception as e:
508
+ error_summary = f"[run_sql_2] SQL执行失败(来源:{sql_file_path}):{str(e)}"
509
+ self.logger.error(error_summary)
510
+ # 记录Hive原始错误详情(含TExecuteStatementResp完整信息)
511
+ full_error_details = repr(e)
512
+ self.logger.error(f"[run_sql_2] Hive原始错误详情(完整内容):\n{full_error_details}")
513
+ # 记录完整堆栈信息(便于定位代码问题)
514
+ stack_trace = traceback.format_exc()
515
+ self.logger.error(f"[run_sql_2] 错误堆栈信息:\n{stack_trace}")
516
+ # 控制台打印错误(实时调试用)
517
+ print(f"[run_sql_2] SQL执行失败:{error_summary}")
518
+ print(f"[run_sql_2] Hive原始错误详情:\n{full_error_details}")
519
+ # 重新抛出异常,确保上层逻辑能感知失败(如中断脚本、发送告警)
520
+ raise
521
+
522
+
523
+ def writer_excel_data(self, path, filename, send_file, sheet_data, headers):
524
+ self.logger.info('开始处理Excel表格')
525
+ print('开始处理Excel表格')
526
+ filename = path + filename # 模板名
527
+ send_file = send_file # 附件名
528
+ dfs = []
529
+ sheet_names = []
530
+ clear_ranges = []
531
+ date_ranges = []
532
+ for sheet in sheet_data: # 循环清除
533
+ dfs.append(sheet['data'])
534
+ sheet_names.append(sheet['sheet_name'])
535
+ clear_ranges.append(sheet['clear_range'])
536
+ date_ranges.append(sheet['date_range'])
537
+ app = xw.App(visible=False, add_book=False)
538
+ wb = app.books.open(filename)
539
+
540
+ for i in range(0, len(dfs)):
541
+ sheet_name = sheet_names[i]
542
+ # 检查sheet是否存在,不存在则创建
543
+ if sheet_name not in [sheet.name for sheet in wb.sheets]:
544
+ wb.sheets.add(name=sheet_name)
545
+ wb.sheets[sheet_name].range(clear_ranges[i]).clear_contents() # 选择清除数据的位置
546
+ wb.sheets[sheet_name].range(date_ranges[i]).options(index=False, header=headers).value = dfs[i] # 选择粘贴数据的位置
547
+
548
+ wb.save()
549
+ wb.close()
550
+ app.quit()
551
+ shutil.copyfile(filename, send_file) # 复制表格
552
+ self.logger.info(f'表格 {os.path.basename(send_file)} 处理完成')
553
+ print(f'表格 {os.path.basename(send_file)} 处理完成')
554
+
555
+
556
+
557
+ def Yesterday_data_num(self, data, sql_name, columns, num):
558
+ self.logger.info(f'检查{sql_name}表中昨日数据数量')
559
+ print(f'检查{sql_name}表中昨日数据数量')
560
+ df = data[[columns]].copy()
561
+ df = df[~df[columns].isnull()]
562
+ df.loc[:, 'date'] = pd.to_datetime(df[columns]).dt.strftime('%Y/%m/%d')
563
+ df_filter = df[df['date'] == self.get_date_and_time('%Y/%m/%d', 1)]
564
+ df_filter_group = df_filter.groupby(['date']).agg({
565
+ columns: 'count'
566
+ }).reset_index(drop=False)
567
+ df_filter_group.rename(columns={columns: '昨日数据量'}, inplace=True)
568
+ df_num = df_filter_group['昨日数据量']
569
+ if pd.isnull(df_filter_group['昨日数据量']).any():
570
+ self.logger.warning(f'警告:数据表 {sql_name} 昨日数据量为0,请尽快检查数据')
571
+ print(f'警告:数据表 {sql_name} 昨日数据量为0,请尽快检查数据')
572
+ self.uxin_wx('dongyang', f'警告:数据表 {sql_name} 昨日数据量为0,请尽快检查数据')
573
+ else:
574
+ df_num_value = df_num.iloc[0] if len(df_num) > 0 else 0
575
+ if df_num_value < num:
576
+ self.logger.warning(
577
+ f'警告:数据表 {sql_name} 昨日数据量不足{num},只有{df_num_value},请尽快检查数据')
578
+ print(f'警告:数据表 {sql_name} 昨日数据量不足{num},只有{df_num_value},请尽快检查数据')
579
+ self.uxin_wx('dongyang',
580
+ f'警告:数据表 {sql_name} 昨日数据量不足{num},只有{df_num_value},请尽快检查数据')
581
+
582
+ def screen(self, filename, sheetname, screen_area, img_name):
583
+ self.logger.info('开始截图')
584
+ print('开始截图')
585
+ # pythoncom.CoInitialize() # 多线程
586
+ app = xw.App(visible=False, add_book=False)
587
+ # app.display_alerts = False
588
+ # app.screen_updating = False
589
+ wb = app.books.open(filename)
590
+ sht = wb.sheets[sheetname]
591
+ range_val = sht.range(screen_area)
592
+ range_val.api.CopyPicture()
593
+ sht.api.Paste()
594
+ pic = sht.pictures[0] # 当前图片
595
+ pic.api.Copy() # 复制图片
596
+ while True:
597
+ img = ImageGrab.grabclipboard() # 获取剪贴板的图片数据
598
+ if img is not None:
599
+ break
600
+ # 如果图片已存在则覆盖
601
+ if os.path.exists(img_name):
602
+ os.remove(img_name)
603
+ img.save(img_name) # 保存图片
604
+ pic.delete() # 删除sheet上的图片
605
+
606
+ # 设置图片尺寸
607
+ def get_FileSize(img_name):
608
+ # filePath = unicode(filePath,'utf8')
609
+ fsize = os.path.getsize(img_name)
610
+ fsize = fsize / float(1024)
611
+ return round(fsize, 2)
612
+
613
+ # 当图片大小小于100k时更改图片尺寸
614
+ def Change_size(img_name):
615
+ p_size = self.get_FileSize(img_name)
616
+ if p_size < 101:
617
+ sImg = Image.open(img_name) # 图片位置
618
+ w, h = sImg.size
619
+ dImg = sImg.resize((int(w * 1.1), int(h * 1.1)), Image.LANCZOS) # 设置压缩尺寸和选项,注意尺寸要用括号
620
+ dImg.save(img_name) # 图片位置
621
+
622
+ Change_size(img_name)
623
+ wb.close() # 关闭excel
624
+ app.quit()
625
+ # pythoncom.CoUninitialize()
626
+ self.logger.info(f'图片:{img_name} 截图并保存完成')
627
+ print(f'图片:{img_name} 截图并保存完成')
628
+
629
+ def column_label(self, n):
630
+ result = ""
631
+ while n > 0:
632
+ n, remainder = divmod(n - 1, 26)
633
+ result = chr(65 + remainder) + result
634
+ return result
635
+
636
+ def excel_catch_screen(self, data, path, filename, sheet_name, start_range, image_filename):
637
+ image_path = path + image_filename + '.png'
638
+ self.screen(filename, sheet_name,
639
+ f"{start_range}:%s" % (self.column_label(len(data.columns)) + str(len(data) + 1)), image_path)
640
+
641
+ def send_email_new(self, recipient_emails, cc_emails=None, bcc_emails=None, subject="", html_body="", attachments=None):
642
+ sender_email = 'cc_yingxiao@xin.com'
643
+ sender_password = 'cw46pfeznNQx'
644
+ # 创建一个多部分消息
645
+ msg = MIMEMultipart()
646
+ msg['From'] = sender_email
647
+ msg['To'] = ', '.join(recipient_emails)
648
+ if cc_emails:
649
+ msg['Cc'] = ', '.join(cc_emails)
650
+ if bcc_emails:
651
+ msg['Bcc'] = ', '.join(bcc_emails)
652
+ msg['Subject'] = subject
653
+
654
+ # 添加 HTML 正文
655
+ body = MIMEText(html_body, 'html')
656
+ msg.attach(body)
657
+
658
+ # 添加附件
659
+ if attachments:
660
+ for attachment in attachments:
661
+ with open(attachment, 'rb') as file:
662
+ # 使用 os.path.basename 来获取正确的文件名
663
+ part = MIMEApplication(file.read(), Name=os.path.basename(attachment))
664
+ part['Content-Disposition'] = f'attachment; filename="{os.path.basename(attachment)}"'
665
+ msg.attach(part)
666
+
667
+ # 连接到 SMTP 服务器
668
+ with smtplib.SMTP('mail.xin.com', 587, timeout=120) as smtp:
669
+ # smtp.starttls()
670
+ smtp.login(sender_email, sender_password)
671
+ all_recipients = recipient_emails
672
+ if cc_emails:
673
+ all_recipients += cc_emails
674
+ if bcc_emails:
675
+ all_recipients += bcc_emails
676
+ smtp.sendmail(sender_email, all_recipients, msg.as_string())
677
+
678
+ # def insert_df_to_hive(self, df, data_table):
679
+ # hive_client = HiveClient('172.20.2.190', 10023, 'cc_yingxiao', 'e147bbed39c810e32f7842cf5f59b9ae')
680
+ # hive_client.insert_df_to_hive(df, data_table)
681
+
682
+ def format_excel_worksheet(self, worksheet, df, workbook):
683
+ # self.logger.info('开始格式化 Excel 工作表')
684
+ # print('开始格式化 Excel 工作表')
685
+ # 定义日期格式(包含边框和居中样式)
686
+ date_format = workbook.add_format({
687
+ 'num_format': 'yyyy-mm-dd',
688
+ 'font_name': '微软雅黑',
689
+ 'font_size': 10,
690
+ 'border': 1,
691
+ 'align': 'center'
692
+ })
693
+
694
+ # 定义数据区域格式
695
+ data_format = workbook.add_format({
696
+ 'font_name': '微软雅黑',
697
+ 'font_size': 10,
698
+ 'border': 1,
699
+ 'align': 'center'
700
+ })
701
+
702
+ # 标题行样式(保持不变)
703
+ header_format = workbook.add_format({
704
+ 'font_name': '微软雅黑',
705
+ 'font_size': 10,
706
+ 'bold': True,
707
+ 'bg_color': '#ADD8E6',
708
+ 'font_color': 'black',
709
+ 'border': 1,
710
+ 'align': 'center'
711
+ })
712
+
713
+ columns_dtypes = df.dtypes # 获取列数据类型
714
+
715
+ def get_char_length(text):
716
+ """优化后的字符长度计算"""
717
+ text = str(text).strip()
718
+ return sum(2 if re.match(r'[\u4e00-\u9fff\uff00-\uffef]', c) else 1 for c in text)
719
+
720
+ # 设置列宽和基础格式
721
+ for col_num, (column_name, col_dtype) in enumerate(zip(df.columns, columns_dtypes)):
722
+ max_data_len = df[column_name].apply(get_char_length).max()
723
+ header_len = get_char_length(column_name)
724
+ max_len = max(max_data_len, header_len) + 2
725
+ worksheet.set_column(col_num, col_num, max_len)
726
+
727
+ # 写入标题行
728
+ worksheet.write_row(0, 0, df.columns, header_format)
729
+
730
+ # 写入数据行(根据列数据类型应用格式)
731
+ max_row, max_col = df.shape
732
+ for row in range(1, max_row + 1):
733
+ for col in range(max_col):
734
+ value = df.iat[row - 1, col]
735
+ col_dtype = columns_dtypes[col]
736
+
737
+ # 日期类型处理
738
+ if pd.api.types.is_datetime64_any_dtype(col_dtype):
739
+ if pd.isna(value):
740
+ worksheet.write_blank(row, col, None, date_format)
741
+ else:
742
+ worksheet.write(row, col, value, date_format)
743
+ # 其他数据类型
744
+ else:
745
+ worksheet.write(row, col, value, data_format)
746
+
747
+ # 冻结窗格和添加筛选
748
+ worksheet.freeze_panes(1, 0)
749
+ worksheet.autofilter(0, 0, max_row, max_col)
750
+ # self.logger.info('Excel 工作表格式化完成')
751
+ # print('Excel 工作表格式化完成')
752
+
753
+ def export_df_to_excel(self, df_list, sheet_names, file_path):
754
+ new_df_list = []
755
+ for df in df_list:
756
+ # 全面替换 Inf 和 NaN
757
+ df = df.replace([np.inf, -np.inf, np.nan], None)
758
+ new_df_list.append(df)
759
+
760
+ writer = pd.ExcelWriter(file_path, engine='xlsxwriter')
761
+
762
+ for idx, (df, sheet_name) in enumerate(zip(new_df_list, sheet_names)):
763
+ df.to_excel(writer, sheet_name=sheet_name, index=False)
764
+ worksheet = writer.sheets[sheet_name]
765
+ workbook = writer.book
766
+ self.format_excel_worksheet(worksheet, df, workbook)
767
+
768
+ writer.close()
769
+ self.logger.info(f'Excel 文件导出完成: {file_path}')
770
+ print(f'Excel 文件导出完成: {file_path}')
771
+
772
+
773
+ # 将中文字转化成拼音+数字
774
+ def name_to_pinyin(self, name):
775
+ """
776
+ 将中文名称+数字格式转换为拼音+数字格式
777
+ :param name: 原始名称(如"张三123")
778
+ :return: 转换后的拼音名称(如"zhangsan123")
779
+ """
780
+ match = re.match(r'(\D+)(\d+)', name)
781
+ if not match:
782
+ return name # 如果不符合格式,保留原值
783
+
784
+ chinese_part = match.group(1)
785
+ number_part = match.group(2)
786
+
787
+ # 转换为拼音(去除空格)
788
+ pinyin = ''.join(lazy_pinyin(chinese_part))
789
+
790
+ # 组合拼音和数字
791
+ return pinyin + number_part
792
+
793
+ # 车辆转移任务 转移给新负责人
794
+ def car_task_transfer(self, df, name_column):
795
+ """车辆转移任务(改用requests直接调用API,无需浏览器驱动)"""
796
+ self.logger.info("开始处理车辆分配任务(使用requests调用API)")
797
+ print("开始处理车辆分配任务(使用requests调用API)")
798
+
799
+ try:
800
+ # 检查必要的列
801
+ if 'vin' not in df.columns:
802
+ raise ValueError("数据框中缺少必要的'vin'列")
803
+ if name_column not in df.columns:
804
+ raise ValueError(f"数据框中缺少指定的名称列: {name_column}")
805
+
806
+ # 转换名称为拼音
807
+ df["name"] = df[name_column].apply(self.name_to_pinyin)
808
+ process_df = df[['vin', 'name']].copy().reset_index()
809
+ self.logger.info(f"成功转换{len(process_df)}条记录")
810
+
811
+ # 获取当前日期
812
+ today = self.get_date_and_time("%Y-%m-%d", 0)
813
+ self.logger.info(f"当前处理日期: {today}")
814
+
815
+ # 直接用requests调用API(无需浏览器)
816
+ success_count = 0
817
+ fail_count = 0
818
+ for index, row in process_df.iterrows():
819
+ try:
820
+ # 构建API地址
821
+ api_url = f"http://api-cs.xin.com/super/tool/again_allot_car_task?date={today}&vin={row['vin']}&master_name={row['name']}"
822
+
823
+ # 发送GET请求(模拟浏览器访问)
824
+ headers = {
825
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
826
+ }
827
+ response = requests.get(api_url, headers=headers, timeout=10)
828
+
829
+ # 检查请求是否成功
830
+ if response.status_code == 200:
831
+ success_count += 1
832
+ self.logger.info(f"第{index + 1}条成功:{row['vin']}")
833
+ else:
834
+ fail_count += 1
835
+ self.logger.error(f"第{index + 1}条失败(状态码:{response.status_code}):{row['vin']}")
836
+
837
+ time.sleep(2) # 避免请求过于频繁
838
+ except Exception as e:
839
+ fail_count += 1
840
+ self.logger.error(f"第{index + 1}条出错:{str(e)}")
841
+
842
+ # 处理结果统计
843
+ self.logger.info(f"处理完成 - 成功: {success_count}, 失败: {fail_count}")
844
+ print(f"处理完成 - 成功: {success_count}, 失败: {fail_count}")
845
+
846
+ except Exception as e:
847
+ self.logger.error(f"车辆分配处理出错: {str(e)}")
848
+ raise
849
+
850
+ # 表格导出使用示例
851
+ # df1 = pd.DataFrame(df)
852
+ # df2 = pd.DataFrame(df)
853
+ # df3 = pd.DataFrame(df)
854
+ #
855
+ # export_dfs_to_excel(
856
+ # df_list=[df1, df2, df3],
857
+ # sheet_names=['汇总', '扣款', '赚钱'],
858
+ # file_path='多Sheet数据.xlsx'
859
+ # )
860
+
861
+
862
+ # # 新版发送邮件调用代码
863
+ # recipients = ["dongyang@xin.com"]
864
+ # cc = ["dongyang@xin.com"]
865
+ # bcc = ["dongyang@xin.com"]
866
+ # subject = "测试邮件"
867
+ # html_body = "这是一封测试邮件"
868
+ # files1 = [send_file1]
869
+ # sender.send_email(recipients, cc, bcc, subject, html_body, files1)
870
+
871
+
872
+ # 实例化 DataProcessingAndMessaging 类
873
+ # ux = DataProcessingAndMessaging()
874
+ # for method_name in dir(ux):
875
+ # if not method_name.startswith("__"):
876
+ # globals()[method_name] = getattr(ux, method_name)
877
+ # # 开始运行脚本,这将设置路径,确保 path和 current_script_name已被正确赋值
878
+ # Start_Get_filepath_and_filename()
879
+ # path = ux.path
880
+ # current_script_name = ux.current_script_name
881
+
882
+
@@ -0,0 +1,7 @@
1
+ # WeComMsg/__init__.py
2
+
3
+ # 从核心代码文件中导入需要对外暴露的类/函数
4
+ from .Functions_d import DataProcessingAndMessaging
5
+
6
+ # 可选:明确指定包对外暴露的成员(规范导入)
7
+ __all__ = ["DataProcessingAndMessaging", "__version__"]
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: Functions_d
3
+ Version: 1.0.0
4
+ Summary: 包含数据处理、Hive交互、企业微信消息发送、Excel操作等功能的工具类库
5
+ Author: DongYang
6
+ Author-email: 649898871@qq.com
7
+ License: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.7
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.7
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: pandas>=1.3.0
18
+ Requires-Dist: numpy>=1.21.0
19
+ Requires-Dist: xlwings>=0.27.0
20
+ Requires-Dist: xlsxwriter>=3.0.0
21
+ Requires-Dist: yagmail>=0.15.293
22
+ Requires-Dist: requests>=2.26.0
23
+ Requires-Dist: pyhive>=0.6.5
24
+ Requires-Dist: thrift>=0.15.0
25
+ Requires-Dist: selenium>=4.0.0
26
+ Requires-Dist: webdriver-manager>=3.8.0
27
+ Requires-Dist: beautifulsoup4>=4.10.0
28
+ Requires-Dist: lxml>=4.6.3
29
+ Requires-Dist: pillow>=8.4.0
30
+ Requires-Dist: pypinyin>=0.48.0
31
+
32
+ # 说明
33
+ 这是一个函数封装工具
34
+
35
+ ## 安装
36
+ 使用pip安装:
37
+ bash
38
+ pip install Functions_d
39
+
40
+ ### 联系我们
41
+ 如果你有任何问题或建议,请联系我649898871@qq.com
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.cfg
4
+ setup.py
5
+ Functions_d/Functions_d.py
6
+ Functions_d/__init__.py
7
+ Functions_d.egg-info/PKG-INFO
8
+ Functions_d.egg-info/SOURCES.txt
9
+ Functions_d.egg-info/dependency_links.txt
10
+ Functions_d.egg-info/requires.txt
11
+ Functions_d.egg-info/top_level.txt
@@ -0,0 +1,14 @@
1
+ pandas>=1.3.0
2
+ numpy>=1.21.0
3
+ xlwings>=0.27.0
4
+ xlsxwriter>=3.0.0
5
+ yagmail>=0.15.293
6
+ requests>=2.26.0
7
+ pyhive>=0.6.5
8
+ thrift>=0.15.0
9
+ selenium>=4.0.0
10
+ webdriver-manager>=3.8.0
11
+ beautifulsoup4>=4.10.0
12
+ lxml>=4.6.3
13
+ pillow>=8.4.0
14
+ pypinyin>=0.48.0
@@ -0,0 +1 @@
1
+ Functions_d
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: Functions_d
3
+ Version: 1.0.0
4
+ Summary: 包含数据处理、Hive交互、企业微信消息发送、Excel操作等功能的工具类库
5
+ Author: DongYang
6
+ Author-email: 649898871@qq.com
7
+ License: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.7
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.7
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: pandas>=1.3.0
18
+ Requires-Dist: numpy>=1.21.0
19
+ Requires-Dist: xlwings>=0.27.0
20
+ Requires-Dist: xlsxwriter>=3.0.0
21
+ Requires-Dist: yagmail>=0.15.293
22
+ Requires-Dist: requests>=2.26.0
23
+ Requires-Dist: pyhive>=0.6.5
24
+ Requires-Dist: thrift>=0.15.0
25
+ Requires-Dist: selenium>=4.0.0
26
+ Requires-Dist: webdriver-manager>=3.8.0
27
+ Requires-Dist: beautifulsoup4>=4.10.0
28
+ Requires-Dist: lxml>=4.6.3
29
+ Requires-Dist: pillow>=8.4.0
30
+ Requires-Dist: pypinyin>=0.48.0
31
+
32
+ # 说明
33
+ 这是一个函数封装工具
34
+
35
+ ## 安装
36
+ 使用pip安装:
37
+ bash
38
+ pip install Functions_d
39
+
40
+ ### 联系我们
41
+ 如果你有任何问题或建议,请联系我649898871@qq.com
@@ -0,0 +1,10 @@
1
+ # 说明
2
+ 这是一个函数封装工具
3
+
4
+ ## 安装
5
+ 使用pip安装:
6
+ bash
7
+ pip install Functions_d
8
+
9
+ ### 联系我们
10
+ 如果你有任何问题或建议,请联系我649898871@qq.com
@@ -0,0 +1,4 @@
1
+ [build-system]
2
+ # 声明构建依赖和后端(使用 setuptools 作为构建工具)
3
+ requires = ["setuptools>=61.0", "wheel"]
4
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,47 @@
1
+ [metadata]
2
+ name = Functions_d
3
+ version = 1.0.0
4
+ author = DongYang
5
+ author_email = 649898871@qq.com
6
+ description = 包含数据处理、Hive交互、企业微信消息发送、Excel操作等功能的工具类库
7
+ long_description = file: README.md
8
+ long_description_content_type = text/markdown
9
+ classifiers =
10
+ Programming Language :: Python :: 3
11
+ Programming Language :: Python :: 3.7
12
+ Programming Language :: Python :: 3.8
13
+ Programming Language :: Python :: 3.9
14
+ Operating System :: Microsoft :: Windows
15
+ Intended Audience :: Developers
16
+ Topic :: Software Development :: Libraries :: Python Modules
17
+ license = MIT
18
+
19
+ [options]
20
+ packages = find:
21
+ python_requires = >=3.7
22
+ install_requires =
23
+ pandas>=1.3.0
24
+ numpy>=1.21.0
25
+ xlwings>=0.27.0
26
+ xlsxwriter>=3.0.0
27
+ yagmail>=0.15.293
28
+ requests>=2.26.0
29
+ pyhive>=0.6.5
30
+ thrift>=0.15.0
31
+ selenium>=4.0.0
32
+ webdriver-manager>=3.8.0
33
+ beautifulsoup4>=4.10.0
34
+ lxml>=4.6.3
35
+ pillow>=8.4.0
36
+ pypinyin>=0.48.0
37
+
38
+ [options.packages.find]
39
+ where = .
40
+ exclude =
41
+ tests*
42
+ docs*
43
+
44
+ [egg_info]
45
+ tag_build =
46
+ tag_date = 0
47
+
@@ -0,0 +1,2 @@
1
+ from setuptools import setup
2
+ setup() # 仅用于兼容旧工具,实际配置从 setup.cfg 读取