Python/utils/feishu.py

246 lines
8.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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)