def add_listener(self, listener=None, **kws): """为工作流添加监听器,该方法有两种使用方式。 直接使用: 直接传递一个监听器对象或者一个AbstractListener子类的类型对象 当传递监听器对象时,此对象会在每次进行execute时重复使用。 当传递类型对象时,每次进行execute都会依照此类型生成一个新的对象 类型对象的构造函数参数除self以外必须为空。 包装函数: 只传递感兴趣的生命周期 workflow.add_listener(start=on_start, finish=on_finish) """ if listener: if (not isinstance(listener, AbstractListener) and not issubclass(listener, AbstractListener)): raise InvalidArgumentException( u"监听器必须是AbstractListener对象或者是其子类类型对象") self._listeners.append(listener) return if kws: event_funcs = [] for event_name in kws: if not event_name.startswith("on_"): event_name = "on_" + event_name if event_name not in AbstractListener.EVENTS: raise InvalidArgumentException(u"不被支持的事件:" + event_name) event_funcs += (event_name, kws[event_name]) self._listeners.append(AbstractListener.wrap_function(event_funcs)) return
def load_module(module_path, module_name=None, entry_point=None): """根据不同形式的module_path来对模块进行加载 :param module_path 如果以冒号开头,那么加载指定entry_point注册的模块 如果以.py结尾,那么按照文件进行加载。 其它情况按照当前PYTHONPATH中的模块名进行加载 :return 如果加载成功,那么返回加载的模块对象 如果加载失败,那么返回None """ if module_path[0] == ":": # 加载entry_point模块 if not entry_point: raise InvalidArgumentException(u"必须指定entry_point参数") module_path = module_path[1:] for ep in pkg_resources.iter_entry_points(entry_point): if ep.name == module_path: return ep.load() return None elif module_path.endswith(".py"): # 加载文件模块 if module_name is None: module_name = os.path.split(module_path)[1].split(".")[0] if not os.path.exists(module_path): raise InvalidArgumentException(u"找不到模块文件 '{}'".format(module_path)) return imp.load_source() else: # 按模块名称加载模块 return __import__(module_path, fromlist=[""])
def __init__(self, name, plugin=None, caller=None, args=None, thread_num=10, task_num_per_thread=None, pool_type=ThreadPoolExecutor, sub_join=None, result_join=_expand_sub_results, error_action="stop", error_handler=None, error_default_value=None, goto=None): """ :param name 工作单元名称 :param plugin 插件名称 :param caller 执行逻辑 :param args 参数 :param thread_num 线程数目,默认开启10个线程 :param task_num_per_thread 每线程任务数,主要用于不可预知总数的生成器参数 :param pool_type 池对象类型 :param sub_join 针对每组线程的join逻辑,接受一个Context对象和一个结果列表作为参数 :param result_join 对最终的结果进行处理,接受一个Context对象和各个Task的结果列表 :param error_action 错误处理动作,如果是stop,那么会终止整个工作流的执行 如果是continue,那么会忽略错误继续执行 :param error_handler 错误处理器,如果错误处理动作为continue,那么不会调用上层workflow 的错误监听器,而是会调用此处的错误处理器 :param error_default_value 如果是error_action为continue,那么会以该默认值作为错误操作默认结果 :param goto 要执行的下一个工作单元 """ Job.__init__(self, name, plugin, caller, args, goto) self._thread_num = thread_num self._task_num_per_thread = task_num_per_thread self._pool_type, self._sub_join = pool_type, sub_join self._result_join, self._error_action = result_join, error_action self._error_handler = error_handler self._error_default_value = error_default_value if self._error_action != "stop" and self._error_action != "continue": raise InvalidArgumentException(u"错误处理动作只允许stop或者continue类型") try: len(args) except TypeError: if self._task_num_per_thread is None: raise InvalidArgumentException( u"args参数为无法预知长度的类型,无法自动分配任务," u"请使用task_num_per_thread参数来为每个线程分配任务数目") else: # 根据参数总数目计算每个线程应该分配的任务数 if self._task_num_per_thread is None: args_len = len(self._args) if args_len % self._thread_num == 0: self._task_num_per_thread = args_len / self._thread_num else: self._task_num_per_thread = args_len / self._thread_num + 1 self._error_break = False # 错误中断标记
def _get_runtime_args(self, context, template_args): """获取运行时参数 Job的参数分为两部分,一部分是在workflow声明时指定的参数 一部分是在运行时通过context指定的参数 参数支持两种形式,一种是参数列表,一种是字典关键字的形式 如果是列表,那么将会用context中的列表取代self._args中的列表 如果是字典,那么将会执行update操作,context中的项将覆盖_args中的项 如果self._args为None,那么直接使用context中的参数 如果self._args与context类型不一致,那么抛出异常 最终将args中的字符串变量名用Context真实值进行替换 """ context_args = context.args(self.name) if isinstance(context_args, types.FunctionType): context_args = context_args(context) if isinstance(context_args, types.StringTypes): if context_args.startswith("$"): context_args = context[context_args[1:]] else: context_args = context[context_args] args_schema = None if not context_args: args_schema = template_args elif not template_args: args_schema = context_args elif isinstance(template_args, SequenceCollectionType): if not isinstance(context_args, SequenceCollectionType): raise InvalidArgumentException( u"Job '{}'的初始参数类型跟上下文指定的参数类型不一致".format(self.name)) args_schema = template_args if context_args: args_schema = context_args elif isinstance(template_args, types.DictType): if not isinstance(context_args, types.DictType): raise InvalidArgumentException( u"Job '{}'的初始参数类型跟上下文指定的参数类型不一致".format(self.name)) args_schema = template_args if context_args: args_schema = dict(template_args) args_schema.update(context_args) else: raise InvalidArgumentException(u"只能接受列表、元组、字典类型的参数对象") # 替换为真正的变量 if not args_schema: return None elif isinstance(args_schema, SequenceCollectionType): return [parse_context_var(context, arg) for arg in args_schema] elif isinstance(args_schema, types.DictType): return { arg_name: parse_context_var(context, args_schema[arg_name]) for arg_name in args_schema }
def execute(self, context): fork_pool = context.get("_fork.pool", None) count_down_latch = context["_fork.count_down_latch"] fork_result = context["_fork.result"] try: count_down_latch. await () # 等待Fork线程执行完毕 result = None if self._join is not None: result = self._join(context, fork_result) else: result = [] for fork_end in fork_result: if fork_end.status == End.STATUS_OK: result.append(fork_end.result) elif fork_end.status == End.STATUS_BAD_REQUEST: raise InvalidArgumentException(fork_end.msg) elif fork_end.status == End.STATUS_ERROR_HAPPENED: raise fork_end.exc_value context["{}.result".format(self._name)] = result return result finally: # 清理上下文变量 if fork_pool is not None: fork_pool.shutdown() del context["_fork.pool"] del context["_fork.count_down_latch"] del context["_fork.result"]
def __init__(self, name, plugin=None, caller=None, args=None, max_items=10, timeout=None, filter=None, immediately=False, give_back_handler=None, goto=None): """ :param name 工作单元名称 :param plugin 使用插件名称 :param caller 执行逻辑 :param args 参数 :param max_items 最大条目 :param timeout 超时时间 :param filter 过滤器,接受一个context参数和一个条目,返回True or False :param immediately 如果超时,是否立即结束工作, 如果为True,那么会造成正在处理的数据丢失,但是会准时停止。 如果为False,那么会等待当前任务完成再继续, 但如果目前正在遭遇IO阻塞之类的情况,会继续阻塞很长时间。 :param give_back_handler 用于处理immediately为True时丢失的数据,比如重新归还到队列等等。 :param goto 下一步要执行的工作单元 """ Job.__init__(self, name, plugin, caller, args, goto) self._max_items, self._timeout = max_items, timeout self._filter = filter self._immediately = immediately self._give_back_handler = give_back_handler if self._timeout is not None and self._timeout < 0: raise InvalidArgumentException(u"timeout参数必须是大于等于0的整数,单位是秒")
def __init__(self, name, sub_jobs, pool=None, pool_type=ThreadPoolExecutor, join=None, error_action="stop", error_handler=None, error_default_value=None, goto=None): """ :param name 并行单元名称 :param sub_jobs 子任务列表 :param pool 指定池对象,若为None,那么会依据pool_type构建一个新的pool :param pool_type 池类型,如果pool为None,根据该类型来构建新的pool :param join 对最终结果进行处理,接受context对象和每个任务的结果列表作为参数 :param error_action 错误处理动作,如果是stop,那么会终止整个工作流, 如果是continue,那么会忽略该错误继续工作流 :param error_handler 错误处理器,当使用continue时,不会触发全局的error listener 可在此指定单独的error_handler进行处理 :param error_default_value 当处于continue的错误处理模式时,出错任务的默认值 :param goto 下一步要执行的单元 """ if self._error_action != "stop" and self._error_action != "continue": raise InvalidArgumentException(u"错误处理动作只允许stop或者continue类型")
def _validate_type(self, value): if not self._type: return if not isinstance(value, self._type): raise InvalidArgumentException( u"参数 '{name}' 的类型不正确,只允许以下类型:{types}".format(name=self._name, types=self._type))
def _check_req(self, req): if not isinstance(req, Req): if isinstance(req, types.StringTypes): req = Req("get", req) else: raise InvalidArgumentException(u"req_list参数中包含不合法的类型") return req
def _validate_regex(self, value): if not self._regex: return value = str(value) if not re.search(self._regex, value): raise InvalidArgumentException( u"参数 '{name}' 不符合正则表达式'{regex}'".format(name=self._name, regex=self._regex))
def _get_read_file_queue(self, context): if not os.path.exists(self._filepath): raise InvalidArgumentException(u"目标文件 '{}' 不存在".format( self._filepath)) read_file_queue = [] if self._pointer is None or not os.path.exists(self._pointer): read_file_queue.append((self._filepath, 0)) return read_file_queue saved_pos, saved_inode = self._read_pointer() # 比较inode是否一致 if os.stat(self._filepath).st_ino == saved_inode: read_file_queue.append((self._filepath, saved_pos)) else: if self._change_file_logic is None: raise InvalidArgumentException( u"文件已经切换,但是未指定change_file_logic文件切换逻辑") old_file = self._change_file_logic(context) read_file_queue.append((old_file, saved_pos)) read_file_queue.append((self._filepath, 0)) return read_file_queue
def __init__(self, event_funcs): super(_WrappedFunctionListener, self).__init__() if len(event_funcs) % 2 != 0: raise InvalidArgumentException( (u"event_funcs元数不正确," u"必须为(事件名1, 函数1, 事件名2, 函数2, ...)的形式")) for event_name, func in itertools.izip(event_funcs[::2], event_funcs[1::2]): if not event_name.startswith("on_"): event_name = "on_" + event_name if event_name not in AbstractListener.EVENTS: raise InvalidArgumentException(u"未知的事件:{}".format(event_name)) def event_handler_factory(func): def event_handler(ctx, f=func): return f(ctx) return event_handler setattr(self, event_name, event_handler_factory(func))
def parse_time_unit(time_unit): """将以下单位描述转换为秒 1d -> 24 * 60 * 60 1h -> 1 * 60 * 60 1m -> 1 * 60 1s -> 1 """ match_result = time_unit_regex.search(time_unit) if not match_result: raise InvalidArgumentException(u"参数 '{}' 不符合时间单位格式".format(time_unit)) time_value, unit = (int(match_result.group(1)), match_result.group(2).lower()) return time_value * time_units[unit]
def _validate_min_max(self, value): if self._min is not None: if isinstance(value, numbers.Number): if self._min > value: raise InvalidArgumentException( u"参数 '{name}' 的值不能小于{min}".format(name=self._name, min=self._min)) else: if self._min > len(value): raise InvalidArgumentException( u"参数 '{name}' 的长度不能小于{min}".format(name=self._name, min=self._min)) if self._max is not None: if isinstance(value, numbers.Number): if self._max < value: raise InvalidArgumentException( u"参数 '{name}' 的值不能大于{max}".format(name=self._name, max=self._max)) else: if self._max < len(value): raise InvalidArgumentException( u"参数 '{name}' 的长度不能大于{max}".format(name=self._name, max=self._max))
def __init__(self, attachment_file, mime_type, attachment_filename=None): """ :param attachment_file 作为附件的文件对象,可以是file对象或者StringIO对象, 如果是字符串,那么将作为文件路径进行加载 :param mime_type 附件的mime type,比如application/octet-stream :param attachment_filename 附件所使用的文件名 """ if attachment_filename is None: if isinstance(attachment_file, types.StringTypes): self._attachment_filename = os.path.split( attachment_file)[1] elif isinstance(attachment_file, types.FileType): self._attachment_filename = os.path.split( attachment_file.name)[1] else: raise InvalidArgumentException( u"必须制定attachement_filename参数作为附件文件名")
def execute(self, context, way, left, right, on, fields, name, titles=None, variable=None): """ :param context 上下文对象 :param way join方式,允许inner、left和right三种join方式 :param on join条件,left_column = right_column,多个条件用逗号隔开 :param fields 结果字段 :param name 结果表格名称 :param titles 结果表格标题 :param variable 用于存储的上下文变量 """ if way not in ("inner", "left", "right"): raise InvalidArgumentException( u"不合法的join方式'{}',只支持inner、left、right三种join方式".format(way)) conditions = self._parse_conditions(on) if isinstance(left, types.StringTypes): left = context[left] if isinstance(right, types.StringTypes): right = context[right] if way == "inner": result = self._inner_join(left, right, fields, conditions) elif way == "left": result = self._side_join("left", left, right, fields, conditions) elif way == "right": result = self._side_join("right", right, left, fields, conditions) if titles is None: titles = tuple(Title(f.split(".")[1]) for f in fields) table = ListTable(name, titles, result) if variable: context[variable] = table else: return table
def validate(self, value): """执行验证 :param value 要验证的值 """ if self._required and self._is_empty(value): raise InvalidArgumentException(u"参数 '{}' 的值是必须的,不能为空".format( self._name)) # 如果非必须并且为空,那么接下来的验证就不必运行了 if self._is_empty(value): return # 检查类型 self._validate_type(value) # 检查大小、长度 self._validate_min_max(value) # 检查正则 self._validate_regex(value) # 检查逻辑 self._validate_logic(value)
def __call__(self, context): read_file_queue = self._get_read_file_queue(context) result = [] for read_file_info in read_file_queue: filepath, pos = read_file_info record_matcher = self._record_matcher if isinstance(self._record_matcher, types.StringTypes): if self._record_matcher == "line": def record_matcher(ctx): if ctx.record_filter is None or \ ctx.record_filter(ctx.current_line): if ctx.record_handler is None: ctx.records.append(ctx.current_line) else: ctx.records.append( ctx.record_handler(ctx.current_line)) ctx.record_pos = ctx.current_pos else: raise InvalidArgumentException( u"record_matcher参数的值必须是函数或者是\"line\"") TextR.TextRecordContext(filepath=filepath, pos=pos, record_matcher=record_matcher, record_handler=self._record_handler, record_filter=self._record_filter, pointer=self._pointer, max_line=self._max_line, records=result).read() if self._result_wrapper is not None: result = self._result_wrapper(result) if self._variable: context[self._variable] = result return result
def __call__(self, context): if (self._style == "line" and self._path and not self._path.startswith(HTTP_SCHEMA)): with open(self._path, "w") as f: for row in self._object: row = self._handle_record(row) f.write(ujson.dumps(row) + "\n") return # json文本 json_text = "" if isinstance(self._object, types.FunctionType): self._object = self._object(context) elif isinstance(self._object, types.StringTypes): self._object = context[self._object] if self._style == "object": json_text = ujson.dumps(self._object) result = [] for row in self._object: row = self._handle_record(row, result.append) # 数组格式直接dump if self._style == "array": json_text = ujson.dumps(result) # line格式 if self._style == "line": string_buffer = [] for row in self._object: row = self._handle_record(row) string_buffer.append(ujson.dumps(row)) json_text = "\n".join(string_buffer) if self._path is None: if self._variable: context[self._variable] = json_text return else: raise InvalidArgumentException( u"当path为None时,必须指定一个有效的variable") if self._path.startswith(HTTP_SCHEMA): if self._http_method.lower() == "post": if self._http_field: requests.post(self._path, data={self._http_field: json_text}) elif self._style == "line": requests.post(self._path, data=json_text) else: requests.post(self._path, json=json_text) elif self._http_method.lower() == "put": requests.put(self._path, json=json_text) else: with open(self._path, "w") as f: f.write(json_text) if self._variable: context[self._variable] = json_text
def execute(self, context, server, receivers, mail=None, sender=None, subject=None, content=None, encoding="utf-8", attachments=None): """ :param server SMTP服务名称 :param receiver 收件人列表,可以是字符串组成的邮箱列表,也可以是对象列表 :param mail 用来动态表述邮件内容的邮件类 :param sender 发件人,可以传递字符串或函数,函数接受上下文以及收件人对象作为参数 :param subject 标题,可传递字符串或函数,函数参数同上 :param content 正文,可以是字符串或函数,函数参数同上 :param encoding 编码,可以是字符串或函数,函数参数同上 :param attachments 附件列表,可以是Attachment对象列表或者函数,函数参数同上 """ # 组装Mail对象列表 mail_list = [] if mail is not None: if not issubclass(mail, Mail): raise InvalidArgumentException( u"mail参数必须是girlfriend.plugin.mail.Mail类型的子类") else: mail = partial(_Mail, sender=sender, subject=subject, content=content, encoding=encoding, attachments=attachments) if isinstance(receivers, types.StringTypes): mail_list = [mail(context=context, receiver=receivers)] else: mail_list = [mail(context=context, receiver=receiver) for receiver in receivers] # 创建SMTP连接 smtp_config = _smtp_manager.get_smtp_config("smtp_" + server) if smtp_config.ssl: smtp_server = smtplib.SMTP_SSL( host=smtp_config.host, port=smtp_config.port ) else: smtp_server = smtplib.SMTP( host=smtp_config.host, port=smtp_config.port ) smtp_server.login(smtp_config.account, smtp_config.password) try: for mail in mail_list: msg = MIMEMultipart() msg['From'] = mail.sender msg['To'] = mail.receiver_email msg['Subject'] = Header(mail.subject, mail.encoding) msg.attach(MIMEText(mail.content, "html", mail.encoding)) attachments = mail.attachments if attachments: for attachment in attachments: msg.attach(attachment.build_mime_object()) smtp_server.sendmail( mail.sender, [email_address.strip() for email_address in mail.receiver_email.split(",")], msg.as_string()) finally: smtp_server.quit()
def __init__(self, workflow_list, config=None, plugin_mgr=plugin_manager, context_factory=Context, logger=None, parrent_context=None, thread_id=None): """ :param workflow_list 工作单元列表 :param config 配置数据 :param plugin_mgr 插件管理器,默认是plugin自带的entry_points管理器 :param context_factory 上下文工厂,必须具有config, args, plugin_mgr, parent 这四个约定的参数 :param logger 日志对象 """ self._workflow_list = workflow_list self._config = config or Config() self._plugin_manager = plugin_mgr self._units = {} # 以名称为Key的工作流单元引用字典 for idx, unit in enumerate(self._workflow_list): if unit.name in self._units: raise WorkflowUnitExistedException(unit.name) self._units[unit.name] = unit if unit.unittype == "job" or unit.unittype == "join": # 如果未指定goto,那么goto的默认值是下一个节点 if unit.goto is None: if idx < len(self._workflow_list) - 1: unit.goto = self._workflow_list[idx + 1].name else: unit.goto = "end" elif unit.unittype == "fork": # 自动设置起始节点 if unit.start_point is None: if idx < len(self._workflow_list) - 1: unit.start_point = self._workflow_list[idx + 1].name else: raise InvalidArgumentException( u"Fork单元 '{}' 必须指定一个有效的start_point参数") # 设置下一步运行的goto节点,如果未指定,则设置最近的join if unit.goto is None: for i, next_unit in enumerate(self._workflow_list[idx + 1:], start=idx + 1): if next_unit.unittype == "join": unit.goto = next_unit.name break else: raise InvalidArgumentException( u"Fork单元 '{}' 必须使用一个有效的goto跳转到Join".format( unit.name)) # 自动设置结束节点 if unit.end_point is None: for i, next_unit in enumerate(self._workflow_list[idx + 1:], start=idx + 1): if (next_unit.unittype == "join" and next_unit.name == unit.goto): # join unit前一个元素 unit.end_point = self._workflow_list[i - 1].name break else: raise InvalidArgumentException( u"Fork单元 '{}' 必须指定一个有效的end_point参数".format( unit.name)) self._context_factory = context_factory self._listeners = [] # 创建logger if logger is None: self._logger = create_logger("girlfriend", (stdout_handler(), )) else: self._logger = logger self._parrent_context = parrent_context self._thread_id = thread_id
def __init__(self, count): if count <= 0: raise InvalidArgumentException(u"count参数必须为正整数") self._count = count self._awaiting_count = count self._condition = threading.Condition()
def add_four(ctx, num): if num < 0: raise InvalidArgumentException("num must more than zero!") return num + 4
def parse(cls, statement): match_result = _JoinCondition.PATTERN.search(statement) if not match_result: raise InvalidArgumentException(u"表达式'{}'格式不合法".format(statement)) return cls(match_result.group(1), match_result.group(2))
def _validate_logic(self, value): if self._logic is None: return msg = self._logic(value) if msg: raise InvalidArgumentException(msg)