# -*- coding: utf-8 -*- """ 飞书客户端模块 """ import re import time from email.parser import BytesParser from email.policy import default from email.utils import parsedate_to_datetime from imaplib import IMAP4_SSL import pandas from request import Request from authenticator import Authenticator class Feishu: """飞书客户端""" def __init__(self): self.authenticator = Authenticator() self.http_client = Request() def _headers(self): """请求头""" # 装配飞书访问凭证 return { "Authorization": f"Bearer {self.authenticator.get_token(servicer='feishu')}", } @staticmethod def get_verification_code(): try: # 执行时间戳 execute_timestamp = time.time() # 超时时间戳 timeout_timestamp = execute_timestamp + 65 # 建立加密IMAP连接 server = IMAP4_SSL("imap.feishu.cn", 993) # 登录 server.login("mars@liubiren.cloud", "a2SfPUgbKDmrjPV2") while True: # 若当前时间戳大于超时时间戳则返回NONE if time.time() <= timeout_timestamp: # 等待10秒 time.sleep(10) # 选择文件夹(邮箱验证码) server.select("&kK57sZqMi8F4AQ-") # noinspection PyBroadException try: # 获取最后一封邮件索引,server.search()返回数据类型为元组,第一个元素为查询状态,第二个元素为查询结果(邮件索引字节串的列表);然后,从列表获取字节串并分割取最后一个,作为最后一封邮件索引 index = server.search(None, "ALL")[1][0].split()[-1] # 获取最后一封邮件内容并解析,server.fetch()返回数据类型为元组,第一个元素为查询状态,第二个元素为查询结果(邮件内容字节串的列表);然后,从列表获取字节串并解析正文 # noinspection PyUnresolvedReferences contents = BytesParser(policy=default).parsebytes( server.fetch(index, "(RFC822)")[1][0][1] ) # 遍历邮件内容,若正文内容类型为纯文本或HTML则解析发送时间和验证码 for content in contents.walk(): if ( content.get_content_type() == "text/plain" or content.get_content_type() == "text/html" ): # 邮件发送时间戳 # noinspection PyUnresolvedReferences send_timestamp = parsedate_to_datetime( content["Date"] ).timestamp() # 若邮件发送时间戳大于执行时间戳则解析验证码并返回 if ( execute_timestamp > send_timestamp >= execute_timestamp - 35 ): # 登出 server.logout() # 解析验证码 return re.search( r"【普康健康】您的验证码是:(\d+)", content.get_payload(decode=True).decode(), ).group(1) # 若文件夹无邮件则继续 except: pass # 若超时则登出 else: server.logout() return None except Exception: raise RuntimeError("获取邮箱验证码发生其它异常") # 查询多维表格记录,单次最多查询500条记录 @restrict(refill_rate=5, max_tokens=5) def query_bitable_records( self, bitable: str, table_id: str, field_names: Optional[list[str]] = None, filter_conditions: Optional[dict] = None, ) -> pandas.DataFrame: # 先查询多维表格记录,在根据字段解析记录 # 装配多维表格查询记录地址 url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{bitable}/tables/{table_id}/records/search?page_size=20" response = self.http_client.post( url=url, headers=self._headers(), json={"field_names": field_names, "filter": filter_conditions}, ) # 响应业务码为0则定义为响应成功 assert response.get("code") == 0, "查询多维表格记录发生异常" # 多维表格记录 records = response.get("data").get("items") # 检查响应中是否包含还有下一页标识,若有则继续请求下一页 while response.get("data").get("has_more"): url_next = url + "&page_token={}".format( response.get("data").get("page_token") ) response = self.http_client.post( url=url_next, headers=self._headers(), json={"field_names": field_names, "filter": filter_conditions}, ) assert response.get("code") == 0, "查询多维表格记录发生异常" # 合并记录 records.append(response.get("data").get("items")) # 装配多维表格列出字段地址 url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{bitable}/tables/{table_id}/fields?page_size=20" response = self.http_client.get( url=url, headers=self._headers(), ) assert response.get("code") == 0, "列出多维表格字段发生异常" # 多维表格字段 fields = response.get("data").get("items") while response.get("data").get("has_more"): url_next = url + "&page_token={}".format( response.get("data").get("page_token") ) response = self.http_client.get( url=url_next, headers=self._headers(), ) assert response.get("code") == 0, "列出多维表格字段发生异常" fields.append(response.get("data").get("items")) # 字段映射 field_mappings = {} for field in fields: # 字段名 field_name = field["field_name"] # 根据字段类型匹配 match field["type"]: case 1005: field_type = "主键" case 1: field_type = "文本" case 3: field_type = "单选" case 2: # 数字、公式字段的显示格式 match field["property"]["formatter"]: case "0": field_type = "整数" case _: raise ValueError("未设置数字、公式字段的显示格式") case _: raise ValueError("未设置字段类型") # noinspection PyUnboundLocalVariable field_mappings.update({field_name: field_type}) # 记录数据体 records_data = [] # 解析记录 for record in records: # 单条记录数据体 record_data = {} for field_name, content in record["fields"].items(): match field_mappings[field_name]: case "主键" | "单选" | "整数": record_data.update({field_name: content}) case "文本": # 若存在多行文本则拼接 fragments_content = "" for fragment_content in content: fragments_content += fragment_content["text"] record_data.update({field_name: fragments_content}) case _: raise ValueError("未设置字段解析方法") records_data.append(record_data) return pandas.DataFrame(records_data)