You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1132 lines
50 KiB

  1. # vim: fileencoding=utf-8
  2. #
  3. import re
  4. import inspect
  5. from typing import Callable, Any, Union, List, Generator, Optional
  6. from calculate.templates.template_processor import DirectoryProcessor
  7. from calculate.variables.datavars import (
  8. DependenceAPI,
  9. DependenceError,
  10. VariableNode,
  11. NamespaceNode,
  12. HashType,
  13. TableType,
  14. StringType,
  15. )
  16. from calculate.variables.loader import Datavars
  17. from calculate.utils.io_module import IOModule
  18. from collections.abc import Iterable, Mapping
  19. class TaskInitializationError(Exception):
  20. pass
  21. class ScriptError(Exception):
  22. pass
  23. class TaskError(Exception):
  24. def __init__(self, message, handlers=set()):
  25. super().__init__(message)
  26. self.handlers = handlers
  27. class ActionError(Exception):
  28. '''Класс исключения для создания штатных исключений в функциях действий.'''
  29. pass
  30. class ConditionError(Exception):
  31. pass
  32. class Script:
  33. '''Класс скрипта, собирает задачи, обработчики, блоки и прочее, хранит их
  34. в себе. Создает экземпляры лаунчеров.'''
  35. def __init__(self, script_id: str,
  36. args: List[Any] = [],
  37. success_message: Optional[str] = None,
  38. failed_message: Optional[str] = None,
  39. interrupt_message: Optional[str] = None):
  40. self._id: str = script_id
  41. self.__items: List[Union['Task', 'Block', 'Handler', 'Run']] = None
  42. self.__tasks_is_set: bool = False
  43. self.__args: list = args
  44. self.__success_message: Optional[str] = success_message
  45. self.__failed_message: Optional[str] = failed_message
  46. self.__interrupt_message: Optional[str] = interrupt_message
  47. @property
  48. def id(self) -> str:
  49. return self._id
  50. @property
  51. def args(self) -> list:
  52. return self.__args
  53. @property
  54. def items(self) -> list:
  55. return self.__items
  56. @property
  57. def success_message(self) -> str:
  58. return self.__success_message
  59. @property
  60. def failed_message(self) -> str:
  61. return self.__failed_message
  62. @property
  63. def interrupt_message(self) -> str:
  64. return self.__interrupt_message
  65. def tasks(self, *items: List[Union['Task', 'Block', 'Handler', 'Run']]
  66. ) -> 'Script':
  67. '''Метод для указания задач и контейнеров, из которых состоит скрипт.
  68. '''
  69. if self.__tasks_is_set:
  70. raise ScriptError("Script object is immutable.")
  71. self.__items = items
  72. self.__tasks_is_set = bool(self.__items)
  73. return self
  74. def make_launcher(self, output: IOModule,
  75. datavars: Union[Datavars, NamespaceNode],
  76. namespace: NamespaceNode = None) -> 'ScriptLauncher':
  77. '''Метод для создания экземпляра объекта, для запуска данного
  78. скрипта.'''
  79. return ScriptLauncher(self, output, datavars, namespace)
  80. class ScriptLauncher:
  81. def __init__(self, script: Script,
  82. output: IOModule,
  83. datavars: Union[Datavars, NamespaceNode],
  84. namespace: NamespaceNode):
  85. '''Метод для инициализации, состоящей в установке модулей вывода,
  86. модуля переменных, текущего пространства имен, а также создании
  87. переменных скрипта.'''
  88. self._script = script
  89. self._output = output
  90. self._datavars = datavars
  91. self._namespace = namespace
  92. self._script_namespace, self._args_vars = self.make_script_variables(
  93. script.id,
  94. script.args,
  95. datavars)
  96. def run(self) -> None:
  97. '''Метод для запуска задач, содержащихся в данном скрипте.'''
  98. essential_error = None
  99. founded_handlers = []
  100. handlers_to_run = set()
  101. for task_item in self._script.items:
  102. if isinstance(task_item, Handler):
  103. founded_handlers.append(task_item)
  104. elif essential_error is None:
  105. try:
  106. output = task_item.run(self._output,
  107. self._datavars,
  108. self._script_namespace,
  109. self._namespace)
  110. handlers_to_run.update(output)
  111. except Exception as error:
  112. if isinstance(error, TaskError):
  113. handlers_to_run.update(error.handlers)
  114. essential_error = error
  115. for handler in founded_handlers:
  116. if handler.id not in handlers_to_run:
  117. continue
  118. try:
  119. handler.run(self._output, self._datavars,
  120. self._script_namespace, self._namespace)
  121. except Exception as error:
  122. # TODO Разобраться что делать с ошибками в обработчике.
  123. self._output.set_error(f"error in handler '{task_item.id}':"
  124. f" {str(error)}")
  125. if essential_error is not None:
  126. raise essential_error
  127. @staticmethod
  128. def make_script_variables(script_id,
  129. args: List[Any],
  130. datavars: Union[Datavars, NamespaceNode]
  131. ) -> NamespaceNode:
  132. '''Метод для создания переменных скрипта. Создает пространство имен
  133. скрипта и переменных аргументов.'''
  134. if 'scripts' not in datavars:
  135. scripts = ScriptLauncher.make_scripts_namespace(datavars)
  136. else:
  137. scripts = datavars.scripts
  138. if script_id not in scripts:
  139. current_script = NamespaceNode(script_id)
  140. scripts.add_namespace(current_script)
  141. else:
  142. current_script = scripts[script_id]
  143. # current_script.clear()
  144. args_vars = []
  145. for arg in args:
  146. if arg not in current_script._variables:
  147. args_vars.append(VariableNode(arg, current_script))
  148. else:
  149. args_vars.append(current_script._variables[arg])
  150. return current_script, args_vars
  151. @staticmethod
  152. def make_scripts_namespace(datavars: Union[Datavars, NamespaceNode]
  153. ) -> NamespaceNode:
  154. '''Метод создающий пространства имен, необходимые для работы задач.'''
  155. # Для тестирования скорее.
  156. scripts = NamespaceNode('scripts')
  157. datavars.add_namespace(scripts)
  158. env = NamespaceNode('env')
  159. scripts.add_namespace(env)
  160. loop = NamespaceNode('loop')
  161. env.add_namespace(loop)
  162. return scripts
  163. def __call__(self, *args) -> None:
  164. '''Метод для запуска скрипта с аргументами.'''
  165. if len(args) < len(self._script.args):
  166. raise ScriptError(
  167. (f"script '{self._script.id}' missing"
  168. f" {len(self._script.args) - len(args)} required"
  169. " arguments: '") +
  170. "', '".join(self._script.args[len(args):]) + "'")
  171. elif len(args) > len(self._script.args):
  172. raise ScriptError(f"script '{self._script.id}' takes"
  173. f" {len(self._script.args)} arguments, but"
  174. f" {len(args)} were given")
  175. args_values = self._get_args_values(args, self._datavars,
  176. self._namespace)
  177. for arg_variable, arg_value in zip(self._args_vars, args_values):
  178. arg_variable.source = arg_value
  179. self.run()
  180. def _get_args_values(self, args: List[Any],
  181. datavars: Union[Datavars, NamespaceNode],
  182. namespace: NamespaceNode) -> list:
  183. '''Метод для получения значений аргументов.'''
  184. args_values = []
  185. for argument in args:
  186. if isinstance(argument, str):
  187. variable = DependenceAPI.find_variable(
  188. argument, datavars,
  189. current_namespace=namespace)
  190. args_values.append(variable.get_value())
  191. elif isinstance(argument, Static):
  192. args_values.append(argument.value)
  193. else:
  194. args_values.append(argument)
  195. return args_values
  196. class RunTemplates:
  197. '''Класс запускателя наложения шаблонов.'''
  198. def __init__(self, id: str = '',
  199. action: str = '',
  200. package: str = '',
  201. chroot_path: str = '',
  202. root_path: str = '',
  203. dbpkg: bool = True,
  204. essential: bool = True,
  205. **group_packages):
  206. self._id: str = id
  207. self._action: str = action
  208. self._package: str = package or None
  209. self._chroot_path: str = chroot_path or None
  210. self._root_path: str = root_path or None
  211. self._dbpkg = dbpkg
  212. self._essential = essential
  213. self._groups: dict = group_packages
  214. def _initialize(self, output: IOModule,
  215. datavars: Union[Datavars, NamespaceNode]):
  216. # Проверяем наличие идентификатора.
  217. if not self._id:
  218. error_msg = "id is not set for templates runner"
  219. if self._essential:
  220. raise TaskError(error_msg)
  221. else:
  222. output.set_error(error_msg)
  223. return set()
  224. # Проверяем наличие параметра action, который обязателен.
  225. if not self._action:
  226. error_msg = ("action parameter is not set for templates runner"
  227. f" '{self._id}'")
  228. if self._essential:
  229. raise TaskError(error_msg)
  230. else:
  231. output.set_error(error_msg)
  232. return set()
  233. # Если установлен chroot_path -- устанавливаем значение соответствующей
  234. # переменной.
  235. if self._chroot_path is not None:
  236. if 'cl_chroot_path' in datavars.main:
  237. datavars.main['cl_chroot_path'].source = self._chroot_path
  238. else:
  239. VariableNode("cl_chroot_path", datavars.main,
  240. variable_type=StringType,
  241. source=self._chroot_path)
  242. # Если установлен root_path -- устанавливаем значение соответствующей
  243. # переменной.
  244. if self._root_path is not None:
  245. if 'cl_root_path' in datavars.main:
  246. datavars.main['cl_root_path'].source = self._root_path
  247. else:
  248. VariableNode("cl_root_path", datavars.main,
  249. variable_type=StringType,
  250. source=self._chroot_path)
  251. if self._groups:
  252. groups = list(self._groups.keys())
  253. for group, atoms in groups:
  254. if isinstance(atoms, str):
  255. self._groups[group] = [name.strip() for name
  256. in atoms.split(',')]
  257. def run(self, output: IOModule,
  258. datavars: Union[Datavars, NamespaceNode],
  259. script_namespace: NamespaceNode,
  260. namespace: NamespaceNode) -> set:
  261. '''Метод для запуска наложения шаблонов.'''
  262. self._initialize(output, datavars)
  263. template_processor = DirectoryProcessor(self._action,
  264. datavars_module=datavars,
  265. package=self._package,
  266. output_module=output,
  267. dbpkg=self._dbpkg,
  268. namespace=namespace,
  269. **self._groups)
  270. changed_files = template_processor.process_template_directories()
  271. self._create_result_var(script_namespace,
  272. changed_files=changed_files)
  273. return set()
  274. def _create_result_var(self, script_namespace: NamespaceNode,
  275. changed_files: dict = {},
  276. skipped: list = []) -> None:
  277. '''Метод для создания переменной задачи, в которую отправляются
  278. результаты вычислений, имеющихся в функции action.'''
  279. VariableNode(self._id, script_namespace, variable_type=HashType,
  280. source={"changed": changed_files,
  281. "skipped": skipped})
  282. class Run:
  283. '''Класс запускателя скриптов.'''
  284. def __init__(self, script: Script, namespace: str = None,
  285. args: List[Any] = [],
  286. when: 'Var' = None,
  287. essential: bool = True):
  288. self._script: Script = script
  289. self._namespace: str = namespace
  290. self._args: List[Any] = args
  291. self._condition: Var = when
  292. self._essential = essential
  293. def run(self, output: IOModule,
  294. datavars: Union[Datavars, NamespaceNode],
  295. parent_namespace: NamespaceNode,
  296. namespace: NamespaceNode) -> set:
  297. '''Метод для запуска скрипта, указанного в запускателе.'''
  298. if self._condition is None or self._condition(datavars,
  299. namespace,
  300. parent_namespace):
  301. # пространство имен или наследуем, либо берем по указанному пути.
  302. if not self._namespace:
  303. self._namespace = namespace
  304. elif isinstance(self._namespace, str):
  305. parts = self._namespace.split('.')
  306. parts.reverse()
  307. self._namespace = datavars[parts.pop()]
  308. while parts:
  309. self._namespace = self._namespace[parts.pop()]
  310. try:
  311. # if not self._script._initialized:
  312. launcher = self._script.make_launcher(
  313. output, datavars,
  314. namespace=self._namespace)
  315. launcher(*self._args)
  316. except (ScriptError, TaskError) as error:
  317. if self._essential:
  318. raise ScriptError(f"essential script '{self._script._id}'"
  319. f" is failed. Reason: {error}")
  320. output.set_error(f"essential script '{self._script._id}'"
  321. f" is failed. Reason: {error}")
  322. return set()
  323. class Static:
  324. '''Класс, оборачивающий значение, которое не должно восприниматься как
  325. переменная.'''
  326. def __init__(self, value: Any):
  327. self._value: Any = value
  328. @property
  329. def value(self) -> Any:
  330. return self._value
  331. class Task:
  332. '''Класс реализующий задачи -- объекты, исполняющие указанные функции
  333. действия action, и содержащий всю необходимую информацию для их работы.'''
  334. def __init__(self, **kwargs):
  335. if 'id' in kwargs:
  336. self._id: str = kwargs['id']
  337. else:
  338. raise TaskInitializationError("id is not set for task")
  339. # Имя не обязательно.
  340. self._name: str = kwargs.get('name', None)
  341. if 'action' in kwargs:
  342. self._action = kwargs['action']
  343. else:
  344. raise TaskInitializationError("action is not set for task"
  345. f" '{self._name or self._id}'")
  346. # Список аргументов и их типы
  347. self._args_sources: Union[tuple, list] = kwargs.get('args', [])
  348. self._args_types: List[type] = []
  349. self._arg_names: List[str] = []
  350. self._args: Union[tuple, list] = []
  351. # Переменные, которым через set будет присвоен результат работы action.
  352. self._set: Union[list, tuple] = kwargs.get('set', None)
  353. self._return_type: type = None
  354. # Условие выполнения задачи.
  355. self._condition: Var = kwargs.get('when', None)
  356. # Существенность задачи. Если задача существенная, ошибка в ней
  357. # приведет к остановке скрипта, если несущественная -- ошибка будет
  358. # записана в переменную этой задачи, а скрипт будет выполняться дальше.
  359. self._essential: bool = kwargs.get('essential', True)
  360. # Параметр для указания обработчиков, которые нужно запустить в конце
  361. # выполнения скрипта.
  362. self._handlers = set()
  363. handlers: set = kwargs.get('notify', set())
  364. if handlers:
  365. if isinstance(handlers, (tuple, list)):
  366. self._handlers.update(handlers)
  367. else:
  368. self._handlers.add(handlers)
  369. self._initialized: bool = False
  370. self._namespace: NamespaceNode = None
  371. self._script_namespace: NamespaceNode = None
  372. self._datavars: Union[Datavars, NamespaceNode] = None
  373. self._output: IOModule = None
  374. @property
  375. def essential(self) -> bool:
  376. return self._essential
  377. @property
  378. def initialized(self) -> bool:
  379. return self._initialized
  380. def _initialize(self, output: IOModule,
  381. datavars: Union[Datavars, NamespaceNode],
  382. script_namespace: NamespaceNode,
  383. namespace: NamespaceNode) -> 'Task':
  384. '''Метод для инициализации задач, привязывает задачу к указанному
  385. модулю переменных, модулю вывода, формирует список аргументов,
  386. определяет их типы и находит переменные, к которым в последствии
  387. нужно будет присвоить значение, возвращенное action.'''
  388. self._datavars = datavars
  389. self._output = output
  390. self._namespace = namespace
  391. self._script_namespace = script_namespace
  392. if not self._initialized:
  393. self._arg_names,\
  394. self._args_types,\
  395. self._return_type = self._get_action_info(self._action)
  396. self._args = self._update_arguments(self._args_sources)
  397. if self._set is not None:
  398. self._set = self._get_set_variables(self._set, datavars,
  399. namespace)
  400. self._initialized = True
  401. else:
  402. self._args = self._update_arguments(self._args_sources)
  403. def run(self, output: IOModule,
  404. datavars: Union[Datavars, NamespaceNode],
  405. script_namespace: NamespaceNode,
  406. namespace: NamespaceNode) -> set:
  407. '''Метод для запуска данной задачи.'''
  408. self._initialize(output, datavars, script_namespace, namespace)
  409. if (self._condition is None or self._condition(
  410. self._datavars,
  411. self._namespace,
  412. self._script_namespace)):
  413. arguments_values = self._get_args_values(self._args,
  414. self._args_types)
  415. try:
  416. result = self._action(*arguments_values)
  417. self._create_task_var(result=result,
  418. success=True)
  419. if self._return_type:
  420. self._check_return_type(result)
  421. if self._set:
  422. self._set_variables(result)
  423. return self._handlers
  424. except Exception as error:
  425. if self._essential and not isinstance(error, ActionError):
  426. raise TaskError(
  427. f"essential task '{self._name or self._id}' is"
  428. f" failed. Reason: {error}")
  429. self._create_task_var(success=False,
  430. error_message=str(error))
  431. return set()
  432. def _get_action_info(self, action: Callable):
  433. '''Метод для получения информации о функции действия.'''
  434. arguments = [arg.name for arg in inspect.signature(
  435. action).parameters.values()]
  436. annotation = action.__annotations__
  437. action_types = []
  438. for argument in arguments:
  439. action_types.append(annotation.get(argument, None))
  440. return arguments, action_types, annotation.get('return', None)
  441. def _update_arguments(self, args_sources: List[Any]) -> tuple:
  442. '''Метод для обновления и установки аргументов функции действия.'''
  443. arguments = []
  444. # Проверяем количество аргументов.
  445. args_length = len(self._arg_names)
  446. if 'output' in self._arg_names:
  447. if (args_length - 1) != len(self._args_sources):
  448. raise TaskInitializationError(f"action function takes"
  449. f" {args_length - 1} arguments"
  450. f" but {len(args_sources)}"
  451. " is given")
  452. elif args_length != len(self._args_sources):
  453. raise TaskInitializationError(f"action function takes"
  454. f" {args_length} arguments but"
  455. f" {len(args_sources)} is given")
  456. # Формируем список значений аргументов.
  457. args_iterator = iter(args_sources)
  458. for arg_name in self._arg_names:
  459. if arg_name == 'output':
  460. arguments.append(self._output)
  461. else:
  462. argument = next(args_iterator)
  463. if isinstance(argument, str):
  464. try:
  465. variable = DependenceAPI.find_variable(
  466. argument,
  467. self._datavars,
  468. current_namespace=self._namespace)
  469. arguments.append(variable)
  470. except DependenceError as error:
  471. raise TaskInitializationError(str(error))
  472. elif isinstance(argument, Static):
  473. arguments.append(argument.value)
  474. else:
  475. arguments.append(argument)
  476. return tuple(arguments)
  477. def _get_args_values(self, args: List[Any],
  478. args_types: List[type]) -> List[Any]:
  479. '''Метод для получения значений из списка аргументов функции action.'''
  480. arguments_values = []
  481. for arg, arg_type in zip(args, args_types):
  482. if isinstance(arg, VariableNode):
  483. value = arg.get_value()
  484. if arg.variable_type is HashType:
  485. value = value.get_hash()
  486. elif arg.variable_type is TableType:
  487. value = value.get_table()
  488. arguments_values.append(self._check_type(value, arg_type))
  489. else:
  490. arguments_values.append(self._check_type(arg, arg_type))
  491. return arguments_values
  492. def _check_type(self, value: Any, arg_type: type) -> Any:
  493. '''Метод для проверки типа аргумента.'''
  494. if arg_type is None or isinstance(value, arg_type):
  495. return value
  496. else:
  497. raise TaskError(f"'{arg_type}' is expected, not {type(value)}")
  498. def _create_task_var(self,
  499. result: Any = None,
  500. success: bool = True,
  501. error_message: str = None) -> None:
  502. '''Метод для создания переменной задачи, в которую отправляются
  503. результаты вычислений, имеющихся в функции action.'''
  504. VariableNode(self._id, self._script_namespace, variable_type=HashType,
  505. source={'result': result,
  506. 'success': success,
  507. 'error_message': error_message})
  508. def _get_set_variables(self, to_set: Union[str, list, tuple, dict],
  509. datavars: Union[NamespaceNode, Datavars],
  510. namespace: NamespaceNode) -> Union[str, list, dict]:
  511. '''Метод для поиска переменных, в которые нужно положить значения,
  512. полученные в результате вычислений в action.'''
  513. if isinstance(to_set, str):
  514. return DependenceAPI.find_variable(to_set, datavars,
  515. current_namespace=namespace)
  516. elif isinstance(to_set, (list, tuple)):
  517. vars_to_set = []
  518. for var in to_set:
  519. vars_to_set.append(
  520. DependenceAPI.find_variable(
  521. var, datavars,
  522. current_namespace=namespace))
  523. return vars_to_set
  524. elif isinstance(to_set, dict):
  525. for key in to_set:
  526. variable = DependenceAPI.find_variable(
  527. to_set[key], datavars,
  528. current_namespace=namespace)
  529. to_set[key] = variable
  530. return to_set
  531. else:
  532. raise TaskError(f"set value must be {dict}, {list}, {tuple} or "
  533. f"{str}, not {type(to_set)}")
  534. def _set_variables(self, result: Any) -> None:
  535. '''Метод для присвоения переменным результатов работы функции action.
  536. '''
  537. self._check_set_vars_types(result)
  538. if isinstance(self._set, dict):
  539. if not isinstance(result, dict):
  540. raise TaskError(f"set parameter value is '{dict}' but action"
  541. f" result is '{type(result)}'")
  542. for key in self._set:
  543. if key in result:
  544. self._set[key].set(result[key])
  545. elif isinstance(self._set, (list, tuple)):
  546. for var in self._set:
  547. var.set(result)
  548. else:
  549. self._set.set(result)
  550. def _check_set_vars_types(self, result: Any) -> None:
  551. '''Метод для проверки соответствия типов значений, полученных из
  552. action, и переменных, в которые нужно положить эти значения.'''
  553. # Теперь проверяем соответствие типа переменных из set и типа значений.
  554. if isinstance(self._set, dict):
  555. for key, variable in self._set.items():
  556. if (key in result and not
  557. isinstance(result[key],
  558. variable.variable_type.python_type)):
  559. raise TaskError("can not set value of"
  560. f" '{type(result[key])}' type to the"
  561. " variable of"
  562. f" '{variable.variable_type.name}'"
  563. " type")
  564. elif isinstance(self._set, (list, tuple)):
  565. for variable in self._set:
  566. if not isinstance(result, variable.variable_type.python_type):
  567. raise TaskError(f"can not set value of '{type(result)}'"
  568. " type to the variable of"
  569. f" '{variable.variable_type.name}' type")
  570. else:
  571. if not isinstance(result, self._set.variable_type.python_type):
  572. raise TaskError(f"can not set value of '{type(result)}'"
  573. " type to the variable of"
  574. f" '{self._set.variable_type.name}' type")
  575. def _check_return_type(self, result: Any) -> None:
  576. '''Метод для проверки типа значения, возвращенного функцией action.'''
  577. if not isinstance(result, self._return_type):
  578. raise TaskError(f"action returned value of '{type(result)}' "
  579. f"type, but expected is '{self._return_type}'")
  580. class Loop:
  581. '''Базовый класс всех объектов цикла для блоков.'''
  582. def initialize_loop(self, datavars: Union[Datavars, NamespaceNode],
  583. script_namespace: NamespaceNode,
  584. namespace: NamespaceNode) -> None:
  585. '''Метод для инициализации объекта цикла.'''
  586. pass
  587. def get_looper(self) -> Generator[bool, bool, None]:
  588. '''Генератор, предназначенный для управления циклом при запуске блока
  589. задач.'''
  590. yield True
  591. yield False
  592. class For(Loop):
  593. '''Класс цикла блока, использующего указанное итерируемое значение.'''
  594. def __init__(self, value: str, iter_arg: Union[str, Iterable]):
  595. self._iter_arg = iter_arg
  596. self._item_name = value
  597. self._iterable_value = None
  598. self._item_variable = None
  599. self._initialized = False
  600. def initialize_loop(self, datavars: Union[Datavars, NamespaceNode],
  601. script_namespace: NamespaceNode,
  602. namespace: NamespaceNode) -> None:
  603. '''Метод для инициализации объекта цикла.'''
  604. self._iterable_value = self._get_iterable_value(self._iter_arg,
  605. datavars,
  606. namespace)
  607. self._item_variable = self._get_item_variable(self._item_name,
  608. script_namespace)
  609. self._initialized = True
  610. def get_looper(self) -> Generator[bool, bool, None]:
  611. '''Генератор, предназначенный для управления циклом при запуске блока
  612. задач.'''
  613. if not self._initialized:
  614. yield False
  615. else:
  616. for item in self._iterable_value:
  617. self._item_variable.source = item
  618. yield True
  619. yield False
  620. def _get_iterable_value(self, iter_argument: Union[str, Iterable],
  621. datavars: Union[Datavars, NamespaceNode],
  622. namespace: NamespaceNode) -> Iterable:
  623. '''Метод для получения и проверки значения, которое будем далее
  624. итерировать.'''
  625. value = self._iter_arg
  626. if isinstance(self._iter_arg, str):
  627. variable = DependenceAPI.find_variable(self._iter_arg,
  628. datavars,
  629. current_namespace=namespace)
  630. value = variable.get_value()
  631. if variable.variable_type is HashType:
  632. value = value.get_hash()
  633. elif variable.variable_type is TableType:
  634. value = value.table_hash()
  635. if not isinstance(value, Iterable):
  636. raise TaskError("For() argument '{value}' is not iterable.")
  637. if isinstance(value, Mapping):
  638. value = value.items()
  639. return value
  640. def _get_item_variable(self, item_name: str,
  641. script_namespace: NamespaceNode) -> VariableNode:
  642. '''Метод для получения или создания переменной, в которой будет
  643. храниться текущее значение, взятое из итерируемого значения.'''
  644. if item_name in script_namespace:
  645. item_variable = script_namespace[item_name]
  646. else:
  647. item_variable = VariableNode(item_name, script_namespace)
  648. return item_variable
  649. class While(Loop):
  650. '''Класс цикла блока, аналогичного while.'''
  651. def __init__(self, condition: 'Var'):
  652. self._condition: Var = condition
  653. self._datavars: Union[Datavars, NamespaceNode] = None
  654. self._namespace: NamespaceNode = None
  655. def initialize_loop(self, datavars: Union[Datavars, NamespaceNode],
  656. script_namespace: NamespaceNode,
  657. namespace: NamespaceNode) -> None:
  658. '''Метод для инициализации объекта цикла.'''
  659. self._datavars: Union[Datavars, NamespaceNode] = datavars
  660. self._namespace: Union[NamespaceNode, None] = namespace
  661. self._script_namespace: NamespaceNode = script_namespace
  662. def get_looper(self) -> Generator[bool, bool, None]:
  663. '''Генератор, предназначенный для управления циклом при запуске блока
  664. задач.'''
  665. while True:
  666. condition_result = self._condition(self._datavars,
  667. self._namespace,
  668. self._script_namespace)
  669. yield condition_result
  670. class Until(Loop):
  671. '''Класс цикла блока, аналогичного конструкции do ... while.'''
  672. def __init__(self, condition: 'Var'):
  673. self._condition: Var = condition
  674. self._datavars: Union[Datavars, NamespaceNode] = None
  675. self._namespace: Datavars = None
  676. def initialize_loop(self, datavars: Union[Datavars, NamespaceNode],
  677. script_namespace: NamespaceNode,
  678. namespace: NamespaceNode) -> None:
  679. '''Метод для инициализации объекта цикла.'''
  680. self._datavars: Union[Datavars, NamespaceNode] = datavars
  681. self._namespace: Union[NamespaceNode, None] = namespace
  682. self._script_namespace: NamespaceNode = script_namespace
  683. def get_looper(self) -> Generator[bool, bool, None]:
  684. '''Генератор, предназначенный для управления циклом при запуске блока
  685. задач.'''
  686. yield True
  687. while True:
  688. yield self._condition(self._datavars,
  689. self._namespace,
  690. self._script_namespace)
  691. class Block:
  692. '''Класс блока задач.'''
  693. def __init__(self, *tasks: List[Task],
  694. when: Union['Var', None] = None,
  695. loop: Loop = Loop()):
  696. self._tasks: List[Task] = tasks
  697. self._rescue_tasks: List[Task] = None
  698. self._condition: 'Var' = when
  699. if not isinstance(loop, Loop):
  700. raise TaskError('loop block parameter must be Loop type')
  701. self._loop: Loop = loop
  702. def run(self, output: IOModule,
  703. datavars: Union[Datavars, NamespaceNode],
  704. script_namespace: NamespaceNode,
  705. namespace: NamespaceNode) -> None:
  706. '''Метод для запуска выполнения задач, содержащихся в блоке.'''
  707. handlers = set()
  708. if (self._condition is None or self._condition(datavars, namespace,
  709. script_namespace)):
  710. self._loop.initialize_loop(datavars, script_namespace, namespace)
  711. looper = self._loop.get_looper()
  712. while next(looper):
  713. for task in self._tasks:
  714. try:
  715. output = task.run(output, datavars, script_namespace,
  716. namespace)
  717. handlers.update(output)
  718. except Exception as error:
  719. if self._rescue_tasks is not None:
  720. self._run_rescue(output, datavars,
  721. script_namespace, namespace)
  722. if isinstance(error, TaskError):
  723. error.handlers.update(handlers)
  724. raise error
  725. return handlers
  726. def _run_rescue(self, output: IOModule,
  727. datavars: Union[Datavars, NamespaceNode],
  728. script_namespace: NamespaceNode,
  729. namespace: NamespaceNode) -> None:
  730. '''Метод для запуска задач, указанных в rescue. Эти задачи выполняются,
  731. если возникает ошибка при выполнении важной задачи, содержащейся в
  732. блоке.'''
  733. for task in self._rescue_tasks:
  734. try:
  735. task.run(output, datavars, script_namespace, namespace)
  736. except Exception:
  737. # TODO разобраться с тем, что делать если ошибка закралась в
  738. # задачи из rescue.
  739. pass
  740. def rescue(self, *tasks: List[Task]) -> 'Block':
  741. '''Метод для задания задач, выполняемых, если некоторая важная задача,
  742. содержащаяся в блоке выполняется с ошибкой.'''
  743. self._rescue_tasks = tasks
  744. return self
  745. class Handler:
  746. '''Класс обработчика -- объекта скрипта, имеющего идентификатор,
  747. запускаемого в конце выполнения скрипта в том случае, если его
  748. идентификатор присутствовал в параметре notify хотя бы у одной успешно
  749. выполненной задачи.'''
  750. def __init__(self, handler_id, *tasks: List[Task]):
  751. self._id: str = handler_id
  752. self._tasks: List[Task] = tasks
  753. @property
  754. def id(self) -> str:
  755. return self._id
  756. def run(self, output: IOModule,
  757. datavars: Union[Datavars, NamespaceNode],
  758. script_namespace: NamespaceNode,
  759. namespace: NamespaceNode) -> None:
  760. '''Метод для запуска выполнения задач, содержащихся в обработчике.'''
  761. handlers = set()
  762. for task in self._tasks:
  763. try:
  764. handlers.update(task.run(output, datavars, script_namespace,
  765. namespace))
  766. except Exception as error:
  767. output.set_error(f"error during execution handle '{self._id}'"
  768. f" in task '{task._name or task._id}':"
  769. f" {str(error)}")
  770. return handlers
  771. class ConditionValue:
  772. '''Базовый класс для всех объектов, предназначенных для создания условий.
  773. '''
  774. def __init__(self, value: Any):
  775. self.value: Any = value
  776. def get(self, datavars: Union[Datavars, NamespaceNode],
  777. namespace: NamespaceNode,
  778. script_namespace: NamespaceNode) -> Any:
  779. return self.value
  780. class Var(ConditionValue):
  781. '''Класс для создания условий выполнения задач.'''
  782. def __init__(self, *variable: List[VariableNode],
  783. value: Any = None, other: Any = None,
  784. tasks: Union[List[str], None] = None,
  785. function: Union[Callable, None] = None):
  786. if variable:
  787. self.variable: Union[VariableNode, str, None] = variable[0]
  788. else:
  789. self.variable = None
  790. self.value: Any = value
  791. self.other: Any = other
  792. self.function: Callable = function
  793. self.tasks: List[str] = tasks
  794. def get(self, datavars: Union[Datavars, NamespaceNode],
  795. namespace: NamespaceNode,
  796. script_namespace: NamespaceNode) -> Any:
  797. '''Метод для расчета итогового значения ноды условия выполнения задачи.
  798. '''
  799. if self.function is not None:
  800. if self.tasks:
  801. return self.function(self.tasks, script_namespace)
  802. elif self.other is not None:
  803. return self.function(self.get_value(datavars, namespace,
  804. script_namespace),
  805. self.get_other(datavars, namespace,
  806. script_namespace))
  807. else:
  808. return self.function(self.get_value(datavars, namespace,
  809. script_namespace))
  810. return self.get_value(datavars, namespace, script_namespace)
  811. def get_value(self, datavars: Union[Datavars, NamespaceNode],
  812. namespace: NamespaceNode,
  813. script_namespace: NamespaceNode) -> Any:
  814. '''Метод для получения собственного значения ноды.'''
  815. if self.value is not None:
  816. return self.value.get(datavars, namespace, script_namespace)
  817. elif self.variable is not None:
  818. if isinstance(self.variable, str):
  819. self.variable = DependenceAPI.find_variable(
  820. self.variable, datavars,
  821. current_namespace=namespace)
  822. return self.variable.get_value()
  823. else:
  824. raise ConditionError('Can not get value for condition')
  825. def get_other(self, datavars: Union[Datavars, NamespaceNode],
  826. namespace: NamespaceNode,
  827. script_namespace: NamespaceNode) -> Any:
  828. '''Метод для получения значения ноды, необходимой для выполнения
  829. бинарной операции.'''
  830. if isinstance(self.other, ConditionValue):
  831. return self.other.get(datavars, namespace, script_namespace)
  832. else:
  833. return self.other
  834. def __call__(self, datavars: Union[Datavars, NamespaceNode],
  835. namespace: NamespaceNode,
  836. script_namespace: NamespaceNode) -> Any:
  837. return self.get(datavars, namespace, script_namespace)
  838. def __eq__(self, other: Any) -> 'Var':
  839. return Var(value=self, other=other, function=lambda x, y: x == y)
  840. def __ne__(self, other: Any) -> 'Var':
  841. return Var(value=self, other=other, function=lambda x, y: x != y)
  842. def __lt__(self, other: Any) -> 'Var':
  843. return Var(value=self, other=other, function=lambda x, y: x < y)
  844. def __gt__(self, other: Any) -> 'Var':
  845. return Var(value=self, other=other, function=lambda x, y: x > y)
  846. def __le__(self, other: Any) -> 'Var':
  847. return Var(value=self, other=other, function=lambda x, y: x <= y)
  848. def __ge__(self, other: Any) -> 'Var':
  849. return Var(value=self, other=other, function=lambda x, y: x >= y)
  850. def __and__(self, other: bool) -> 'Var':
  851. return Var(value=self, other=other, function=lambda x, y: x and y)
  852. def __or__(self, other: bool) -> 'Var':
  853. return Var(value=self, other=other, function=lambda x, y: x or y)
  854. def __invert__(self) -> 'Var':
  855. return Var(value=self, function=lambda x: not x)
  856. def __lshift__(self, other: Any) -> 'Var':
  857. '''Метод для переопределения операции <<, которая теперь играет роль
  858. in.'''
  859. return Var(value=self, other=other, function=lambda x, y: y in x)
  860. def has(self, other: Any) -> 'Var':
  861. '''Метод аналогичный операции in.'''
  862. return Var(value=self, other=other, function=lambda x, y: y in x)
  863. def match(self, pattern: str) -> 'Var':
  864. '''Метод для проверки с помощью регулярки, если в строке нечто,
  865. соответствующее паттерну.'''
  866. return Var(value=self, other=pattern,
  867. function=lambda x, y: bool(re.search(y, x)))
  868. def regex(self, pattern: str, repl: str) -> 'Var':
  869. '''Метод для реализации sub'''
  870. return Var(value=self, other=(pattern, repl),
  871. function=lambda x, y: re.sub(*y, x))
  872. def replace(self, original: str, repl: str) -> 'Var':
  873. '''Метод для реализации replace.'''
  874. return Var(value=self, other=(original, repl),
  875. function=lambda x, y: x.replace(*y))
  876. def __repr__(self) -> 'Var':
  877. return ("<Condition value>")
  878. def Done(*tasks: List[str]) -> Var:
  879. '''Функция создающая объект Var, получающий информацию о том, все ли
  880. указанные методы выполнены.'''
  881. return Var(tasks=tasks,
  882. function=_is_done)
  883. def DoneAny(*tasks: List[str]) -> Var:
  884. '''Функция создающая объект Var, получающий информацию о том, есть ли
  885. среди указанных методов те, которые выполнены.'''
  886. return Var(tasks=tasks,
  887. function=_is_done_any)
  888. def Success(*tasks: List[str]) -> Var:
  889. '''Функция создающая объект Var, получающий информацию о том, все ли
  890. указанные методы выполнены и притом выполнены успешно.'''
  891. return Var(tasks=tasks,
  892. function=_is_success)
  893. def SuccessAny(*tasks: List[str]) -> Var:
  894. '''Функция создающая объект Var, получающий информацию о том, есть ли
  895. среди указанных методов те, которые выполнены успешно.'''
  896. return Var(tasks=tasks,
  897. function=_is_success_any)
  898. def Failed(*tasks: List[str]) -> Var:
  899. '''Функция создающая объект Var, получающий информацию о том, все ли
  900. указанные методы не выполнены или выполнены с ошибкой.'''
  901. return Var(tasks=tasks,
  902. function=_is_failed)
  903. def FailedAny(*tasks: List[str]) -> Var:
  904. '''Функция создающая объект Var, получающий информацию о том, есть ли
  905. среди указанных методов те, которые не выполнены или выполнены с ошибкой.
  906. '''
  907. return Var(tasks=tasks,
  908. function=_is_failed_any)
  909. def _is_done(tasks, script_namespace: NamespaceNode) -> bool:
  910. '''Функция проверяющая по содержимому пространства имен tasks все ли методы
  911. из указанных выполнены.'''
  912. for task in tasks:
  913. if task not in script_namespace:
  914. return False
  915. return True
  916. def _is_done_any(tasks, script_namespace: NamespaceNode) -> bool:
  917. '''Функция проверяющая по содержимому пространства имен tasks есть ли среди
  918. указанных методов те, которые выполнены.'''
  919. for task in tasks:
  920. if task in script_namespace:
  921. return True
  922. return False
  923. def _is_success(tasks, script_namespace: NamespaceNode) -> bool:
  924. '''Функция проверяющая по содержимому пространства имен tasks все ли методы
  925. из указанных выполнены успешно.'''
  926. for task in tasks:
  927. if (task not in script_namespace or
  928. not script_namespace[task].get_value().get_hash()['success']):
  929. return False
  930. return True
  931. def _is_success_any(tasks, script_namespace: NamespaceNode) -> bool:
  932. '''Функция проверяющая по содержимому пространства имен tasks есть ли среди
  933. указанных методов те, которые выполнены успешно.'''
  934. for task in tasks:
  935. if (task in script_namespace and
  936. script_namespace[task].get_value().get_hash()['success']):
  937. return True
  938. return False
  939. def _is_failed(tasks, script_namespace: NamespaceNode) -> bool:
  940. '''Функция проверяющая по содержимому пространства имен tasks все ли методы
  941. из указанных не выполнены или выполнены с ошибкой.'''
  942. for task in tasks:
  943. if (task in script_namespace and
  944. script_namespace[task].get_value().get_hash()['success']):
  945. return False
  946. return True
  947. def _is_failed_any(tasks, script_namespace: NamespaceNode) -> bool:
  948. '''Функция проверяющая по содержимому пространства имен tasks есть ли среди
  949. указанных методов те, которые не выполнены или выполнены с ошибкой.'''
  950. for task in tasks:
  951. if (task not in script_namespace or
  952. not script_namespace[task].get_value().get_hash()['success']):
  953. return True
  954. return False