Functions-d 1.30__tar.gz → 1.32__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.
- {functions_d-1.30 → functions_d-1.32}/Functions_d/Functions_d.py +166 -40
- {functions_d-1.30 → functions_d-1.32}/Functions_d.egg-info/PKG-INFO +1 -1
- {functions_d-1.30 → functions_d-1.32}/PKG-INFO +1 -1
- {functions_d-1.30 → functions_d-1.32}/setup.cfg +1 -1
- {functions_d-1.30 → functions_d-1.32}/Functions_d/__init__.py +0 -0
- {functions_d-1.30 → functions_d-1.32}/Functions_d.egg-info/SOURCES.txt +0 -0
- {functions_d-1.30 → functions_d-1.32}/Functions_d.egg-info/dependency_links.txt +0 -0
- {functions_d-1.30 → functions_d-1.32}/Functions_d.egg-info/requires.txt +0 -0
- {functions_d-1.30 → functions_d-1.32}/Functions_d.egg-info/top_level.txt +0 -0
- {functions_d-1.30 → functions_d-1.32}/README.md +0 -0
- {functions_d-1.30 → functions_d-1.32}/pyproject.toml +0 -0
- {functions_d-1.30 → functions_d-1.32}/setup.py +0 -0
|
@@ -812,63 +812,188 @@ class DataProcessingAndMessaging:
|
|
|
812
812
|
self.screen(filename, sheet_name,
|
|
813
813
|
f"{start_range}:%s" % (self.column_label(len(data.columns)) + str(len(data) + 3)), image_path)
|
|
814
814
|
|
|
815
|
-
# --------------------------
|
|
815
|
+
# -------------------------- 发送邮件(新版-优化增强版) --------------------------
|
|
816
816
|
def send_email_new(self, recipient_emails, cc_emails=None, bcc_emails=None, subject="", html_body="",
|
|
817
817
|
attachments=None):
|
|
818
|
+
# 1. 内部工具:格式化邮箱(和旧版完全对齐,转列表+过滤无效值)
|
|
819
|
+
def format_email(emails):
|
|
820
|
+
if not emails:
|
|
821
|
+
return []
|
|
822
|
+
# 单个邮箱转列表,列表直接使用
|
|
823
|
+
email_list = [emails] if isinstance(emails, str) else emails
|
|
824
|
+
# 过滤:非空字符串 + 简单格式校验(含@和.)
|
|
825
|
+
valid_emails = [
|
|
826
|
+
e.strip() for e in email_list
|
|
827
|
+
if isinstance(e, str) and e.strip() and '@' in e.strip() and '.' in e.strip()
|
|
828
|
+
]
|
|
829
|
+
return valid_emails
|
|
830
|
+
|
|
831
|
+
# 2. 内部工具:自动识别附件MIME类型(支持全格式文件)
|
|
832
|
+
def get_mime_type(file_path):
|
|
833
|
+
suffix = os.path.splitext(file_path)[1].lower()
|
|
834
|
+
mime_map = {
|
|
835
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
836
|
+
'.xls': 'application/vnd.ms-excel',
|
|
837
|
+
'.pdf': 'application/pdf',
|
|
838
|
+
'.doc': 'application/msword',
|
|
839
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
840
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
841
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
842
|
+
'.jpg': 'image/jpeg',
|
|
843
|
+
'.jpeg': 'image/jpeg',
|
|
844
|
+
'.png': 'image/png',
|
|
845
|
+
'.gif': 'image/gif',
|
|
846
|
+
'.txt': 'text/plain',
|
|
847
|
+
'.csv': 'text/csv',
|
|
848
|
+
'.zip': 'application/zip',
|
|
849
|
+
'.rar': 'application/x-rar-compressed'
|
|
850
|
+
}
|
|
851
|
+
return mime_map.get(suffix, 'application/octet-stream')
|
|
852
|
+
|
|
853
|
+
# 3. 参数标准化处理(修复None报错隐患)
|
|
854
|
+
subject = subject.strip()
|
|
855
|
+
to_emails = format_email(recipient_emails)
|
|
856
|
+
cc_emails = format_email(cc_emails)
|
|
857
|
+
bcc_emails = format_email(bcc_emails)
|
|
858
|
+
attachments = attachments or []
|
|
859
|
+
# 附件标准化:单个字符串转列表
|
|
860
|
+
if isinstance(attachments, str):
|
|
861
|
+
attachments = [attachments.strip()]
|
|
862
|
+
|
|
863
|
+
# 4. 核心参数校验(提前报错,避免无效连接)
|
|
864
|
+
if not to_emails:
|
|
865
|
+
err_msg = "❌ 邮件发送失败:无有效收件人邮箱"
|
|
866
|
+
self.logger.error(err_msg)
|
|
867
|
+
print(err_msg)
|
|
868
|
+
raise ValueError(err_msg)
|
|
869
|
+
if not subject:
|
|
870
|
+
err_msg = "❌ 邮件发送失败:邮件主题不能为空"
|
|
871
|
+
self.logger.error(err_msg)
|
|
872
|
+
print(err_msg)
|
|
873
|
+
raise ValueError(err_msg)
|
|
874
|
+
|
|
875
|
+
# 5. 控制台打印(和旧版完全对齐,实时展示发送进度)
|
|
876
|
+
print(f"📧 开始发送邮件 - 主题: {subject}")
|
|
877
|
+
print(f"📥 收件人: {', '.join(to_emails)}")
|
|
878
|
+
if cc_emails:
|
|
879
|
+
print(f"📋 抄送: {', '.join(cc_emails)}")
|
|
880
|
+
if bcc_emails:
|
|
881
|
+
print(f"🔒 密送: {', '.join(bcc_emails)}")
|
|
882
|
+
if attachments:
|
|
883
|
+
attach_names = [os.path.basename(att) for att in attachments]
|
|
884
|
+
print(f"📎 附件: {', '.join(attach_names)}")
|
|
885
|
+
|
|
886
|
+
# 6. 日志记录(和原新版逻辑对齐)
|
|
818
887
|
self.logger.info(f"开始发送邮件 - 主题: {subject}")
|
|
819
|
-
self.logger.info(f"收件人: {', '.join(
|
|
888
|
+
self.logger.info(f"收件人: {', '.join(to_emails)}")
|
|
820
889
|
if cc_emails:
|
|
821
890
|
self.logger.info(f"抄送: {', '.join(cc_emails)}")
|
|
822
891
|
if bcc_emails:
|
|
823
|
-
self.logger.info(f"密送: {', '.join(bcc_emails)
|
|
892
|
+
self.logger.info(f"密送: {', '.join(bcc_emails)}")
|
|
824
893
|
if attachments:
|
|
825
894
|
attach_names = [os.path.basename(att) for att in attachments]
|
|
826
895
|
self.logger.info(f"附件: {', '.join(attach_names)}")
|
|
827
896
|
|
|
897
|
+
# 7. SMTP固定配置
|
|
828
898
|
sender_email = 'cc_yingxiao@xin.com'
|
|
829
899
|
sender_password = 'cw46pfeznNQx'
|
|
900
|
+
smtp_host = 'mail.xin.com'
|
|
901
|
+
smtp_port = 587
|
|
830
902
|
|
|
831
903
|
try:
|
|
904
|
+
# 8. 构建邮件对象
|
|
832
905
|
msg = MIMEMultipart()
|
|
833
906
|
msg['From'] = sender_email
|
|
834
|
-
msg['To'] = ', '.join(
|
|
907
|
+
msg['To'] = ', '.join(to_emails)
|
|
835
908
|
if cc_emails:
|
|
836
909
|
msg['Cc'] = ', '.join(cc_emails)
|
|
837
910
|
if bcc_emails:
|
|
838
911
|
msg['Bcc'] = ', '.join(bcc_emails)
|
|
839
912
|
msg['Subject'] = subject
|
|
840
913
|
|
|
841
|
-
|
|
914
|
+
# 9. 邮件正文(修复中文乱码,指定utf-8编码)
|
|
915
|
+
body = MIMEText(html_body, 'html', 'utf-8')
|
|
842
916
|
msg.attach(body)
|
|
843
917
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
918
|
+
# 10. 附件处理(全格式支持+提前校验)
|
|
919
|
+
valid_attachments = []
|
|
920
|
+
for attachment in attachments:
|
|
921
|
+
attachment = attachment.strip()
|
|
922
|
+
if not os.path.exists(attachment):
|
|
923
|
+
error_msg = f"❌ 邮件发送失败:附件不存在 -> {attachment}"
|
|
924
|
+
self.logger.error(error_msg)
|
|
925
|
+
print(error_msg)
|
|
926
|
+
raise FileNotFoundError(error_msg)
|
|
927
|
+
|
|
928
|
+
# 自动识别MIME类型,适配不同文件
|
|
929
|
+
file_name = os.path.basename(attachment)
|
|
930
|
+
mime_type = get_mime_type(attachment)
|
|
931
|
+
|
|
932
|
+
with open(attachment, 'rb') as file:
|
|
933
|
+
file_content = file.read()
|
|
934
|
+
|
|
935
|
+
# 按文件类型选择对应的MIME对象
|
|
936
|
+
if mime_type.startswith('image/'):
|
|
937
|
+
part = MIMEImage(file_content, _subtype=mime_type.split('/')[1])
|
|
938
|
+
elif mime_type == 'text/plain' or mime_type == 'text/csv':
|
|
939
|
+
part = MIMEText(file_content.decode('utf-8'), _subtype=mime_type.split('/')[1], _charset='utf-8')
|
|
940
|
+
else:
|
|
941
|
+
part = MIMEApplication(file_content, _subtype=mime_type.split('/')[1])
|
|
942
|
+
|
|
943
|
+
part['Content-Disposition'] = f'attachment; filename="{file_name}"'
|
|
944
|
+
part['Content-Type'] = f'{mime_type}; name="{file_name}"'
|
|
945
|
+
msg.attach(part)
|
|
946
|
+
valid_attachments.append(file_name)
|
|
947
|
+
self.logger.debug(f"已添加附件: {file_name}")
|
|
948
|
+
print(f"✅ 已添加附件: {file_name}")
|
|
949
|
+
|
|
950
|
+
# 11. 连接SMTP服务器并发送(修复核心bug:587端口必须启用TLS)
|
|
951
|
+
print(f"🔌 正在连接SMTP服务器 {smtp_host}:{smtp_port}...")
|
|
952
|
+
with smtplib.SMTP(smtp_host, smtp_port, timeout=120) as smtp:
|
|
953
|
+
smtp.starttls() # 587端口强制启用TLS,绝大多数服务器要求
|
|
858
954
|
smtp.login(sender_email, sender_password)
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
all_recipients.extend(bcc_emails)
|
|
955
|
+
print(f"🔐 SMTP服务器登录成功")
|
|
956
|
+
|
|
957
|
+
# 所有收件人去重,避免重复发送
|
|
958
|
+
all_recipients = list(set(to_emails + cc_emails + bcc_emails))
|
|
864
959
|
smtp.sendmail(sender_email, all_recipients, msg.as_string())
|
|
865
960
|
|
|
866
|
-
|
|
961
|
+
# 12. 发送成功:控制台打印+日志(和旧版完全对齐)
|
|
962
|
+
success_msg = f"✅ 邮件发送成功 | 主题:{subject} | 收件人:{len(to_emails)}人"
|
|
963
|
+
if cc_emails:
|
|
964
|
+
success_msg += f" | 抄送:{len(cc_emails)}人"
|
|
965
|
+
if bcc_emails:
|
|
966
|
+
success_msg += f" | 密送:{len(bcc_emails)}人"
|
|
967
|
+
if valid_attachments:
|
|
968
|
+
success_msg += f" | 附件:{len(valid_attachments)}个"
|
|
867
969
|
|
|
970
|
+
self.logger.info(success_msg)
|
|
971
|
+
print(success_msg)
|
|
972
|
+
return True
|
|
973
|
+
|
|
974
|
+
# 13. 分类异常捕获(和旧版对齐,易排查问题)
|
|
975
|
+
except smtplib.SMTPAuthenticationError:
|
|
976
|
+
err_msg = "❌ 邮件发送失败:SMTP账号/密码/授权码错误"
|
|
977
|
+
self.logger.error(err_msg, exc_info=True)
|
|
978
|
+
print(err_msg)
|
|
979
|
+
raise ValueError(err_msg)
|
|
980
|
+
except smtplib.SMTPConnectError:
|
|
981
|
+
err_msg = "❌ 邮件发送失败:SMTP服务器连接失败(检查host/port/防火墙/网络)"
|
|
982
|
+
self.logger.error(err_msg, exc_info=True)
|
|
983
|
+
print(err_msg)
|
|
984
|
+
raise ConnectionError(err_msg)
|
|
985
|
+
except smtplib.SMTPException as e:
|
|
986
|
+
err_msg = f"❌ 邮件发送失败:SMTP协议错误 - {str(e)}"
|
|
987
|
+
self.logger.error(err_msg, exc_info=True)
|
|
988
|
+
print(err_msg)
|
|
989
|
+
raise RuntimeError(err_msg)
|
|
990
|
+
except FileNotFoundError:
|
|
991
|
+
raise # 附件错误已提前处理,直接抛出
|
|
868
992
|
except Exception as e:
|
|
869
|
-
|
|
870
|
-
self.logger.error(
|
|
871
|
-
|
|
993
|
+
err_msg = f"❌ 邮件发送失败:{str(e)}"
|
|
994
|
+
self.logger.error(err_msg, exc_info=True)
|
|
995
|
+
print(err_msg)
|
|
996
|
+
raise RuntimeError(err_msg)
|
|
872
997
|
|
|
873
998
|
# -------------------------- Excel格式化 --------------------------
|
|
874
999
|
def format_excel_worksheet(self, worksheet, df, workbook):
|
|
@@ -1561,7 +1686,7 @@ class DataProcessingAndMessaging:
|
|
|
1561
1686
|
# -------------------------- 上传文件/DF到内网文件服务器(最终完整版) --------------------------
|
|
1562
1687
|
def upload_to_internal_server(self, input_data, filename_in_server: str = "dataframe_result.xlsx"):
|
|
1563
1688
|
"""
|
|
1564
|
-
上传数据到内网 http://fs-cc.xin.com
|
|
1689
|
+
上传数据到内网 http://fs-cc.xin.com(按要求定制:重试3次、读超时5分钟,无拆分无压缩)
|
|
1565
1690
|
:param input_data: 支持 3 种输入 →
|
|
1566
1691
|
1) 单个 DataFrame
|
|
1567
1692
|
2) 字典 {sheet名: df} → 自动生成多Sheet Excel
|
|
@@ -1581,14 +1706,15 @@ class DataProcessingAndMessaging:
|
|
|
1581
1706
|
self.logger.info(f"开始上传数据到内网服务器,服务器文件名:{filename_in_server}")
|
|
1582
1707
|
print(f"📤 正在上传 → 服务器文件名:{filename_in_server}")
|
|
1583
1708
|
|
|
1584
|
-
# ======================
|
|
1709
|
+
# ====================== 【按要求修改:重试固定3次】 ======================
|
|
1585
1710
|
def create_retry_session(retries=3):
|
|
1586
1711
|
session = requests.Session()
|
|
1587
1712
|
retry_strategy = Retry(
|
|
1588
1713
|
total=retries,
|
|
1589
|
-
backoff_factor=
|
|
1590
|
-
status_forcelist=[500, 502, 503, 504],
|
|
1591
|
-
allowed_methods=["POST"]
|
|
1714
|
+
backoff_factor=2,
|
|
1715
|
+
status_forcelist=[500, 502, 503, 504, 408],
|
|
1716
|
+
allowed_methods=["POST"],
|
|
1717
|
+
raise_on_status=False
|
|
1592
1718
|
)
|
|
1593
1719
|
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
1594
1720
|
session.mount("http://", adapter)
|
|
@@ -1599,7 +1725,7 @@ class DataProcessingAndMessaging:
|
|
|
1599
1725
|
|
|
1600
1726
|
try:
|
|
1601
1727
|
# ==============================================
|
|
1602
|
-
#
|
|
1728
|
+
# 【完全保留原逻辑:无Sheet拆分、无ZIP压缩】
|
|
1603
1729
|
# ==============================================
|
|
1604
1730
|
if isinstance(input_data, dict):
|
|
1605
1731
|
# 字典 → 多Sheet Excel
|
|
@@ -1639,9 +1765,11 @@ class DataProcessingAndMessaging:
|
|
|
1639
1765
|
raise ValueError(f"不支持输入类型:{type(input_data)}")
|
|
1640
1766
|
|
|
1641
1767
|
# 上传
|
|
1642
|
-
session = create_retry_session(
|
|
1768
|
+
session = create_retry_session()
|
|
1643
1769
|
files = {"file": (filename_in_server, file_obj, file_type)}
|
|
1644
|
-
|
|
1770
|
+
# ====================== 【按要求修改:读超时改为5分钟(300秒)】 ======================
|
|
1771
|
+
response = session.post(upload_url, files=files, timeout=(10, 300))
|
|
1772
|
+
# ======================================================================
|
|
1645
1773
|
|
|
1646
1774
|
# 关闭文件
|
|
1647
1775
|
if isinstance(input_data, str):
|
|
@@ -1654,18 +1782,16 @@ class DataProcessingAndMessaging:
|
|
|
1654
1782
|
print("✅ 上传成功!")
|
|
1655
1783
|
print("🔗 下载地址:", download_url)
|
|
1656
1784
|
|
|
1657
|
-
#
|
|
1785
|
+
# 生成同名链接文件(完全保留原逻辑)
|
|
1658
1786
|
file_base_name = os.path.splitext(filename_in_server)[0]
|
|
1659
1787
|
link_file_name = f"{file_base_name}.log"
|
|
1660
1788
|
link_path = os.path.join(os.path.dirname(self.main_log_file), link_file_name)
|
|
1661
1789
|
|
|
1662
1790
|
now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1663
|
-
|
|
1664
|
-
file_content = f"{now_str} | {filename_in_server} | {download_url}\n请打开链接下载报表"
|
|
1791
|
+
file_content = f"\n{now_str} | {filename_in_server} | {download_url}"
|
|
1665
1792
|
|
|
1666
1793
|
with open(link_path, 'a', encoding='utf-8') as f:
|
|
1667
1794
|
f.write(file_content)
|
|
1668
|
-
# ======================================================================
|
|
1669
1795
|
|
|
1670
1796
|
self.logger.info(f"上传链接已保存到:{link_path}")
|
|
1671
1797
|
return download_url
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|