""" API 模块 """ import requests from typing import Optional, List, Dict, Any from config import Config from decimal import Decimal import time import numpy as np from log_config import configure_task_logger, configure_error_task_logger import json # 获取已经配置好的常规日志记录器 logger = configure_task_logger() # 获取已经配置好的错误任务日志记录器 error_task_logger = configure_error_task_logger() class NpEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, np.integer): return int(obj) elif isinstance(obj, np.floating): return float(obj) elif isinstance(obj, np.ndarray): return obj.tolist() else: return super(NpEncoder, self).default(obj) def replace_decimals(obj): if isinstance(obj, dict): return {k: replace_decimals(v) for k, v in obj.items()} elif isinstance(obj, list): return [replace_decimals(item) for item in obj] elif isinstance(obj, Decimal): return float(obj) # 或者 str(obj) return obj class API: def entry_data_get(self, data: dict, replace: bool = False) -> Dict: # 获取单条表单数据 """ 获取单条表单数据 :param replace: 是否替换字段,默认为关 :param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息 :return: """ url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/get' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 app_key 'Content-Type': 'application/json' } payload = json.dumps({ "app_id": data['api_key'], # 应用ID "entry_id": data['entry_id'], # 表单ID "data_id": data['data_id'] # 数据ID }) res = requests.post(url=url, data=payload, headers=headers, timeout=10) data_get = res.json() if replace: data_get = self.field_replacement(data, data_get) # 字段替换,由id替换为标签名 return data_get def entry_data_list(self, data: dict, replace: bool = False, max_retries: int = 20) -> Dict: # 获取多条表单数据 """ 获取多条表单数据 :param max_retries: 最大重试次数 :param replace: 是否替换字段 :param data: api_key: 应用id entry_id: 表单id :return: """ url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/list' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 app_key 'Content-Type': 'application/json' } all_data_batches = [] # 用于存储每次请求返回的数据批次 last_data_id = None exit_flag = False count = 0 while True: payload = json.dumps({ "app_id": data['api_key'], # 应用ID "entry_id": data['entry_id'], # 表单ID "limit": 100, "data_id": last_data_id }) retries = 0 while retries <= max_retries: try: res = requests.post(url=url, data=payload, headers=headers, timeout=10) res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 data_get = res.json() if data_get["data"]: all_data_batches.extend(data_get['data']) last_data_id = data_get['data'][-1].get('_id') print(f"已获取 {len(all_data_batches)} 条数据") break # 成功则跳出循环 else: if 'data' not in data_get or len(data_get['data']) == 0: exit_flag = True break logger.warning(f"请求异常, 将重新请求") retries += 1 time.sleep(0.1) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(0.1) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error(f"任务 {last_data_id}组 连续{max_retries}次请求失败,放弃此次请求。") all_data_batches.append(None) # 或者可以选择记录失败的payload以便后续处理 if exit_flag: break # 构建最终返回的字典 final_data = { 'data': all_data_batches # 'data' 键对应的值是列表的列表 } logger.info(f"获取了{len(all_data_batches)}条数据") if replace: print("进行了替换") return_data = self.field_replacement(data, final_data) # 字段替换,由id替换为标签名 return return_data else: return final_data @staticmethod def entry_widget_list(data: dict) -> Optional[Dict[str, Any]]: # 获取表单字段 """ 获取表单字段 :param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息 :return: """ url = 'https://api.jiandaoyun.com/api/v5/app/entry/widget/list' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 app_key 'Content-Type': 'application/json' } payload = json.dumps({ "app_id": data['api_key'], "entry_id": data['entry_id'], }) res = requests.post(url=url, data=payload, headers=headers, timeout=10) return res.json() def field_replacement(self, data: dict, data_get: dict) -> dict: """ 字段替换,将id替换为标签名,即唯一值替换为表单中显示字段的名字 :param data: 简道云插件发送过来的data,包含表单id、数据id、应用id :param data_get: 简道云请求的数据,一般是根据数据id获取到表单的数据 :return: 将根据数据id获取到的表单数据,进行替换,返回替换后的数据 """ # 获取表单对应字段标签名称 widget_list = self.entry_widget_list(data) # 检查widget_list是否有效 if not widget_list or 'widgets' not in widget_list or not isinstance(widget_list['widgets'], list): raise ValueError("映射表没有接受到数据") # 创建一个映射表,将_widget_名称映射到label name_to_label = {widget['name']: widget['label'] for widget in widget_list['widgets']} def replace_keys(obj): """递归替换字典中的键名""" if isinstance(obj, dict): new_dict = {} for key, value in obj.items(): new_key = name_to_label.get(key, key) new_dict[new_key] = replace_keys(value) return new_dict elif isinstance(obj, list): return [replace_keys(item) for item in obj] else: return obj # 复制 data_get,避免修改原始数据 data_get_copy = json.loads(json.dumps(data_get)) # 深拷贝 # 替换 data 字段下的所有键 if 'data' in data_get_copy: data_get_copy['data'] = replace_keys(data_get_copy['data']) return data_get_copy @staticmethod def data_batch_create(data: dict, max_retries: int = 20) -> Optional[requests.Response]: # 新建单条数据 """ 新建单条表单数据 :param max_retries: 最大重试次数 :param data: 应该包含应用id、表单id,以及新建的数据data['data'] :return: 返回创建后简道云返回的信息 """ url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/create' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 app_key 'Content-Type': 'application/json' } """ data 样式 # 后续优化发送数据样式 目前输入字段,后续优化输入表单名称 jiandaoyun_data['data'] = {"_widget_1731650067055":{"value":f'{username}{password}'}, "_widget_1731650067056":{"value": f"{group}"}} """ # noinspection DuplicatedCode payload = json.dumps({ "app_id": data['api_key'], # 应用ID "entry_id": data['entry_id'], # 表单ID "data": data['data'], "is_start_workflow": data.get('is_start_workflow', "false"), "is_start_trigger": data.get('is_start_trigger', "false"), "transaction_id": data.get('transaction_id', "") } ) retries = 0 while retries <= max_retries: try: res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 data_get = res.json() if res.status_code == 200: return data_get else: logger.warning(f"请求异常, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error( f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。") return None @staticmethod def entry_data_batch_create( data: dict, chunk_size: int = 90, max_retries: int = 20 ) -> List[Optional[requests.Response]]: # 新建多条数据 注意简道云限制1次最多100条数据 """ 新建多条数据 :param max_retries: 最大重试次数,此处设置20次 :param data:应包含数据id、表单id、以及需要新建的信息,新建信息应该是一个列表 :param chunk_size: 简道云限制批量新建一次最多100条,这里默认值设置为90条一次 :return:返回请求后的结果 """ data = replace_decimals(data) url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_create' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey 'Content-Type': 'application/json' } """ data_list 样式 # 后续优化发送数据样式 目前输入字段,后续优化输入表单名称 jiandaoyun_data_list['data_list'] = [{"_widget_1731650067055":{"value":f'{username}{password}'}, "_widget_1731650067056":{"value": f"{group}"}}, {"_widget_1731650067055":{"value":f'{username}{password}'}, "_widget_1731650067056":{"value": f"{group}"}}] """ # 获取data_list长度 total_length = len(data['data_list']) logger.info(f"多数据写入行数: {total_length}") # 计算需要发送的次数 num_chunks = (total_length + chunk_size - 1) // chunk_size # //整除向下取证,需要加上chunk_size - 1保证不会有缺失数据 data_get_list = [] for i in range(num_chunks): start_index = i * chunk_size end_index = min(start_index + chunk_size, total_length) payload = json.dumps({ "app_id": data['api_key'], # 应用ID "entry_id": data['entry_id'], # 表单ID "data_list": data['data_list'][start_index:end_index], "is_start_workflow": data.get('is_start_workflow', "false"), "is_start_trigger": data.get('is_start_trigger', "false"), }, cls=NpEncoder) retries = 0 while retries <= max_retries: try: res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) # print(res.json()) res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 data_get = res.json() if data_get["status"] == "success": data_get_list.append(data_get) break # 成功则跳出循环 else: logger.warning(f"请求异常,将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(0.1) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error( f"任务 {data['data_list'][start_index:end_index]} 连续{max_retries}次请求失败,放弃此次请求。") data_get_list.append(None) # 或者可以选择记录失败的payload以便后续处理 return data_get_list @staticmethod def entry_data_update(data: dict, max_retries: int = 20) -> dict: # 修改数据 """ 修改数据 :param max_retries: 最大重试次数,此处设置100次 :param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息 :return: 修改数据后简道云返回的结果 """ url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/update' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey 'Content-Type': 'application/json' } payload = json.dumps({ "app_id": data['api_key'], # 应用ID "entry_id": data['entry_id'], # 表单ID "data_id": data['data_id'], # 数据ID "data": data['data'] } ) data_get = None retries = 0 while retries <= max_retries: try: res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 data_get = res.json() # print(data_get) if res.status_code == 200: break # 成功则跳出循环 else: logger.warning(f"请求异常, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(10) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。") continue return data_get @staticmethod def entry_data_banch_update(data: dict, max_retries: int = 20, chunk_size: int = 90) -> list[dict]: # 修改数据 """ 批量修改数据 :param max_retries: 最大重试次数,此处设置100次 :param data: 简道云插件发送过来的data,包含应用id、表单id、数据id等信息 :return: 修改数据后简道云返回的结果 """ url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_update' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey 'Content-Type': 'application/json' } # 获取data_list长度 total_length = len(data['data_ids']) logger.info(f"多数据写入行数: {total_length}") # 计算需要发送的次数 num_chunks = (total_length + chunk_size - 1) // chunk_size # //整除向下取证,需要加上chunk_size - 1保证不会有缺失数据 data_get_list = [] for i in range(num_chunks): start_index = i * chunk_size end_index = min(start_index + chunk_size, total_length) payload = json.dumps({ "app_id": data['api_key'], # 应用ID "entry_id": data['entry_id'], # 表单ID "data_list": data['data_ids'][start_index:end_index], "data": data['data'] }, cls=NpEncoder) retries = 0 while retries <= max_retries: try: res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 data_get = res.json() # print(data_get) if res.status_code == 200: data_get_list.append(data_get) break # 成功则跳出循环 else: logger.warning(f"请求异常, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(10) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。") continue return data_get_list @staticmethod def entry_data_delete(data: dict, max_retries: int = 20, ) -> dict: """ 删除单条数据 :param data: 应包含应用ID、表单ID、数据ID :param max_retries: 最大重试次数,默认20 :return: """ url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/delete' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey 'Content-Type': 'application/json' } payload = json.dumps({ "app_id": data['api_key'], # 应用ID "entry_id": data['entry_id'], # 表单ID "data_id": data['data_id'], # 数据ID } ) retries = 0 delete_status = None while retries <= max_retries: try: res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) delete_status = res.json() # 手动处理状态码 4001 if delete_status == { "code": 4001, "msg": "Data does not exist." }: logger.info(f"返回结果:, {delete_status}") break # 成功则跳出循环 # 检查其他状态码 res.raise_for_status() # 只对非 4001 的状态码进行检查 # logger.info(f"返回结果:, {delete_status}") if res.status_code == 200: break # 成功则跳出循环 else: logger.warning(f"请求异常, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(10) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。") continue return delete_status @staticmethod def entry_data_batch_delete( data: dict, chunk_size: int = 90, max_retries: int = 20 ) -> List[Optional[requests.Response]]: # 新建多条数据 注意简道云限制1次最多100条数据 """ 批量删除数据 :param data: 应包含应用ID、表单ID、数据ID列表 :param chunk_size:单词删除最大条数,默认90 :param max_retries:重试次数,默认20 :return: """ data = replace_decimals(data) url = 'https://api.jiandaoyun.com/api/v5/app/entry/data/batch_delete' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey 'Content-Type': 'application/json' } # 获取data_list长度 total_length = len(data['data_ids']) logger.info(f"多数据删除行数: {total_length}") # 计算需要发送的次数 num_chunks = (total_length + chunk_size - 1) // chunk_size # //整除向下取证,需要加上chunk_size - 1保证不会有缺失数据 data_get_list = [] for i in range(num_chunks): start_index = i * chunk_size end_index = min(start_index + chunk_size, total_length) payload = json.dumps({ "app_id": data['api_key'], # 应用ID "entry_id": data['entry_id'], # 表单ID "data_ids": data['data_ids'][start_index:end_index], }, cls=NpEncoder) retries = 0 while retries <= max_retries: try: res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 data_get = res.json() logger.info(f"{i}页 返回结果: {data_get}") if data_get["status"] == "success": data_get_list.append(data_get) break # 成功则跳出循环 else: logger.warning(f"请求异常, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(0.1) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error( f"任务 {data['data_list'][start_index:end_index]} 连续{max_retries}次请求失败,放弃此次请求。") data_get_list.append(None) # 或者可以选择记录失败的payload以便后续处理 return data_get_list @staticmethod def workflow_instance_get(data: dict, max_retries: int = 20) -> dict: """ 查询实例流程信息 :param max_retries: :param data: 简道云插件发送过来的data,包含应用id :return: 查询简道云流程实例信息返回的结果 """ url = 'https://api.jiandaoyun.com/api/v5/workflow/instance/get' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey 'Content-Type': 'application/json' } payload = json.dumps({ "instance_id": data['data_id'], "tasks_type": 1 } ) data_get = None retries = 0 while retries <= max_retries: try: res = requests.post(url=url, data=payload, headers=headers, timeout=10) res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 data_get = res.json() # print( "返回结果:", data_get) if res.status_code == 200: break # 成功则跳出循环 else: logger.warning(f"请求异常, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(0.1) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error(f"任务 {data['data_id']} 连续{max_retries}次请求失败,放弃此次请求。") return data_get @staticmethod def workflow_task_approve(data: dict) -> dict: """ 流程待办提交 :param data:应包含username、instance_id(data_id)、task_id等信息 :return:返回简道云流程待办提交的结果 """ url = 'https://api.jiandaoyun.com/api/v1/workflow/task/approve' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey 'Content-Type': 'application/json' } payload = json.dumps({ "username": data["username"], "instance_id": data["instance_id"], "task_id": data['task_id'], "comment": "" } ) res = requests.post(url=url, data=payload, headers=headers, timeout=10) return res.json() @staticmethod def workflow_task_hand_over(data: dict, max_retries: int = 10) -> dict | None: """ 流程待办转交 :param max_retries: 最大重试次数 :param data:应包含username、instance_id(data_id)、task_id等信息 :return:返回简道云流程待办转交的结果 """ url = 'https://api.jiandaoyun.com/api/v1/workflow/task/transfer' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey 'Content-Type': 'application/json' } payload = json.dumps({ "username": data["username"], # 当前节点的负责人 "instance_id": data["instance_id"], "task_id": data['task_id'], "transfer_username": data['transfer_username'], # 转交人 "comment": "转交" } ) retries = 0 while retries <= max_retries: try: res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 if res.status_code == 200: return res.json() else: logger.warning(f"请求异常, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error( f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。") return None @staticmethod def get_upload_token(data: dict, max_retries: int = 10) -> dict[str, Any] | None: """ 获取文件上传凭证 :param max_retries: 最大重试次数 :param data: 应包含应用ID、表单ID、事务ID :return: 返回upload_url、upload_token """ url = 'https://api.jiandaoyun.com/api/v5/app/entry/file/get_upload_token' headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey 'Content-Type': 'application/json' } payload = json.dumps({ "app_id": data['api_key'], # 应用ID "entry_id": data['entry_id'], # 表单ID "transaction_id": data['transaction_id'], # 事务ID }) retries = 0 while retries <= max_retries: try: res: requests.Response = requests.post(url=url, data=payload, headers=headers, timeout=10) res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 res_j = res.json() upload_url = res_j['token_and_url_list'][0]['url'] upload_token = res_j['token_and_url_list'][0]['token'] logger.info(f"返回结果: {upload_url}, {upload_token}") if res.status_code == 200: return { 'upload_url': upload_url, 'upload_token': upload_token } else: logger.warning(f"请求异常, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error( f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。") return None @staticmethod def upload_file(data: dict, max_retries: int = 10) -> Any | None: """ 上传文件 :param max_retries: 最大重试次数 :param data: 应包含上传文件路径、上传文件url、上传文件token :return: 返回上传文件结果 """ url = data['upload_url'] headers = { 'Authorization': Config.JIANDAOYUN_API_TOKEN, # 曹伟应用api测试 appKey # 'Content-Type': 'application/json' } file_path = data['file_path'] # 上传文件路径 payload = { "token": data['upload_token'], # 上传文件token } f = open(file_path, 'rb') files = { "file": f } retries = 0 while retries <= max_retries: try: res: requests.Response = requests.post(url=url, data=payload, headers=headers, files=files, timeout=10) res.raise_for_status() # 检查HTTP响应状态码,如果不等于200会抛出异常 data_get = res.json() logger.info(f"返回结果: {data_get}") if res.status_code == 200: return data_get else: logger.warning(f"请求异常, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 except requests.exceptions.RequestException as e: logger.warning(f"请求异常: {e}, 将重新请求") retries += 1 time.sleep(3) # 在重试之间稍作停顿 if retries > max_retries: error_task_logger.error( f"任务 {data['data_list']} 连续{max_retries}次请求失败,放弃此次请求。") f.close() return None