Fixed force parameter. Added new worker ipc, some pydantic response and requests schemas and server api description.

master
Иванов Денис 3 years ago
parent 91690f73e2
commit c0a552a1cf

@ -42,11 +42,11 @@ class Command:
def __init__(self, command_id: str = '',
category: str = '',
title: str = '',
command: str = '',
script: Union[Callable, Script, None] = None,
args: Union[tuple, list] = tuple(),
parameters: Union[List[BaseParameter], None] = None,
namespace: Union[str, NamespaceNode, None] = None,
command: str = '',
gui: bool = False,
icon: Union[str, List[str]] = '',
setvars: dict = {},
@ -176,7 +176,7 @@ class CommandRunner:
def __init__(self, command: Command,
datavars: Union[Datavars, NamespaceNode],
output: IOModule):
'''Класс инкапулирующий все данные о команде, а также необходимые для
'''Класс инкапcулирующий все данные о команде, а также необходимые для
нее параметры, модуль ввода-вывода, переменные и прочее. Предназначен
для конфигурации команды и запуска ее в воркере.'''
self._command = command

@ -232,7 +232,7 @@ class Table(ParameterType):
def process_value(self, value: TableValue, parameter):
if isinstance(value, TableValue):
# Если родитель не установлен -- значит параметр был только что
# инициализирован.его, и проводим полную проверку
# инициализирован. Устанавливаем его, и проводим полную проверку
# инициализированных значений таблицы.
if value._parent is None:
value._parent = parameter
@ -270,7 +270,7 @@ class Password(ParameterType):
class Description:
'''Класс, содержащий описание параметра и способ его отображения.'''
# словарь со способами отображения параметра в графическом интерфейсе.
# Словарь со способами отображения параметра в графическом интерфейсе.
representations = {}
def __init__(self, short='', full='', usage=''):
@ -434,7 +434,7 @@ class BaseParameter:
else:
return None, None
def to_set(self, *variables):
def set_to(self, *variables):
'''Метод для добавления переменных, которым будет установлено значение
параметра.'''
self._subscribers = variables

@ -2,7 +2,7 @@
#
import re
import inspect
from typing import Callable, Any, Union, List, Generator
from typing import Callable, Any, Union, List, Generator, Optional
from calculate.templates.template_processor import DirectoryProcessor
from calculate.variables.datavars import (
DependenceAPI,
@ -46,18 +46,18 @@ class Script:
в себе. Создает экземпляры лаунчеров.'''
def __init__(self, script_id: str,
args: List[Any] = [],
success_message: Union[str, None] = None,
failed_message: Union[str, None] = None,
interrupt_message: Union[str, None] = None):
success_message: Optional[str] = None,
failed_message: Optional[str] = None,
interrupt_message: Optional[str] = None):
self._id: str = script_id
self.__items: List[Union['Task', 'Block', 'Handler', 'Run']] = None
self.__tasks_is_set: bool = False
self.__args: list = args
self.__success_message: Union[str, None] = success_message
self.__failed_message: Union[str, None] = failed_message
self.__interrupt_message: Union[str, None] = interrupt_message
self.__success_message: Optional[str] = success_message
self.__failed_message: Optional[str] = failed_message
self.__interrupt_message: Optional[str] = interrupt_message
@property
def id(self) -> str:
@ -198,7 +198,7 @@ class ScriptLauncher:
return scripts
def __call__(self, *args):
def __call__(self, *args) -> None:
'''Метод для запуска скрипта с аргументами.'''
if len(args) < len(self._script.args):
raise ScriptError(
@ -219,7 +219,7 @@ class ScriptLauncher:
def _get_args_values(self, args: List[Any],
datavars: Union[Datavars, NamespaceNode],
namespace: NamespaceNode):
namespace: NamespaceNode) -> list:
'''Метод для получения значений аргументов.'''
args_values = []
for argument in args:
@ -235,7 +235,7 @@ class ScriptLauncher:
return args_values
class RunTemplate:
class RunTemplates:
'''Класс запускателя наложения шаблонов.'''
def __init__(self, id: str = '',
action: str = '',

@ -0,0 +1,830 @@
Вопросы
-------
1. >> Что является ресурсом?
* commands -- информация о командах, их список, группы, описания и тд.
* workers(?) -- воркеры, исполняющие команды. Сомнительно.
Возможно есть смысл скрыть реализацию, т.е. воркеров, под некоторыми
другими сущностями. Например, execution и configuration.
workers -- мб оставить как дополнительный низкоуровневый интерфейс.
<< Ресурсы: commands, configuration, execution, workers (low_level)
2. >> Что делать с демонами-воркерами, обнаруженными при инциализации сервера,
но не зарегистрированными в БД?
<< Убиваем их и удаляем PID-файлы.
3. >> Что делать с демонами-воркерами, для которых найдены PID-файлы, по PID
есть информация в БД, но WID из имени PID-файла не совпадает с WID в БД.
<< Убиваем воркера, удаляем PID-файл и запись в БД.
4. >> Стоит ли убрать разделение команд и их id?
<< Не стоит.
5. >> Нужна ли возможность сброса текущих параметров конфигурации?
<< Да.
Взаимодействие с воркером с точки зрения сервера
------------------------------------------------
1. Сервер получает от клиента запрос, POST /configs/{command_id}. Далее:
* Сервер создает объект воркера команды и запускает с его помощью
демона-рабочего, оставляя его в режиме ожидания конфигурационных данных.
* После создания объекта воркера в БД сохраняется основная информация
необходимая для воссоздания объекта воркера в новом процессе сервера. Такая
ситуация может возникнуть при переходе на новую версию сервера:
- int wid -- идентификатор воркера;
- str socket -- путь к сокету для чтения данных воркера;
- int daemon_pid -- PID демона-воркера для взаимодействия с ним.
* В ответ на запрос сервер получает список доступных ресурсов, включающих
в себя в том числе действия воркера, а также URI этих ресурсов и
доступные для этого URI http-методы. На данном этапе:
- configure (patch) -- изменить некоторые параметры конфигурации;
- cancel (delete) -- закончить конфигурацию без продолжения выполнения.
По сути удаляет данную конфигурацию как ресурс;
- execute (post) -- запустить выполнение команды с дефолтной
конфигурацией. По сути удаляет конфигурацию как ресурс и создает
новый ресурс -- выполнение.
2. Сервер получает от клиента запрос PATCH /configs/{WID}/parameters,
содержащий в теле конфигурационные данные. Далее:
* Формируется конфигурационное сообщение (1a), содержащее в себе список
объектов, описывающих один или несколько параметров команды, которое
затем отправляется воркеру.
* Oжидается ответ от воркера (1a) с информацией о результатах обработки
параметров, найденных ошибках и т.д, который пересылается клиенту.
* Конфигурационный цикл повторяется.
Возможно стоит добавить на стороне сервера таймаут при получении ответа от
воркера.
3. Сервер получает от клиента запрос POST /execs/{WID}, указывающий
закончить конфигурацию команды и начать выполнение.
Далее:
* Формируется сообщение воркеру (3a), сообщающее уже ему об окончании цикла
конфигурации.
* Если есть неисправленные ошибки, воркер отвечает на него сообщением (4b),
содержащим оповещение о том, что он заканчивает работу;
Если ошибок нет, то сообщением (4a), оповещающим об успешном запуске
скрипта команды.
* В случае, когда обработка параметров прошла с ошибками -- сразу после
выдачи сообщения об ошибках конфигурации, воркер отправляет сообщение
об окончании своей работы, содержащее список ошибок (9b) и (?) ждет
подтверждение от сервера (?).
4. Сервер получает от клиента запрос GET /execs/{WID}/output на
получение имеющихся выходных данных воркера. Далее:
* Cервер читает все, что есть на сокете воркера. Сообщения (5a, 5b или 6a)
на сокете разделены нулевым символом '\0'. Собирается список полных
сообщений на сокете, формируется сообщение, содержащее этот список,
которое отправляется клиенту.
5. Сервер получает от клиента запрос PATCH /execs/{WID}/input на передачу
воркеру ввода, который был запрошен последним в одном из сообщений ранее
переданных клиенту через сервер. Далее:
* Сервер формирует сообщение (7a) воркеру с данными ввода и отправляет его
воркеру. Если данных нет, что означает, что ввод был сброшен
пользователем, воркер отправляет серверу сообщение (7b) об некорректном
вводе, а метод input объекта ввода/вывода кидает в скрипте специальное
исключение.
Возможно, необходимо убеждаться в том, что данные успешно получены воркером.
Воркер должен ответить сообщением (8a) об успешном получении данных.
- Если данное сообщение не получено в течение некоторого времени
(таймаута) -- выполняем некоторые действия по диагностике и устранению
последствий ошибки, затем отправляем клиенту сообщение, сообщающее о том,
что воркер закончил работу, и содержащее последние сообщения, полученные
от воркера, если таковые имеются.
- Если сообщение об успешном вводе в воркер получено -- отправляем
соответствующее сообщение клиенту.
6. Работа воркера закончилась в результате успешного выполнения команды,
или было прервано клиентом командой stop, или по причине ошибок, возникших
при выполнении скриптов команды. Далее:
* Воркер отправляет на сервер одно из двух специальных сообщений,
оповещающих об окончании работы:
- Содержащее также информацию об ошибках (9b), если таковые
были, неотправленных по каким-то причинам сообщениях (?) или о факте
выполнения команды stop.
- Содержащие только объявление об окончании выполнения команды, то есть
состояния run.
В зависимости от того, возможно ли чтение из сокета, когда процесс воркера
уже закрыт сервер будет или не будет отправлять сообщение разрешающее
воркеру прекратить работу, но клиент его видеть не должен. По факту
получения конечного сообщения и, возможно, данных вместе с ним, клиенту
будет присылаться специальное сообщение.
Контекст воркера
----------------
worker_state -- состояние воркера;
socket_context -- контекст чтения сокета воркера;
worker_output -- вывод воркера перед его выключением сервера.
Что делать
----------
[*] Проверить возможность чтения сервером данных из сокета уже закрытого
воркера. Если такой возможности нет, предусмотреть обработку завершения и
успешной, и некорректной работы воркера с передачей всех необходимых
сообщений. Сделать это внутри воркера.
Итог: чтение из сокета убитого демона возможно.
[ ] Добавить в воркер цикл конфигурации или его тестовый вариант;
[*] Убрать на стороне интерфейса клиента лишний шаг создания объекта воркера;
[*] Убрать self._runner_process и self._pid_file, они в таком случае не нужны;
[*] PID-файл демона удаляем сразу после чтения;
[ ] Реализовать описанный ниже протокол взаимодействия сервера с воркером;
[ ] Добавить в метод kill воркера чтение всего содержимого сокета перед его
удалением.
[ ] Разработать HAL-формат сообщений об ошибках вместо HTTPException.
[*] Сделать метод read воркера асинхронным.
[ ] На случай если воркер убит через SIGKILL -- поскольку обработку
такого завершения работы воркера в самом воркере организовать невозможно,
добавить дополнительную проверку is_alive воркера и предусмотреть обработку
этой ситуации.
[*] Добавить главный блок try-catch-finally в воркер. В раздел finally добавить
отправку сообщения об окончании работы воркера. В зависимости от
возможности читать данный из сокета законченного процесса воркера, ждать
или не ждать ответ сервера по поводу окончания работы воркера;
[*] Добавить предварительно деление воркеров по состояниям, вероятно добавить
WorkersManager;
[ ] Добавить состояния воркера:
- config -- воркер в состоянии конфигурации, Т.е. ждёт значения
параметров или собщение об окончании конфигурации;
- exec -- воркер в процессе выполнения команды;
- input -- воркер в процессе ожидания ввода пользователя;
- finish -- воркер в состоянии завершения некоторого состояния.
[ ] Реализовать предварительно REST API в соответствии с наработками;
[*] Решить, какое значение должны иметь заголовки Content-Type и Accept:
- application/json;
- application/hal+json;
- application/vnd.cphl+json;
- application/vnd.api+json.
Подумать над версионированием API;
[ ] Добавить в ответы сервера обозначения о кэшируемости и некэшируемости,
проработать кэширование в целом, определить, что должно кэшироваться, а что
нет;
[ ] Проработать механизм остановки взаимодействия сервера с клиентом и
возвращения к нему после переподключения клиента, перезагрузки сервера, а
также при этих обоих событиях;
[ ] Добавить проверку во время инициализации сервера наличия PID-файлов
демонов-воркеров:
- Если такие есть -- проверить, что это за процессы;
- Если такие процессы не существуют или не являются воркерами -- удалить
эти PID-файлы;
- Если процессы существуют, являются воркерами и есть информация о них
в БД -- проверяем соответствие PID и WID.
-- Если все совпадает -- удаляем PID-файлы и далее создаем объекты
воркеров по информации в БД;
-- [Q#3] Если есть несовпадения -- убиваем воркер, PID-файл и
записи в БД;
- [Q#2] Если процессы существуют, являются воркерами, но в БД о них нет
информации -- наверное, удаляем PID-файлы и пытаемся убить этих
воркеров с помощью их PID.
Интерфейс сервера
-----------------
REST-интерфейс (вроде). Представляем состояния воркеров в виде ресурсов или
объектов. В соответствии с HATEAOS присылаем URI на ресурсы и действия воркера,
которые доступны в тот или иной момент. Для этого применяем формат
json-сообщений напоминающий HAL или CPHL (не очень распространенный формат).
Различие от HAL в наличии атрибута "methods" в "_links".
* root -- корневой ресурс, являющийся точкой входа для всего API. Возможно,
стоит создать алиас /index или заменить на него.
- GET / -- получить доступные корневые ресурсы;
* request:
null
* response:
Media-Type: application/hal+json
Cache-Control: public
{
"data": {
// Some information about server.
},
"_links": {
"commands": {
"href": "/commands/"
},
"workers": {
"href": "/workers/"
}
}
}
{
"meta": {
// Server info
},
"links": {
}
}
* сommands -- совокупность данных о командах, доступных для запуска на
сервере.
- GET /commands/ -- получить список команд;
* request:
Media-Type: application/json
{
"gui": {true/false}
}
* response:
Media-Type: application/hal+json
Cache-Control: private
{
"{command_id}": {
"data": {
// Some command info.
},
"_links": {
"self": {
"href": "/commands/{CID}"
},
"parameters": {
"href": "/commands/{CID}/parameters"
},
"cofigure": {
"href": "/configs/{CID}"
}
}
},
...
}
- GET /commands/{command}/ -- получить информацию об указанной команде, a
также ее параметры. Запрос используемый
консольным клиентом;
* request:
null
* response:
Media-Type: application/hal+json
Cache-Control: private
{
"data": {
// Some command info.
},
"_links": {
"self": {
"href": "/commands/{CID}"
},
"parameters": {
"href": "/commands/{CID}/parameters"
}
"cofigure": {
"href": "/configuration/{CID}"
}
},
"_embedded": {
"parameters": {
"data": [
{
"group_id": "{group_id}",
"parameters": [...]
},
...
],
"_links":
"self": {
"href": "/commands/{CID}/parameters"
}
}
}
}
- GET /commands/{CID}/parameters -- получить параметры указанной команды;
* request:
null
* response:
{
"data": [
{
"group_id": "{group_id}",
"parameters": [...]
},
...
],
"_links": {
"self": {
"href": "/commands/{CID}/parameters"
}
}
}
* configs -- совокупность объектов воркеров, находящихся в состоянии
конфигурации.
- POST /configs/{command} -- создать конфигурацию;
201 -- конфигурация уже создана;
404 -- команды с указанным id нет;
400 -- неправильный формат запроса.
* request:
null
* response:
{
"_links": {
"configure": {
"href": "/configs/{WID}/parameters"
},
"execute": {
"href": "/executions/{WID}"
},
"cancel": {
"href": "/configs/{WID}"
}
}
}
- PATCH /configs/{WID}/parameters -- изменить конфигурацию
указанными в теле значениями;
404 -- конфигурации с указанным
WID нет;
400 -- неправильный формат
запроса.
* request:
[
{"id": "{param_id}", "value": "{param_value}"},
...
]
* response:
{
"data": {
"{param_id}": "{error_message}",
...
},
"_links": {
"configure": {
"href": "/configs/{WID}/parameters"
},
"cancel": {
"href": "/configs/{WID}"
}
}
}
OR
{
"_links": {
"configure": {
"href": "/configs/{WID}/parameters"
},
"execute": {
"href": "/executions/{WID}"
},
"cancel": {
"href": "/configs/{WID}"
}
}
}
- PUT /configs/{WID}/parameters -- заменить конфигурацию с
использованием указанных в теле
запроса значений. Пустой набор
значений позволяет сбросить
конфигурацию;
* request:
[
{"id": "{param_id}", "value": "{param_value}"},
...
]
* response:
{
"data": {
"{param_id}": "{error_message}",
...
},
"_links": {
"configure": {
"href": "/configs/{WID}/parameters"
},
"cancel": {
"href": "/configs/{WID}"
}
}
}
OR
{
"_links": {
"configure": {
"href": "/configs/{WID}/parameters"
},
"execute": {
"href": "/executions/{WID}"
},
"cancel": {
"href": "/configs/{WID}"
}
}
}
- DELETE /configs/{WID} -- закончить конфигурации без выполнения
команды.
* request:
null
* response:
[
{
"data": {
"type": "output",
"logging": {logging_level/null},
"text": "{message_text}",
"source": "{source_script}",
}
},
...
]
* executions -- совокупность объектов воркеров, находящихся в состоянии
исполнения.
- POST /executions/{WID} -- создать новое исполнение из конфигурации;
201 -- исполнение создано;
409 -- исполнение с этим WID уже есть;
400 -- неправильный формат запроса.
404 -- конфигурация не найдена.
* request:
null
* response:
- Response Code: 201
{
"_links": {
"output": {
"href": "/executions/{WID}/output"
},
"stop": {
"href": "/executions/{WID}"
}
}
}
- DELETE /executions/{WID} -- удалить исполнение, происходит путем
отправки в скрипт SIGINT;
* request:
null
* response (возвращает список непереданных сообщений, если они есть):
[
{
"data": {
"type": "output",
"logging": {logging_level/null},
"text": "{message_text}",
"source": "{source_script}",
}
},
...
]
- GET /executions/{WID}/output -- получить список имеющихся сообщений от
команды;
* request:
null
* response (необходимость остановить выполнение определяется по
Response Code):
[
{
"data": {
"type": "output",
"logging": {logging_level/null},
"text": "{message_text}",
"source": "{source_script}",
}
},
{
"data": {
"type": "input",
"text": "{input_message}"
"source": "{source_script}"
},
"_links": {
"input": {
"href": "/executions/{WID}/input",
}
}
}
]
- PATCH /executions/{WID}/input -- отправить в воркер некоторый ввод;
* request (если null, значит ввод прерывается):
{
"data": {input_data/null}
}
* response:
- Response Code: 200
null
- Response Code: 404
{
"error": "Execution {WID} is not found"
}
- Response Code: 400
{
"error": "Input message is not correct"
}
* workers -- вся совокупность воркеров.
- GET /workers/ -- получить список демонов-воркеров и информацию о них;
* request:
null
* response:
{
"{WID}": {
"data": {
"socket": "{socket_path}",
"command": "{worker_command}",
"pid": "{daemon_pid}",
}
"_links": {
"kill": {
"href": "/workers/{WID}",
"methods": ["delete"]
}
}
},
...
}
- DELETE /workers/{WID} -- отправить сигнал SIGKILL демону-воркеру;
* request:
null
* response (сообщения полученные от убитого воркера):
[
{
"data": {
"type": "output",
"logging": {logging_level/null},
"text": "{message_text}",
"source": "{source_script}",
}
},
...
]
Протокол взаимодействия воркера и сервера
-----------------------------------------
Все сообщения представляют собой json-структуры, включающие в себя 3 поля:
- state -- состояние воркера:
* Если сообщение пришло воркеру от сервера, тогда это поле указывает
воркеру, какое состояние он должен принять;
* Если сообщение пришло серверу от воркера, тогда это поле указывает на
текущее состояние воркера.
- status -- статус текущего состояния:
0 -- состояние корректно;
1 -- состояние некорректно.
- data -- некоторые данные, структура которых может быть любой.
1. Данные конфигурации от клиента.
a. Продолжить конфигурацию указанными значениями:
{
"state": "config",
"status": 0,
"data": [
{"id": "{param_id}", "value": "{parameter_value}"},
...
]
}
b. Сбросить конфигурацию и продолжить указанными значениями:
{
"state": "config",
"status": 1,
"data": [
{"id": "{param_id}", "value": "{parameter_value}"},
...
]
}
2. Ответ воркера по данным конфигурации.
a. Все параметры конфигурации корректны:
{
"state": "config",
"status": 0,
"data": null
}
b. Некоторые параметры конфигурации некорректны:
{
"state": "config",
"status": 1,
"data": {
"{param_id}": "{error_message}",
...
}
}
3. Запрос на окончание конфигурации от клиента.
a. Закончить конфигурацию, начать выполнение команды:
{
"state": "finish",
"status": 0,
"data": null
}
b. Закончить конфигурацию и работу воркера:
{
"state": "finish",
"status": 1,
"data": null
}
4. Ответ воркера на запрос об окончании конфигурации:
a. Закончить конфигурацию, начать выполнение команды:
{
"state": "finish",
"status": 0,
"data": null
}
b. Закончить конфигурацию и работу воркера:
{
"state": "finish",
"status": 1,
"data": {
"{param_id}": "{error_message}",
...
}
}
5. Сообщение с выводом воркера.
a. Сообщение из скрипта при его "штатной" работe:
{
"state": "exec",
"status": 0,
"data": {
"type": "message",
"logging": {logging_level/null},
"text": "{message_text}",
"source": "{source_script}"
}
}
b. Сообщение об ошибке приводящей к завершению выполнения скрипта и
команды:
{
"state": "exec",
"status": 1,
"data": {
"error": "{error_message}",
"source": "{source_script}"
}
}
6. Сообщение с запросом на ввод.
a. Состояние "input" -- воркер в состоянии ожидания ввода:
{
"state": "input",
"status": 0,
"data": {
"text": "{message_text}",
"source": "{source_script}"
}
}
7. Сообщение от клиента с данными.
a. Ввод успешно выполнен и передается:
{
"state": "input",
"status": 0,
"data": "{client_data}"
}
b. Ввод данных прерван, данных не будет:
{
"state": "input",
"status": 1,
"data": null
}
8. Сообщение от воркера о получении ввода.
a. Ввод успешно получен, о чем говорит переход в состояние выполнения со
статусом 0, т.е. без ошибок:
{
"state": "exec",
"status": 0,
"data": null
}
9. Сообщение воркера об окончании работы:
a. Успешное окончание работы:
{
"state": "finish",
"status": 0,
"data": null
}
b. Работа воркера была прервана или закончилась с ошибками:
{
"state": "finish",
"status": 1,
"data": [
"{error_message}",
...
]
}

@ -0,0 +1,165 @@
#! env/bin/python3
import sys
import asyncio
import aiohttp
import logging
from typing import List, Tuple, Any
loop = asyncio.get_event_loop()
SERVER_DOMAIN = "http://127.0.0.1:2007"
LOG_LEVELS = {logging.ERROR: "ERROR",
logging.INFO: "INFO",
logging.WARNING: "WARNING",
logging.DEBUG: "DEBUG",
logging.CRITICAL: "CRITICAL",
}
async def check_server(client: aiohttp.ClientSession):
try:
async with client.get(f"{SERVER_DOMAIN}/") as response:
assert response.status == 200
data = await response.json()
assert data == {"status": "active"}
print(f"Connected to the Calculate Server on {SERVER_DOMAIN}")
return True
except Exception:
print("Can not connect to the calculate server.")
return False
async def get_commands(client: aiohttp.ClientSession):
async with client.get(f"{SERVER_DOMAIN}/commands") as response:
assert response.status == 200
data = await response.json()
return data
async def create_worker(client: aiohttp.ClientSession, command: str,
arguments: List[str]):
worker_args = {"arguments": arguments}
async with client.post(f"{SERVER_DOMAIN}/workers/{command}",
json=worker_args) as response:
assert response.status == 200
data = await response.json()
return data["wid"]
async def start_worker(client: aiohttp.ClientSession, wid: int):
async with client.post(f"{SERVER_DOMAIN}/workers/{wid}/start") as response:
assert response.status == 200
data = await response.json()
return data
async def get_worker_messages(client: aiohttp.ClientSession, wid: int):
async with client.get(f"{SERVER_DOMAIN}/workers/{wid}/messages"
) as response:
assert response.status == 200
data = await response.json()
return data["data"]
async def send_data_to_worker(client: aiohttp.ClientSession, wid: int,
data: Any):
first_try = True
while True:
answer = {"data": data}
try:
async with client.post(f"{SERVER_DOMAIN}/workers/{wid}/send",
json=answer) as response:
assert response.status == 200
data = await response.json()
return
except aiohttp.client_exceptions.ClientOSError:
if first_try:
first_try = False
continue
else:
raise
# return data
async def finish_worker(client: aiohttp.ClientSession, wid: int):
async with client.post(f"{SERVER_DOMAIN}/workers/{wid}/finish"
) as response:
assert response.status == 200
data = await response.json()
return data
async def main():
print("Calculate Console Client 0.0.1")
async with aiohttp.ClientSession(
connector=aiohttp.TCPConnector(force_close=False)) as client:
if not await check_server(client):
return
commands = await get_commands(client)
command, command_args = get_console_command()
message = check_command(command, commands)
if message:
print(message)
return
wid = await create_worker(client, command, command_args)
try:
print("CREATED WORKER ID:", wid)
status_data = await start_worker(client, wid)
if status_data.get("status", None) == "error":
print("ERROR:", status_data.get("description", "NONE"))
return
finished = False
while not finished:
messages = await get_worker_messages(client, wid)
for message in messages:
if message["type"] == "output":
print(f"{LOG_LEVELS[message['level']]}:"
f" {message['msg']}")
elif message["type"] == "input":
print(message["msg"])
input_data = input(">> ")
await send_data_to_worker(client, wid, input_data)
elif (message["type"] == "control"
and message["action"] == "finish"):
finished = True
finally:
await finish_worker(client, wid)
def get_console_command() -> Tuple[str, List[str]]:
offset = 1 if sys.argv[0].endswith("client.py") else 0
if len(sys.argv) < 1 + offset:
command = None
command_args = []
else:
command = sys.argv[offset]
command_args = sys.argv[1 + offset:]
return command, command_args
def check_command(command: str, available_commands: List[str]) -> str:
if command is None:
return "Command is not set."
if command not in available_commands:
return "Command is not available."
return ''
if __name__ == "__main__":
try:
loop.run_until_complete(main())
except KeyboardInterrupt:
print("\r<< Keyboard interrupt.")
loop.close()

@ -0,0 +1,183 @@
#! env/bin/python3
import sys
import requests
import logging
from typing import Tuple, List, Union, Dict
from pprint import pprint
LOG_LEVELS = {logging.ERROR: "ERROR",
logging.INFO: "INFO",
logging.WARNING: "WARNING",
logging.DEBUG: "DEBUG",
logging.CRITICAL: "CRITICAL",
}
def main():
# Обращаемся к корню сервера для получения списка доступных ресурсов.
data, links = get_root_data("http://127.0.0.1:2007")
if data is None:
return
print(f"Connected to {data['name']}")
print("\nRoot links:")
pprint(links)
# Получаем данные о текущей команде.
cl_command, cl_parameters = get_console_command()
print_console_parameters(cl_parameters)
data, links, parameters = find_command(cl_command, links)
if data is None:
return
print_command_info(data)
print_parameters(parameters)
links = start_command_configuration(links)
print("\nConfig links:")
pprint(links)
print("\nSetting Parameters")
print("\nLinks:")
data, links = set_parameters(links, cl_parameters)
pprint(links)
if data is not None:
print_config_errors(data)
print("\nCancel configuration:")
result = cancel_configuration(links)
pprint(result)
def get_root_data(base_url: str) -> Tuple[Union[dict, None],
Union[dict, None]]:
try:
response = requests.get(f"{base_url}/")
if response.status_code == 200:
data = response.json()["data"]
links = response.json()["_links"]
return data, links
else:
detail = response.json()['detail']
print(f"{response.status_code}: {detail}")
except Exception as error:
print("ERROR:", str(error))
return None, None
def find_command(console_command: str, links: dict) -> dict:
find_uri = links["find_command"]["href"].format(
console_command=console_command,
is_gui=0)
response = requests.get(find_uri)
if response.status_code == 200:
json = response.json()
data = json["data"]
links = json["_links"]
parameters = json["_embedded"]["parameters"]
return data, links, parameters
else:
detail = response.json()['detail']
print(f"{response.status_code}: {detail}")
return None, None, None
def get_parameters(links: dict) -> List[dict]:
params_uri = links["parameters"]["href"]
response = requests.get(params_uri)
if response.status_code == 200:
return response.json()
else:
print(f"Response Code: {response.status_code}, {response.text}")
return None
def start_command_configuration(links: dict) -> Union[dict, None]:
configure_uri = links["configure"]["href"]
response = requests.post(configure_uri)
if response.status_code == 200:
json = response.json()
return json["_links"]
else:
print(f"Response Code: {response.status_code}, {response.text}")
return None
def set_parameters(links: dict, parameters: List[dict]) -> Dict[str, dict]:
configure_uri = links["configure"]["href"]
response = requests.patch(configure_uri, json=parameters)
if response.status_code == 200:
json = response.json()
data = json.get("data", None)
links = json["_links"]
return data, links
else:
print(f"Response Code: {response.status_code}, {response.text}")
return None, None
def cancel_configuration(links: dict) -> List[dict]:
configure_uri = links["cancel"]["href"]
response = requests.delete(configure_uri)
if response.status_code == 200:
return response.json()
else:
print(f"Response Code: {response.status_code}, {response.text}")
return None
def get_console_command() -> Tuple[str, List[str]]:
offset = 1 if sys.argv[0].endswith("client.py") else 0
command_args = []
if len(sys.argv) < 1 + offset:
command = None
else:
command = sys.argv[offset].split("/")[-1]
args = sys.argv[1 + offset:]
if len(args) >= 2 and not (len(args) % 2):
for index in range(0, len(args) - len(args) % 2, 2):
id, value = args[index: index + 2]
command_args.append({"id": id.strip("-"),
"value": value})
return command, command_args
def print_console_parameters(parameters: dict):
print("\nConsole parameters:")
for parameter in parameters:
print(f" {parameter['id']} = {parameter['value']}")
def print_command_info(data: dict) -> None:
print(f"\nCurrent command: {data['id']}\n"
f" category: {data['category']}\n"
f" title: {data['title']}")
def print_parameters(parameters_data: list) -> None:
parameters = parameters_data["data"]
print("\nParameters:")
for group in parameters:
print(f" {group['group_id']}")
group_parameters = group["parameters"]
for parameter in group_parameters:
print(f" parameter:\t{parameter['id']}\n"
f" default:\t{parameter['default']}\n")
def print_config_errors(errors: Dict[str, str]) -> None:
print("\nConfiguration errors:")
for parameter_id, error in errors.items():
print(f" {parameter_id}: {error}")
if __name__ == "__main__":
main()

@ -0,0 +1,77 @@
from typing import List, Callable, Dict
from scripts import script_0, script_1, script_2
from collections import OrderedDict
class Command:
'''Тестовый класс команд'''
def __init__(self,
command_id: str,
command: str,
category: str,
title: str,
scripts: List[Callable],
parameters: Dict[str, dict]):
self.id = command_id
self.command = command
self.category = category
self.command = command
self.title = title
self.scripts = scripts
self.parameters = parameters
command_0 = Command(command_id="command_0",
category="Test",
title="Test command number 0",
command="cl-command-0",
scripts=OrderedDict({"script_0": script_0,
"script_1": script_1}),
parameters=[{"group_id": "group_0",
"parameters": [{"id": "value_0",
"default": 253},
{"id": "value_1",
"default": 413}]
}
]
)
command_1 = Command(command_id="command_1",
category="Test",
title="Test command number 1",
command="cl-command-1",
scripts=OrderedDict({"script_0": script_0,
"script_1": script_1,
"script_2": script_2}),
parameters=[{"group_id": "group_1",
"parameters": [{"id": "value_0",
"default": 35},
{"id": "value_1",
"default": 41}]
},
{"group_id": "group_2",
"parameters": [{"id": "value_2",
"default": 11}]
}
]
)
command_2 = Command(command_id="command_2",
category="Test",
title="Test command number 2",
command="cl-command-2",
scripts=OrderedDict({"script_0": script_0,
"script_2": script_2}),
parameters=[{"group_id": "group_0",
"parameters": [{"id": "value_0",
"default": 253}]
},
{"group_id": "group_1",
"parameters": [{"id": "value_2",
"default": 413},
{"id": "value_5",
"default": 23}]
}]
)

@ -0,0 +1,121 @@
# import time
from typing import Union, List, Optional, Dict
from multiprocessing.connection import Listener
from commands import Command
from io_module import IOModule
def daemon(listener: Listener, wid: int, base_dir: str, command: Command):
"""Daemon main function."""
with IOModule(listener, command.id) as io:
io.script = "worker"
try:
# int("lolkek")
parameters = configure(io,
get_default_parameters(command.parameters))
except KeyboardInterrupt:
# Остановлено пользователем.
send_finish(io)
return
except Exception as error:
# Остановлено из-за ошибки.
send_finish(io, errors=[str(error)])
return
try:
for name, script in command.scripts.items():
script_func = script[0]
parameters = configure(io, dict())
io.set_info(f"Running script '{name}'")
io.script = name
script_func(io, parameters)
io.script = "worker"
for num in range(5):
io.set_error(f"test_error_{num}")
raise Exception("critical error LOL")
io.set_error("there is no life after CRITICAL error")
except Exception as error:
io.set_error(str(error))
finally:
io.set_info("Command is done.")
io.send({"type": "finish", "msg": "finishing."})
for num in range(5):
io.set_info(f"After finish message_{num}.")
def configure(io: IOModule, parameters: dict) -> Union[None, dict]:
# Копируем чтобы уже для тестов реализовать возможность сброса.
processing_parameters = parameters.copy()
while True:
errors = []
server_message = io.receive()
state = server_message["state"]
status = server_message["status"]
if state == "config":
if status == 1:
# Сброс параметров.
processing_parameters = parameters.copy()
values: list = server_message.get("data", None)
if values is not None:
# Модифицируем параметры.
errors = modify_parameters(processing_parameters, values)
elif state == "finish":
if status == 0:
# Заканчиваем конфигурацию и возвращаем полученные параметры
# если статус 1.
return parameters
# Останавливаем конфигурацию и работу воркера если статус 1.
raise KeyboardInterrupt()
if errors:
send_config_status(io, errors=errors)
else:
send_config_status(io)
def modify_parameters(current: dict, new: list) -> None:
errors = {}
for parameter in new:
parameter_id = parameter["id"]
parameter_value = parameter["value"]
if parameter_id in current:
try:
parameter_value = int(parameter_value)
current[parameter_id] = parameter_value
except ValueError as error:
errors[parameter_id] = str(error)
else:
errors[parameter_id] = f'Parameter "{parameter_id}" is not found.'
return errors
def send_finish(io: IOModule, errors: Optional[List[str]] = []) -> None:
io.send({"state": "finish",
"status": int(bool(errors)),
"data": errors})
def send_config_status(io: IOModule,
errors: Optional[Dict[str, str]] = {}) -> None:
io.send({"state": "config",
"status": int(bool(errors)),
"data": errors})
def get_default_parameters(parameters_description):
parameters = {}
for group in parameters_description:
for parameter in group["parameters"]:
parameters[parameter["id"]] = parameter["default"]
return parameters

@ -0,0 +1,143 @@
# Copyright 2017 Dan Krause
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import socketserver
import socket
import struct
import json
class IPCError(Exception):
pass
class UnknownMessageClass(IPCError):
pass
class InvalidSerialization(IPCError):
pass
class ConnectionClosed(IPCError):
pass
def _read_objects(sock):
header = sock.recv(4)
if len(header) == 0:
raise ConnectionClosed()
size = struct.unpack('!i', header)[0]
data = sock.recv(size - 4)
if len(data) == 0:
raise ConnectionClosed()
return Message.deserialize(json.loads(data))
def _write_objects(sock, objects):
data = json.dumps([o.serialize() for o in objects])
sock.sendall(struct.pack('!i', len(data) + 4))
sock.sendall(data)
def _recursive_subclasses(cls):
classmap = {}
for subcls in cls.__subclasses__():
classmap[subcls.__name__] = subcls
classmap.update(_recursive_subclasses(subcls))
return classmap
class Message(object):
@classmethod
def deserialize(cls, objects):
classmap = _recursive_subclasses(cls)
serialized = []
for obj in objects:
if isinstance(obj, Message):
serialized.append(obj)
else:
try:
serialized.append(classmap[obj['class']](*obj['args'],
**obj['kwargs']))
except KeyError as e:
raise UnknownMessageClass(e)
except TypeError as e:
raise InvalidSerialization(e)
return serialized
def serialize(self):
args, kwargs = self._get_args()
return {'class': type(self).__name__, 'args': args, 'kwargs': kwargs}
def _get_args(self):
return [], {}
def __repr__(self):
r = self.serialize()
args = ', '.join([repr(arg) for arg in r['args']])
kwargs = ''.join([', {}={}'.format(k, repr(v))
for k, v in r['kwargs'].items()])
name = r['class']
return '{}({}{})'.format(name, args, kwargs)
class Client(object):
def __init__(self, server_address):
self.addr = server_address
if isinstance(self.addr, str):
address_family = socket.AF_UNIX
else:
address_family = socket.AF_INET
self.sock = socket.socket(address_family, socket.SOCK_STREAM)
def connect(self):
self.sock.connect(self.addr)
def close(self):
self.sock.close()
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def send(self, objects):
_write_objects(self.sock, objects)
return _read_objects(self.sock)
class Server(socketserver.ThreadingUnixStreamServer):
def __init__(self, server_address, callback, bind_and_activate=True):
if not callable(callback):
callback = lambda x: []
class IPCHandler(socketserver.BaseRequestHandler):
def handle(self):
while True:
try:
results = _read_objects(self.request)
except ConnectionClosed:
return
_write_objects(self.request, callback(results))
if isinstance(server_address, str):
self.address_family = socket.AF_UNIX
else:
self.address_family = socket.AF_INET
socketserver.TCPServer.__init__(self, server_address, IPCHandler,
bind_and_activate)

@ -0,0 +1,830 @@
Вопросы
-------
1. >> Что является ресурсом?
* commands -- информация о командах, их список, группы, описания и тд.
* workers(?) -- воркеры, исполняющие команды. Сомнительно.
Возможно есть смысл скрыть реализацию, т.е. воркеров, под некоторыми
другими сущностями. Например, execution и configuration.
workers -- мб оставить как дополнительный низкоуровневый интерфейс.
<< Ресурсы: commands, configuration, execution, workers (low_level)
2. >> Что делать с демонами-воркерами, обнаруженными при инциализации сервера,
но не зарегистрированными в БД?
<< Убиваем их и удаляем PID-файлы.
3. >> Что делать с демонами-воркерами, для которых найдены PID-файлы, по PID
есть информация в БД, но WID из имени PID-файла не совпадает с WID в БД.
<< Убиваем воркера, удаляем PID-файл и запись в БД.
4. >> Стоит ли убрать разделение команд и их id?
<< Не стоит.
5. >> Нужна ли возможность сброса текущих параметров конфигурации?
<< Да.
Взаимодействие с воркером с точки зрения сервера
------------------------------------------------
1. Сервер получает от клиента запрос, POST /configuration/{command_id}. Далее:
* Сервер создает объект воркера команды и запускает с его помощью
демона-рабочего, оставляя его в режиме ожидания конфигурационных данных.
* После создания объекта воркера в БД сохраняется основная информация
необходимая для воссоздания объекта воркера в новом процессе сервера. Такая
ситуация может возникнуть при переходе на новую версию сервера:
- int wid -- идентификатор воркера;
- str socket -- путь к сокету для чтения данных воркера;
- int daemon_pid -- PID демона-воркера для взаимодействия с ним.
* В ответ на запрос сервер получает список доступных ресурсов, включающих
в себя в том числе действия воркера, а также URI этих ресурсов и
доступные для этого URI http-методы. На данном этапе:
- configure (patch) -- изменить некоторые параметры конфигурации;
- cancel (delete) -- закончить конфигурацию без продолжения выполнения.
По сути удаляет данную конфигурацию как ресурс;
- execute (post) -- запустить выполнение команды с дефолтной
конфигурацией. По сути удаляет конфигурацию как ресурс и создает
новый ресурс -- выполнение.
2. Сервер получает от клиента запрос PATCH /configuration/{WID}/parameters,
содержащий в теле конфигурационные данные. Далее:
* Формируется конфигурационное сообщение (1a), содержащее в себе список
объектов, описывающих один или несколько параметров команды, которое
затем отправляется воркеру.
* Oжидается ответ от воркера (1a) с информацией о результатах обработки
параметров, найденных ошибках и т.д, который пересылается клиенту.
* Конфигурационный цикл повторяется.
Возможно стоит добавить на стороне сервера таймаут при получении ответа от
воркера.
3. Сервер получает от клиента запрос POST /executions/{WID}, указывающий
закончить конфигурацию команды и начать выполнение.
Далее:
* Формируется сообщение воркеру (3a), сообщающее уже ему об окончании цикла
конфигурации.
* Если есть неисправленные ошибки, воркер отвечает на него сообщением (4b),
содержащим оповещение о том, что он заканчивает работу;
Если ошибок нет, то сообщением (4a), оповещающим об успешном запуске
скрипта команды.
* В случае, когда обработка параметров прошла с ошибками -- сразу после
выдачи сообщения об ошибках конфигурации, воркер отправляет сообщение
об окончании своей работы, содержащее список ошибок (9b) и (?) ждет
подтверждение от сервера (?).
4. Сервер получает от клиента запрос GET /executions/{WID}/output на
получение имеющихся выходных данных воркера. Далее:
* Cервер читает все, что есть на сокете воркера. Сообщения (5a, 5b или 6a)
на сокете разделены нулевым символом '\0'. Собирается список полных
сообщений на сокете, формируется сообщение, содержащее этот список,
которое отправляется клиенту.
5. Сервер получает от клиента запрос PATCH /executions/{WID}/input на передачу
воркеру ввода, который был запрошен последним в одном из сообщений ранее
переданных клиенту через сервер. Далее:
* Сервер формирует сообщение (7a) воркеру с данными ввода и отправляет его
воркеру. Если данных нет, что означает, что ввод был сброшен
пользователем, воркер отправляет серверу сообщение (7b) об некорректном
вводе, а метод input объекта ввода/вывода кидает в скрипте специальное
исключение.
Возможно, необходимо убеждаться в том, что данные успешно получены воркером.
Воркер должен ответить сообщением (8a) об успешном получении данных.
- Если данное сообщение не получено в течение некоторого времени
(таймаута) -- выполняем некоторые действия по диагностике и устранению
последствий ошибки, затем отправляем клиенту сообщение, сообщающее о том,
что воркер закончил работу, и содержащее последние сообщения, полученные
от воркера, если таковые имеются.
- Если сообщение об успешном вводе в воркер получено -- отправляем
соответствующее сообщение клиенту.
6. Работа воркера закончилась в результате успешного выполнения команды,
или было прервано клиентом командой stop, или по причине ошибок, возникших
при выполнении скриптов команды. Далее:
* Воркер отправляет на сервер одно из двух специальных сообщений,
оповещающих об окончании работы:
- Содержащее также информацию об ошибках (9b), если таковые
были, неотправленных по каким-то причинам сообщениях (?) или о факте
выполнения команды stop.
- Содержащие только объявление об окончании выполнения команды, то есть
состояния run.
В зависимости от того, возможно ли чтение из сокета, когда процесс воркера
уже закрыт сервер будет или не будет отправлять сообщение разрешающее
воркеру прекратить работу, но клиент его видеть не должен. По факту
получения конечного сообщения и, возможно, данных вместе с ним, клиенту
будет присылаться специальное сообщение.
Контекст воркера
----------------
worker_state -- состояние воркера;
socket_context -- контекст чтения сокета воркера;
worker_output -- вывод воркера перед его выключением сервера.
Что делать
----------
[*] Проверить возможность чтения сервером данных из сокета уже закрытого
воркера. Если такой возможности нет, предусмотреть обработку завершения и
успешной, и некорректной работы воркера с передачей всех необходимых
сообщений. Сделать это внутри воркера.
Итог: чтение из сокета убитого демона возможно.
[ ] Добавить в воркер цикл конфигурации или его тестовый вариант;
[*] Убрать на стороне интерфейса клиента лишний шаг создания объекта воркера;
[*] Убрать self._runner_process и self._pid_file, они в таком случае не нужны;
[*] PID-файл демона удаляем сразу после чтения;
[ ] Реализовать описанный ниже протокол взаимодействия сервера с воркером;
[ ] Добавить в метод kill воркера чтение всего содержимого сокета перед его
удалением.
[ ] Разработать HAL-формат сообщений об ошибках вместо HTTPException.
[*] Сделать метод read воркера асинхронным.
[ ] На случай если воркер убит через SIGKILL -- поскольку обработку
такого завершения работы воркера в самом воркере организовать невозможно,
добавить дополнительную проверку is_alive воркера и предусмотреть обработку
этой ситуации.
[ ] Добавить главный блок try-catch-finally в воркер. В раздел finally добавить
отправку сообщения об окончании работы воркера. В зависимости от
возможности читать данный из сокета законченного процесса воркера, ждать
или не ждать ответ сервера по поводу окончания работы воркера;
[ ] Добавить предварительно деление воркеров по состояниям, вероятно добавить
WorkersPool;
[ ] Добавить состояния воркера:
- config -- воркер в состоянии конфигурации, Т.е. ждёт значения
параметров или собщение об окончании конфигурации;
- exec -- воркер в процессе выполнения команды;
- input -- воркер в процессе ожидания ввода пользователя;
- finish -- воркер в состоянии завершения некоторого состояния.
[ ] Реализовать предварительно REST API в соответствии с наработками;
[ ] Решить, какое значение должны иметь заголовки Content-Type и Accept:
- application/json;
- application/hal+json;
- application/vnd.cphl+json;
- application/vnd.api+json.
Подумать над версионированием API;
[ ] Добавить в ответы сервера обозначения о кэшируемости и некэшируемости,
проработать кэширование в целом, определить, что должно кэшироваться, а что
нет;
[ ] Проработать механизм остановки взаимодействия сервера с клиентом и
возвращения к нему после переподключения клиента, перезагрузки сервера, а
также при этих обоих событиях;
[ ] Добавить проверку во время инициализации сервера наличия PID-файлов
демонов-воркеров:
- Если такие есть -- проверить, что это за процессы;
- Если такие процессы не существуют или не являются воркерами -- удалить
эти PID-файлы;
- Если процессы существуют, являются воркерами и есть информация о них
в БД -- проверяем соответствие PID и WID.
-- Если все совпадает -- удаляем PID-файлы и далее создаем объекты
воркеров по информации в БД;
-- [Q#3] Если есть несовпадения -- убиваем воркер, PID-файл и
записи в БД;
- [Q#2] Если процессы существуют, являются воркерами, но в БД о них нет
информации -- наверное, удаляем PID-файлы и пытаемся убить этих
воркеров с помощью их PID.
Интерфейс сервера
-----------------
REST-интерфейс (вроде). Представляем состояния воркеров в виде ресурсов или
объектов. В соответствии с HATEAOS присылаем URI на ресурсы и действия воркера,
которые доступны в тот или иной момент. Для этого применяем формат
json-сообщений напоминающий HAL или CPHL (не очень распространенный формат).
Различие от HAL в наличии атрибута "methods" в "_links".
* root -- корневой ресурс, являющийся точкой входа для всего API. Возможно,
стоит создать алиас /index или заменить на него.
- GET / -- получить доступные корневые ресурсы;
* request:
null
* response:
Media-Type: application/hal+json
Cache-Control: public
{
"data": {
// Some information about server.
},
"_links": {
"commands": {
"href": "/commands/"
},
"workers": {
"href": "/workers/"
}
}
}
{
"meta": {
// Server info
},
"links": {
}
}
* сommands -- совокупность данных о командах, доступных для запуска на
сервере.
- GET /commands/ -- получить список команд;
* request:
Media-Type: application/json
{
"gui": {true/false}
}
* response:
Media-Type: application/hal+json
Cache-Control: private
{
"{command_id}": {
"data": {
// Some command info.
},
"_links": {
"self": {
"href": "/commands/{CID}"
},
"parameters": {
"href": "/commands/{CID}/parameters"
},
"cofigure": {
"href": "/configs/{CID}"
}
}
},
...
}
- GET /commands/{command}/ -- получить информацию об указанной команде, a
также ее параметры. Запрос используемый
консольным клиентом;
* request:
null
* response:
Media-Type: application/hal+json
Cache-Control: private
{
"data": {
// Some command info.
},
"_links": {
"self": {
"href": "/commands/{CID}"
},
"parameters": {
"href": "/commands/{CID}/parameters"
}
"cofigure": {
"href": "/configuration/{CID}"
}
},
"_embedded": {
"parameters": {
"data": [
{
"group_id": "{group_id}",
"parameters": [...]
},
...
],
"_links":
"self": {
"href": "/commands/{CID}/parameters"
}
}
}
}
- GET /commands/{CID}/parameters -- получить параметры указанной команды;
* request:
null
* response:
{
"data": [
{
"group_id": "{group_id}",
"parameters": [...]
},
...
],
"_links": {
"self": {
"href": "/commands/{CID}/parameters"
}
}
}
* configs -- совокупность объектов воркеров, находящихся в состоянии
конфигурации.
- POST /configs/{command} -- создать конфигурацию;
201 -- конфигурация уже создана;
404 -- команды с указанным id нет;
400 -- неправильный формат запроса.
* request:
null
* response:
{
"_links": {
"configure": {
"href": "/configs/{WID}/parameters"
},
"execute": {
"href": "/executions/{WID}"
},
"cancel": {
"href": "/configs/{WID}"
}
}
}
- PATCH /configs/{WID}/parameters -- изменить конфигурацию
указанными в теле значениями;
404 -- конфигурации с указанным
WID нет;
400 -- неправильный формат
запроса.
* request:
[
{"id": "{param_id}", "value": "{param_value}"},
...
]
* response:
{
"data": {
"{param_id}": "{error_message}",
...
},
"_links": {
"configure": {
"href": "/configs/{WID}/parameters"
},
"cancel": {
"href": "/configs/{WID}"
}
}
}
OR
{
"_links": {
"configure": {
"href": "/configs/{WID}/parameters"
},
"execute": {
"href": "/executions/{WID}"
},
"cancel": {
"href": "/configs/{WID}"
}
}
}
- PUT /configs/{WID}/parameters -- заменить конфигурацию с
использованием указанных в теле
запроса значений. Пустой набор
значений позволяет сбросить
конфигурацию;
* request:
[
{"id": "{param_id}", "value": "{param_value}"},
...
]
* response:
{
"data": {
"{param_id}": "{error_message}",
...
},
"_links": {
"configure": {
"href": "/configs/{WID}/parameters"
},
"cancel": {
"href": "/configs/{WID}"
}
}
}
OR
{
"_links": {
"configure": {
"href": "/configs/{WID}/parameters"
},
"execute": {
"href": "/executions/{WID}"
},
"cancel": {
"href": "/configs/{WID}"
}
}
}
- DELETE /configs/{WID} -- закончить конфигурации без выполнения
команды.
* request:
null
* response:
[
{
"data": {
"type": "output",
"logging": {logging_level/null},
"text": "{message_text}",
"source": "{source_script}",
}
},
...
]
* executions -- совокупность объектов воркеров, находящихся в состоянии
исполнения.
- POST /executions/{WID} -- создать новое исполнение из конфигурации;
201 -- исполнение создано;
409 -- исполнение с этим WID уже есть;
400 -- неправильный формат запроса.
404 -- конфигурация не найдена.
* request:
null
* response:
- Response Code: 201
{
"_links": {
"output": {
"href": "/executions/{WID}/output"
},
"stop": {
"href": "/executions/{WID}"
}
}
}
- DELETE /executions/{WID} -- удалить исполнение, происходит путем
отправки в скрипт SIGINT;
* request:
null
* response (возвращает список непереданных сообщений, если они есть):
[
{
"data": {
"type": "output",
"logging": {logging_level/null},
"text": "{message_text}",
"source": "{source_script}",
}
},
...
]
- GET /executions/{WID}/output -- получить список имеющихся сообщений от
команды;
* request:
null
* response (необходимость остановить выполнение определяется по
Response Code):
[
{
"data": {
"type": "output",
"logging": {logging_level/null},
"text": "{message_text}",
"source": "{source_script}",
}
},
{
"data": {
"type": "input",
"text": "{input_message}"
"source": "{source_script}"
},
"_links": {
"input": {
"href": "/executions/{WID}/input",
}
}
}
]
- PATCH /executions/{WID}/input -- отправить в воркер некоторый ввод;
* request (если null, значит ввод прерывается):
{
"data": {input_data/null}
}
* response:
- Response Code: 200
null
- Response Code: 404
{
"error": "Execution {WID} is not found"
}
- Response Code: 400
{
"error": "Input message is not correct"
}
* workers -- вся совокупность воркеров.
- GET /workers/ -- получить список демонов-воркеров и информацию о них;
* request:
null
* response:
{
"{WID}": {
"data": {
"socket": "{socket_path}",
"command": "{worker_command}",
"pid": "{daemon_pid}",
}
"_links": {
"kill": {
"href": "/workers/{WID}",
"methods": ["delete"]
}
}
},
...
}
- DELETE /workers/{WID} -- отправить сигнал SIGKILL демону-воркеру;
* request:
null
* response (сообщения полученные от убитого воркера):
[
{
"data": {
"type": "output",
"logging": {logging_level/null},
"text": "{message_text}",
"source": "{source_script}",
}
},
...
]
Протокол взаимодействия воркера и сервера
-----------------------------------------
Все сообщения представляют собой json-структуры, включающие в себя 3 поля:
- state -- состояние воркера:
* Если сообщение пришло воркеру от сервера, тогда это поле указывает
воркеру, какое состояние он должен принять;
* Если сообщение пришло серверу от воркера, тогда это поле указывает на
текущее состояние воркера.
- status -- статус текущего состояния:
0 -- состояние корректно;
1 -- состояние некорректно.
- data -- некоторые данные, структура которых может быть любой.
1. Данные конфигурации от клиента.
a. Продолжить конфигурацию указанными значениями:
{
"state": "config",
"status": 0,
"data": [
{"id": "{param_id}", "value": "{parameter_value}"},
...
]
}
b. Сбросить конфигурацию и продолжить указанными значениями:
{
"state": "config",
"status": 1,
"data": [
{"id": "{param_id}", "value": "{parameter_value}"},
...
]
}
2. Ответ воркера по данным конфигурации.
a. Все параметры конфигурации корректны:
{
"state": "config",
"status": 0,
"data": null
}
b. Некоторые параметры конфигурации некорректны:
{
"state": "config",
"status": 1,
"data": {
"{param_id}": "{error_message}",
...
}
}
3. Запрос на окончание конфигурации от клиента.
a. Закончить конфигурацию, начать выполнение команды:
{
"state": "finish",
"status": 0,
"data": null
}
b. Закончить конфигурацию и работу воркера:
{
"state": "finish",
"status": 1,
"data": null
}
4. Ответ воркера на запрос об окончании конфигурации:
a. Закончить конфигурацию, начать выполнение команды:
{
"state": "finish",
"status": 0,
"data": null
}
b. Закончить конфигурацию и работу воркера:
{
"state": "finish",
"status": 1,
"data": {
"{param_id}": "{error_message}",
...
}
}
5. Сообщение с выводом воркера.
a. Сообщение из скрипта при его "штатной" работe:
{
"state": "exec",
"status": 0,
"data": {
"type": "message",
"logging": {logging_level/null},
"text": "{message_text}",
"source": "{source_script}"
}
}
b. Сообщение об ошибке приводящей к завершению выполнения скрипта и
команды:
{
"state": "exec",
"status": 1,
"data": {
"error": "{error_message}",
"source": "{source_script}"
}
}
6. Сообщение с запросом на ввод.
a. Состояние "input" -- воркер в состоянии ожидания ввода:
{
"state": "input",
"status": 0,
"data": {
"text": "{message_text}",
"source": "{source_script}"
}
}
7. Сообщение от клиента с данными.
a. Ввод успешно выполнен и передается:
{
"state": "input",
"status": 0,
"data": "{client_data}"
}
b. Ввод данных прерван, данных не будет:
{
"state": "input",
"status": 1,
"data": null
}
8. Сообщение от воркера о получении ввода.
a. Ввод успешно получен, о чем говорит переход в состояние выполнения со
статусом 0, т.е. без ошибок:
{
"state": "exec",
"status": 0,
"data": null
}
9. Сообщение воркера об окончании работы:
a. Успешное окончание работы:
{
"state": "finish",
"status": 0,
"data": null
}
b. Работа воркера была прервана или закончилась с ошибками:
{
"state": "finish",
"status": 1,
"data": [
"{error_message}",
...
]
}

@ -0,0 +1,100 @@
import json
import logging
from multiprocessing.connection import Listener, Connection
from typing import Union
class WorkerIOError(KeyboardInterrupt):
pass
class IOModule:
'''Класс модуля ввода/вывода для воркеров.'''
def __init__(self, listener: Listener, command_name: str):
self._listener: Listener = listener
self._connection: Union[Connection, None] = None
self._command: str = command_name
self._script: str = ""
@property
def command(self) -> str:
return self._command
@property
def script(self) -> str:
return self._script
@script.setter
def script(self, script: str) -> None:
self._script = script
def set_debug(self, msg: str) -> None:
self.output(msg, level=logging.DEBUG)
def set_info(self, msg: str) -> None:
self.output(msg, level=logging.INFO)
def set_warning(self, msg: str) -> None:
self.output(msg, level=logging.WARNING)
def set_error(self, msg: str) -> None:
self.output(msg, level=logging.ERROR)
def set_critical(self, msg: str) -> None:
self.output(msg, level=logging.CRITICAL)
def output(self, text: str, level: int = logging.INFO) -> None:
'''Метод для отправки серверу вывода с указанием уровня.'''
output_request = {"state": "exec",
"status": 0,
"data": {
"type": "message",
"logging": level,
"text": text,
"source": self._script
}
}
self.send(output_request)
def input(self, msg: str) -> str:
'''Метод через который возможен ввод данных в скрипт.'''
input_request = {"state": "input",
"status": 0,
"data": {"text": msg,
"source": f"{self.command}:{self.script}"}}
answer = None
while answer is None:
self.send(input_request)
answer = self.receive()
return answer['data']
def send(self, data: dict) -> None:
'''Метод для отправки данных серверу.'''
if self._connection is None:
raise WorkerIOError("No one is connected now.")
data = json.dumps(data).encode()
self._connection.send_bytes(data)
def receive(self) -> dict:
'''Метод для получения данных от сервера.'''
if self._connection is None:
raise WorkerIOError("No one is connected now.")
data: bytes = self._connection.recv_bytes()
return json.loads(data.decode())
def __enter__(self):
self._connection = self._listener.accept()
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
if exc_type:
print("exc_type =", exc_type)
print("exc_value =", exc_value)
print("exc_traceback =", exc_traceback)
if exc_type is KeyboardInterrupt:
print("correctly")
if not self._connection.closed:
self._connection.close()

@ -0,0 +1,202 @@
from typing import Any, List, Dict, Optional, Literal, Union
from pydantic import BaseModel, Field, HttpUrl
# from utils import ResponseStructure
# from pprint import pprint
# import json
# from fastapi import HTTPException, status
class NotTemplatedLinkData(BaseModel):
href: HttpUrl = "URI"
class Config:
extra = "forbid"
class TemplatedLinkData(NotTemplatedLinkData):
templated: bool = False
LinkData = Union[NotTemplatedLinkData, TemplatedLinkData]
# GET /
class ServerInfo(BaseModel):
name: str = "Server name"
version: str = "Server version"
class GetRootResponse(BaseModel):
data: ServerInfo
links: Dict[str, LinkData] = Field(..., alias="_links",
description="Links to resources")
# GET /commands/{CID}/parameters
class ParameterInfo(BaseModel):
id: str
default: Any
class ParametersGroup(BaseModel):
group_id: str
parameters: List[ParameterInfo]
class GetCommandParametersResponse(BaseModel):
data: List[ParametersGroup]
links: Dict[str, LinkData] = Field(..., alias="_links")
# GET /commands/{CID}
class CommandData(BaseModel):
id: str
title: str
category: str
command: str
class CommandInfo(BaseModel):
data: CommandData
links: Dict[str, LinkData] = Field(..., alias="_links")
class FoundCommandInfo(CommandInfo):
embedded: Dict[str,
GetCommandParametersResponse] = Field(...,
alias="_embedded")
# GET /commands/
class GetCommandsResponse(BaseModel):
__root__: Dict[str, CommandInfo]
# class Config:
# @staticmethod
# def schema_extra(schema, model):
# example = {}
# for num in range(2):
# resp = ResponseStructure()
# resp.add_data(id=f"command_{num}",
# command=f"cl-command-{num}",
# title=f"Command {num}",
# category="cool-category"
# )
# resp.add_link("parameters",
# f"/commands/command_{num}/parameters")
# resp.add_link("configure", f"/configs/command_{num}")
# example[f"command_{num}"] = resp.get_dict()
# schema["example"] = example
# POST /configs/{command}
class CreateConfigResponse(BaseModel):
links: Dict[str, LinkData] = Field(..., alias="_links")
# PATCH /configs/{WID}/parameters
class ParameterValue(BaseModel):
id: str
value: Any
class ModifyConfigRequest(BaseModel):
__root__: List[ParameterValue]
class ModifyConfigCorrectResponse(BaseModel):
links: Dict[str, LinkData] = Field(..., alias="_links")
class Config:
extra = "forbid"
class ModifyConfigUncorrectResponse(ModifyConfigCorrectResponse):
data: Optional[Dict[str, str]]
links: Dict[str, LinkData] = Field(..., alias="_links")
ModifyConfigResponse = Union[ModifyConfigCorrectResponse,
ModifyConfigUncorrectResponse]
# PUT /configs/{WID}/parameters
class ResetConfigRequest(BaseModel):
__root__: List[ParameterValue]
class ResetConfigResponse(BaseModel):
data: Optional[Dict[str, str]]
links: Dict[str, LinkData] = Field(..., alias="_links")
# POST /executions/{WID}
class CreateExecutionResponse(BaseModel):
links: Dict[str, LinkData] = Field(..., alias="_links")
# DELETE /executions/{WID}
class OutputData(BaseModel):
type: Literal["output"]
logging: Union[int, None]
text: str
source: str
class OutputMessage(BaseModel):
data: OutputData
class StopExecutionsResponse(BaseModel):
__root__: List[OutputMessage]
# DELETE /configs/{WID}
class StopConfigResponse(BaseModel):
__root__: List[OutputMessage]
# GET /executions/{WID}/output
class InputRequestData(BaseModel):
type: Literal["input"]
text: str
source: str
class InputMessage(BaseModel):
data: InputRequestData
links: Dict[str, LinkData] = Field(..., alias="_links")
class GetExecutionOutputResponse(BaseModel):
__root__: List[Union[OutputMessage, InputMessage]]
# PATCH /executions/{WID}/input
class WriteToExecutionRequest(BaseModel):
data: Any
# GET /workers/
class WorkerData(BaseModel):
socket: str
command: str
pid: int
class WorkerInfo(BaseModel):
data: WorkerData
links: Dict[str, LinkData] = Field(..., alias="_links")
class GetWorkersResponse(BaseModel):
__root__: Dict[int, WorkerInfo]
# DELETE /workers/{WID}
# Сообщения протокола взаимодействия сервера и воркеров.
class WorkerMessageBase(BaseModel):
state: str
status: int

@ -0,0 +1,55 @@
from io_module import IOModule
import time
def script_0(io: IOModule, parameters: dict) -> None:
try:
io.set_info(f"PARAMETERS: {parameters}")
value_0 = get_integer(0, parameters, io)
value_1 = get_integer(1, parameters, io)
time.sleep(2)
result = value_0 + value_1
io.set_info(f"Script executed with result = {result}")
except Exception as error:
io.set_error(f"{str(error)}")
def script_1(io: IOModule, parameters: dict) -> None:
try:
io.set_info(f"PARAMETERS: {parameters}")
value_0 = get_integer(0, parameters, io)
value_1 = get_integer(1, parameters, io)
time.sleep(3)
result = value_0 * value_1
io.set_info(f"Script executed with result = {result}")
except Exception as error:
io.set_error(f"{str(error)}")
def script_2(io: IOModule, parameters: dict) -> None:
try:
io.set_info(f"PARAMETERS: {parameters}")
value_0 = get_integer(0, parameters, io)
value_1 = get_integer(1, parameters, io)
time.sleep(4)
result = value_0 ** value_1
io.set_info(f"Script executed with result = {result}")
except Exception as error:
io.set_error(f"{str(error)}")
def get_integer(index: int, parameters: dict, io: IOModule) -> int:
io.set_info(f"Looking for 'value_{index}'")
value = parameters.get(f"value_{index}", None)
if value is None:
io.set_warning(f"'value_{index}' not found")
value = io.input(f"Enter 'value_{index}'")
else:
io.set_info(f"value_{index} = {value}")
return int(value)

@ -0,0 +1,282 @@
import asyncio
# import logging
import uvicorn
from starlette.responses import JSONResponse
from starlette.requests import Request
from fastapi import FastAPI, status, HTTPException
from fastapi.encoders import jsonable_encoder
from pydantic.main import ModelMetaclass
from pydantic import parse_obj_as
from typing import _GenericAlias, Union, Optional
from commands import command_0, command_1, command_2
from utils import ResponseStructure, Commands
from worker import (
WorkersManager,
UnexpectedWorkerFinish,
UnexpectedWorkerState,
DaemonIsDead
)
from schemas import (
GetRootResponse,
GetCommandsResponse,
FoundCommandInfo,
GetCommandParametersResponse,
CreateConfigResponse,
ModifyConfigRequest,
ModifyConfigResponse,
ResetConfigRequest,
ResetConfigResponse,
StopConfigResponse,
CreateExecutionResponse
)
from pprint import pprint
api = FastAPI(title="Calculate Server")
workers = WorkersManager(loop=asyncio.get_event_loop())
commands = Commands(command_0,
command_1,
command_2)
@api.on_event("startup")
async def startup_event():
check_pid_files()
@api.on_event("shutdown")
async def shutdown_event():
workers.clean()
@api.get("/", response_model=GetRootResponse, tags=["Home"])
async def get_primary_commands(request: Request):
resp = ResponseStructure(get_base_url(request))
resp.add_data(name="Calculate Server", version="0.1")
resp.add_link("commands", "/commands?gui={is_gui}", templated=True)
resp.add_link("find_command", "/commands/{console_command}?gui={is_gui}",
templated=True)
resp.add_link("workers", "/workers")
return validate_response(resp.get_dict(), GetRootResponse,
media_type="application/hal+json")
@api.get("/commands", response_model=GetCommandsResponse, tags=["Commands"])
async def get_available_commands(request: Request,
gui: Optional[bool] = False):
response_data = commands.get_commands(get_base_url(request))
return validate_response(response_data, GetCommandsResponse,
media_type="application/hal+json")
@api.get("/commands/{command}",
response_model=FoundCommandInfo,
tags=["Commands"])
async def find_command_data(command: str, request: Request,
gui: Optional[bool] = False,
by_id: Optional[bool] = False):
base_url = get_base_url(request)
if by_id:
command_data = commands.get_by_id(command, base_url)
if command_data is None:
raise get_command_not_found(command)
else:
command_data = commands.find_command(command, base_url)
if command_data is None:
raise get_cl_command_not_found(command)
return validate_response(command_data, FoundCommandInfo,
media_type="application/hal+json")
@api.get("/commands/{command_id}/parameters",
response_model=GetCommandParametersResponse,
tags=["Commands"])
async def get_command_parameters(command_id: str, request: Request):
parameters_data = commands.get_parameters(command_id,
get_base_url(request))
if parameters_data is None:
raise get_command_not_found(command_id)
return parameters_data
@api.post("/configs/{command_id}", response_model=CreateConfigResponse,
tags=["Configurations"])
async def create_configuration(command_id: str, request: Request):
command = commands.get_command_object(command_id)
if command is None:
raise get_command_not_found(command_id)
print("Making worker")
worker_id, error = await workers.make_worker(command)
if error is not None:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error)
resp = ResponseStructure(get_base_url(request))
resp.add_link("configure", f"/configs/{worker_id}/parameters")
resp.add_link("execute", f"/execs/{worker_id}")
resp.add_link("cancel", f"/configs/{worker_id}")
return validate_response(resp.get_dict(), CreateConfigResponse)
@api.patch("/configs/{wid}/parameters", tags=["Configurations"])
async def modify_configuration_parameters(wid: int,
parameters: ModifyConfigRequest,
request: Request):
response = await change_config(get_base_url(request), wid, parameters)
return validate_response(response, ModifyConfigResponse)
@api.put("/configs/{wid}/parameters", tags=["Configurations"])
async def reset_configuration_parameters(wid: int,
parameters: ResetConfigRequest,
request: Request):
response = await change_config(get_base_url(request), wid, parameters,
reset=True)
return validate_response(response, ResetConfigResponse)
async def change_config(base_url: str, wid: int,
parameters: ModifyConfigRequest,
reset: bool = False) -> dict:
parameters = parameters.dict()["__root__"]
try:
result = await workers.configure_worker(wid, parameters, reset=reset)
except UnexpectedWorkerFinish as error:
return HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=("Unexpected configuration finish. Reason:"
f" {error.reason[0]}"))
except UnexpectedWorkerState as error:
print("Unexpected worker state.")
return HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=('Unexpected configuration state:'
f' "{error.state}". Expected:'
f' "{error.expected}"'))
except DaemonIsDead:
print("DAEMON IS DEAD")
return HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Daemon is dead")
print("RESULT:")
pprint(result)
if result is None:
raise get_configuration_not_found(wid)
resp = ResponseStructure(base_url)
resp.add_link("configure", f"configs/{wid}/parameters")
resp.add_link("reset", f"configs/{wid}/parameters")
resp.add_link("cancel", f"configs/{wid}")
if not result:
resp.add_link("execute", f"/execs/{wid}")
else:
resp.add_data(result)
return resp.get_dict()
@api.delete("/configs/{wid}", response_model=StopConfigResponse,
tags=["Configurations"])
async def stop_configuration(wid: int):
output = await workers.cancel_worker_configuration(wid)
if output is None:
raise get_configuration_not_found(wid)
return []
@api.post("/execs/{wid}", tags=["Executions"])
async def start_command_execution(wid: int, request: Request):
result = workers.run_execution(wid)
if result > 0:
raise get_configuration_not_found(wid)
elif result < 0:
raise HTTPException(status_code=status.HTTP_409_CONFLICT,
detail=f"Execution id = {wid} already exists.")
resp = ResponseStructure(get_base_url(request))
resp.add_link("configure", f"/configs/{wid}/parameters")
resp.add_link("reset", f"/configs/{wid}/parameters")
resp.add_link("cancel", f"/configs/{wid}")
return validate_response(resp.get_dict(), CreateExecutionResponse)
@api.delete("/execs/{wid}", tags=["Executions"])
async def stop_command_execution(wid: int):
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="'stop execution' is not implemented")
@api.get("/execs/{wid}/output", tags=["Executions"])
async def read_execution_output(wid: int):
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="'stop execution' is not implemented")
@api.patch("/execs/{wid}/input", tags=["Executions"])
async def write_to_execution(wid: int):
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="'write to execution' is not implemented")
def check_pid_files(pid_files_dir: str = "./") -> None:
pass
def get_base_url(request: Request):
base_url = f"{request.base_url.scheme}://{request.base_url.netloc}"
return base_url
def get_command_not_found(command_id: str) -> HTTPException:
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=(f'Command with id "{command_id}"'
' is not found.'))
def get_cl_command_not_found(console_command: str) -> HTTPException:
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=(f'Console command "{console_command}" is not'
' found.'))
def get_configuration_not_found(wid: int) -> HTTPException:
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Configuration with id={wid} is not found.")
def validate_response(data: Union[dict, list],
schema: Union[ModelMetaclass, _GenericAlias, type],
media_type: Optional[str] = None,
status_code: int = 200
) -> JSONResponse:
"""Функция для валидации данных ответа сервера по указанной схеме с учетом
наличия псевдонимов полей и возможности того, что схема задана обычным
типом, типом из typing или BaseModel с __root__. Возвращает объект ответа.
"""
if isinstance(schema, ModelMetaclass):
if "__root__" in schema.__annotations__:
validated = schema(__root__=data)
else:
validated = schema(**data)
elif isinstance(schema, (_GenericAlias, type)):
validated = parse_obj_as(schema, data)
return JSONResponse(content=jsonable_encoder(validated, by_alias=True),
media_type=media_type, status_code=status_code)
if __name__ == "__main__":
uvicorn.run("server:api", host="127.0.0.1", port=2007, reload=True)

@ -0,0 +1,199 @@
from fastapi import status, HTTPException
from fastapi.encoders import jsonable_encoder
from starlette.responses import JSONResponse
from starlette.requests import Request
from pydantic.main import ModelMetaclass
from pydantic import parse_obj_as
from typing import Dict, Union, List, _GenericAlias, Optional
from commands import Command
# from pprint import pprint
class ResponseStructure:
'''Класс структуры данных, для получения HATEAOS ответа на запросы
клиента.'''
def __init__(self, base_url: str):
self._base_url: str = base_url.strip("/")
self._data: dict = dict()
self._links: Dict[str, Dict[str, str]] = dict()
self._embedded: Dict[str, dict] = dict()
def get_dict(self) -> dict:
dictionary = dict()
if self._data:
dictionary["data"] = self._data
if self._links:
dictionary["_links"] = self._links
if self._embedded:
dictionary["_embedded"] = self._embedded
return dictionary
def add_data(self, *args, **kwargs) -> "ResponseStructure":
if args:
self._data = args[0]
if kwargs:
self._data.update(kwargs)
return self
def embed(self, resource: str,
content: Union[dict, list]) -> "ResponseStructure":
self._embedded[resource] = content
return self
def add_link(self, action: str, uri: str,
templated: bool = False) -> "ResponseStructure":
uri = self._get_uri(uri)
if action in self._links:
link_section = self._links[action]
if link_section["href"] != uri:
link_section["href"] = uri
else:
link_section = {"href": uri}
self._links[action] = link_section
if templated:
print("TEMPLATED:", action)
link_section["templated"] = templated
return self
def _get_uri(self, uri: str) -> str:
if uri.startswith(self._base_url):
return uri
return f"{self._base_url}/{uri.strip('/')}"
class Commands:
'''Предварительная реализация контейнера описаний команд.'''
def __init__(self, *commands: List[Command]):
self._by_id: dict = dict()
self._by_command: dict = dict()
for command in commands:
self._by_id[command.id] = command
self._by_command[command.command] = command
def get_commands(self, base_url: str) -> Dict[str, dict]:
response = dict()
for command in self._by_id.values():
data = ResponseStructure(base_url)
data.add_data(id=command.id,
title=command.title,
category=command.category,
command=command.command)
data.add_link("self", f"/commands/{command.id}?by_id=true")
data.add_link("parameters", f"/commands/{command.id}/parameters")
data.add_link("configure", f"/configs/{command.id}")
response[command.id] = data.get_dict()
return response
def get_by_id(self, command_id: str, base_url: str) -> Dict[str, Dict]:
if command_id in self._by_id:
command = self._by_id[command_id]
data = ResponseStructure(base_url)
data.add_data(id=command.id,
title=command.title,
category=command.category,
command=command.command)
data.add_link("self", f"/commands/{command.id}?by_id=1")
data.add_link("parameters", f"/commands/{command.id}/parameters")
data.add_link("configure", f"/configs/{command.id}")
parameters_data = self._get_parameters_data(command.parameters,
command.id, base_url)
data.embed("parameters", parameters_data)
return data.get_dict()
return None
def find_command(self, console_command: str,
base_url: str) -> Union[dict, None]:
if console_command in self._by_command:
command = self._by_command[console_command]
data = ResponseStructure(base_url)
data.add_data(id=command.id,
title=command.title,
category=command.category,
command=command.command)
data.add_link("self", f"/commands/{command.id}?by_id=1")
data.add_link("parameters", f"/commands/{command.id}/parameters")
data.add_link("configure", f"/configs/{command.id}")
parameters_data = self._get_parameters_data(command.parameters,
command.id, base_url)
data.embed("parameters", parameters_data)
return data.get_dict()
else:
return None
def get_parameters(self, command_id: str, base_url: str
) -> Union[dict, None]:
if command_id in self._by_id:
command = self._by_id[command_id]
parameters_data = self._get_parameters_data(command.parameters,
command.id, base_url)
return parameters_data
else:
return None
def get_command_object(self, command_id: str) -> Command:
if command_id in self._by_id:
return self._by_id[command_id]
return None
def _get_parameters_data(self, parameters: list, command_id: str,
base_url: str) -> List[dict]:
data = ResponseStructure(base_url)
data.add_data(parameters)
data.add_link("self", f"/commands/{command_id}/parameters")
return data.get_dict()
def get_base_url(request: Request):
base_url = f"{request.base_url.scheme}://{request.base_url.netloc}"
return base_url
def get_command_not_found(command_id: str) -> HTTPException:
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=(f'Command with id "{command_id}"'
' is not found.'))
def get_cl_command_not_found(console_command: str) -> HTTPException:
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=(f'Console command "{console_command}" is not'
' found.'))
def get_configuration_not_found(wid: int) -> HTTPException:
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Configuration with id={wid} is not found.")
def validate_response(data: Union[dict, list],
schema: Union[ModelMetaclass, _GenericAlias, type],
media_type: Optional[str] = None,
status_code: int = 200
) -> JSONResponse:
"""Функция для валидации данных ответа сервера по указанной схеме с учетом
наличия псевдонимов полей и возможности того, что схема задана обычным
типом, типом из typing или BaseModel с __root__. Возвращает объект ответа.
"""
if isinstance(schema, ModelMetaclass):
if "__root__" in schema.__annotations__:
validated = schema(__root__=data)
else:
validated = schema(**data)
elif isinstance(schema, (_GenericAlias, type)):
validated = parse_obj_as(schema, data)
return JSONResponse(content=jsonable_encoder(validated, by_alias=True),
media_type=media_type, status_code=status_code)

@ -0,0 +1,413 @@
import os
import sys
import json
import struct
import signal
import asyncio
import logging
from multiprocessing import Process
from multiprocessing.connection import Listener
# import logging
# import time
from daemon import daemon
from commands import Command
from typing import Dict, Optional, List, Union, Tuple, Literal
from asyncio.unix_events import _UnixSelectorEventLoop
from asyncio import get_event_loop
LOG_LEVELS = {logging.ERROR: "ERROR",
logging.INFO: "INFO",
logging.WARNING: "WARNING",
logging.DEBUG: "DEBUG",
logging.CRITICAL: "CRITICAL",
}
class WorkerException(Exception):
pass
class UnexpectedWorkerFinish(WorkerException):
def __init__(self, reason: List[str], output: Optional[List[str]] = []):
"""Исключение кидаемое в случае, если воркер неожиданно умер."""
self.reason: List[str] = reason
self.output: List[dict] = output
class UnexpectedWorkerState(WorkerException):
"""Исключение кидаемое в случае, если состояние воркера не соответствует
ожидаемому."""
def __init__(self, state: str, expected: str):
self.state: str = state
self.expected: str = expected
class DaemonIsDead(WorkerException):
"""Исключение кидаемое в случае, если демон-воркер неожиданно оказался
мертв."""
def __init__(self, info: str, output: Optional[List[str]] = []):
self.info: str = info
self.output: List[dict] = output
class IncorrectConfiguration(WorkerException):
"""Исключение кидаемое при попытке запустить выполнение конфигурации, в
которой есть ошибки."""
def __init__(self, errors: Dict[str, str]):
self._errors = errors
class WorkersManager:
def __init__(self, loop: Optional[_UnixSelectorEventLoop] = None):
if loop is None:
self._loop = get_event_loop()
else:
self._loop = loop
self._configs: Dict[int, WorkerProtocol] = {}
self._execs: Dict[int, WorkerProtocol] = {}
self._wids: List[int] = []
async def make_worker(self, command: Command) -> Tuple[Union[int, None],
Union[str, None]]:
"""Метод для создания воркеров."""
wid = self._get_wid()
try:
# Создаем воркера и получаем интерфейс для взаимодействия с ним.
worker_api = await self._create_worker(wid, command)
self._configs[wid] = worker_api
except Exception as error:
if wid in self._configs:
self._configs.pop(wid)
return None, str(error)
return wid, None
async def configure_worker(self, wid: int, parameters: List[dict],
reset: bool = False) -> Union[dict, None]:
"""Метод для модификации или сброса параметров конфигурации указанными
значениями.
"""
if not parameters:
return {}
if wid not in self._configs:
return None
worker_api: WorkerProtocol = self._configs[wid]
try:
result = await worker_api.configure(parameters)
except (UnexpectedWorkerFinish, DaemonIsDead):
worker_api.kill()
self._configs.pop(wid)
raise
return result
async def cancel_worker_configuration(self, wid: int):
"""Метод для завершения конфигурации воркера и завершения его работы.
"""
if wid not in self._configs:
return None
worker_api: WorkerProtocol = self._configs[wid]
output = await worker_api.cancel_configuration()
worker_api.delete_socket()
self._configs.pop(wid)
return output
def clean(self):
"""Метод для убийства всех работающих на данный момент демонов."""
# TODO доработать его логику так, чтобы при перезапуске сервера этот
# метод никого не убивал.
for wid, worker in {**self._configs, **self._execs}.items():
result = False
try:
result = worker.kill()
finally:
if result:
print(f"[*] Worker {wid} is killed.")
else:
print(f"[x] Worker {wid} is not killed.")
continue
def get_config(self, wid: int):
if wid in self._configs:
return self._configs[wid]
return None
def get_execution(self, wid: int):
if wid in self._execs:
return self._execs[wid]
return None
def run_execution(self, wid: int) -> Tuple[int, Union[dict, None]]:
'''Метод для запуска исполнения команды, используя указанную
конфигурацию.
Вывод:
0, None -- исполнение успешно запущено;
1, None -- конфигурация с данным wid не найдена;
2, errors -- конфигурация с данным wid содержит ошибки;
3, None -- исполнение с данным wid уже существует;
'''
if wid not in self._configs:
return 1, None
if self._configs[wid].status > 0:
# Берем ошибки, как-то.
errors = {}
return 2, errors
if wid in self._execs:
return 3, None
# worker = self._configs.pop(wid)
return 0, None
async def _create_worker(self, wid: int, command: Command
) -> "WorkerProtocol":
# Путь к временному файлу, в котором будет лежать pid демона-рабочего.
pid_file_path: str = os.path.join(os.getcwd(), f"pid_file_{wid}")
socket_path: str = os.path.join(os.getcwd(),
f"worker_{wid}.sock")
if os.path.exists(socket_path):
os.remove(socket_path)
process_runner = Process(target=daemonize_worker,
args=(socket_path, wid, pid_file_path,
command)
)
process_runner.start()
process_runner.join()
daemon_pid = await self._get_daemon_pid(pid_file_path)
self._delete_pid_file(pid_file_path)
transport, worker_interface = await self._loop.create_unix_connection(
lambda: WorkerProtocol(daemon_pid,
socket_path),
socket_path)
return worker_interface
def _get_wid(self):
if not self._wids:
worker_id = 0
else:
worker_id = self._wids[-1] + 1
self._wids.append(worker_id)
return worker_id
async def _get_daemon_pid(self, pid_file_path: str) -> int:
while True:
if not os.path.exists(pid_file_path):
# Если файла нет -- уступаем работу другим воркерам.
await asyncio.sleep(0)
with open(pid_file_path, "r") as pid_file:
daemon_pid = int(pid_file.readline())
return daemon_pid
def _delete_pid_file(self, pid_file_path: str) -> None:
try:
os.remove(pid_file_path)
except OSError:
pass
def __repr__(self):
configs = ["Configurations:"]
configs.extend([f"wid={wid}" for wid in self._configs.keys()])
configs_repr = "\n\t".join(configs)
execs = ["Executions:"]
execs.extend([f"wid={wid}" for wid in self._execs.keys()])
execs_repr = "\n\t".join(execs)
return f"{configs_repr}\n{execs_repr}"
def daemonize_worker(socket_path: str, wid: int, pid_file: str,
command: Command) -> None:
'''Функция запускаемая в отдельном процессе и порождающая демона при помощи
магии двойного форка.'''
base_dir = os.getcwd()
new_fork()
os.chdir("/")
os.setsid()
os.umask(0)
new_fork()
# Создаем файл, с помощью которого будем передавать pid демона-рабочего.
daemon_pid = str(os.getpid())
with open(pid_file, "w") as pid_file:
pid_file.write(daemon_pid)
with Listener(socket_path) as listener:
daemon(listener, wid, base_dir, command)
def new_fork() -> None:
'''Функция для создания форка текущего процесса.'''
try:
middle_pid = os.fork()
if middle_pid > 0:
sys.exit(0)
except OSError as error:
print(f"FORK ERROR: {error.errno} - {str(error)}")
sys.exit(1)
class WorkerProtocol(asyncio.Protocol):
"""Класс протокола управления воркером."""
def __init__(self, pid: int, socket_path: str):
self.loop = asyncio.get_running_loop()
self._pid: int = pid
self._socket_path: str = socket_path
# Объект Event, предназначенный для остановки записи при переполнении
# буффера записи в трспорте.
self._can_write = asyncio.Event()
self._can_write.clear()
self._msg_queue = asyncio.Queue()
# Контекст чтения данных воркера.
self._buffer = bytearray()
self._msg_size: int = 0
self._connection_closed: bool = True
async def configure(self, parameters: List[dict],
reset: bool = False) -> list:
"""Метод для конфигурации воркера указанными значениями параметров.
Возвращает список ошибок, если таковые есть, или пустой список, если
никаких ошибок нет.
"""
message = self._make_worker_message("config", int(reset), parameters)
await self._write(message)
response = await self._read()
response = json.loads(response)
return response["data"]
async def cancel_configuration(self) -> List[dict]:
"""Метод для остановки конфигурации воркера и его работы в целом."""
stop_message: bytes = self._make_worker_message("finish", 1, None)
await self._write(stop_message)
response = await self._read()
response = json.loads(response)
return response["data"]
def connection_made(self, transport: asyncio.Transport) -> None:
self.transport = transport
self._can_write.set()
self._connection_closed: bool = False
print("Connection made.")
def connection_lost(self, exc: BaseException) -> None:
if exc:
print("Connection lost. Error:", str(exc))
else:
print("Connection lost.")
self._connection_closed: bool = True
def data_received(self, data: bytes) -> None:
self._buffer.extend(data)
while True:
if not self._msg_size:
if len(self._buffer) < 4:
return
size_length = struct.calcsize(">i")
self._msg_size, = struct.unpack(">i",
self._buffer[:size_length])
self._buffer = self._buffer[size_length:]
if len(self._buffer) >= self._msg_size:
data = self._buffer[:self._msg_size]
self._buffer = self._buffer[self._msg_size:]
self._msg_size = 0
self._msg_queue.put_nowait(data.decode())
else:
break
def pause_writing(self) -> None:
self._can_write.clear()
def resume_writing(self) -> None:
self._can_write.set()
async def _read(self) -> str:
if self._connection_closed and self._msg_queue.empty():
return None
return await self._msg_queue.get()
async def read_all(self) -> List[str]:
if self._msg_queue.empty():
if self._connection_closed:
return None
else:
return await self._msg_queue.get()
output = []
while not self._msg_queue.empty():
output.append(self._msg_queue.get_nowait())
return output
async def _write(self, data: Union[str, bytes]) -> None:
await self._can_write.wait()
if isinstance(data, str):
data = data.encode()
self.transport.write(struct.pack(">i", len(data)))
self.transport.write(data)
def _make_worker_message(
self,
state: Literal["config", "finish", "exec", "input"],
status: int, data: Union[list, dict]) -> dict:
return json.dumps({"state": state,
"status": status,
"data": data}).encode()
def delete_socket(self) -> None:
"""Метод для удаления сокета."""
try:
os.remove(self._socket_path)
self.transport.close()
self._connection_closed = True
except OSError:
pass
def kill(self) -> bool:
"""Метод для убийства демона."""
print("GONNA KILLA DAEMON:", self._pid)
if self.is_daemon_alive():
try:
os.kill(self._pid, signal.SIGKILL)
except OSError:
return False
else:
print("ALREADY DEAD")
return True
def stop(self) -> bool:
print("STOP DAEMON:", self._daemon_pid)
if self.is_daemon_alive():
try:
# Для остановки демона посылаем в него KeyboardInterrupt.
os.kill(self._pid, signal.SIGINT)
except OSError as error:
print(f"ERROR: Failed stopping daemon PID={self._pid}. "
f"Reason: {str(error)}")
return False
return True
def is_daemon_alive(self) -> bool:
try:
os.kill(self._pid, 0)
return True
except OSError:
return False

@ -0,0 +1,428 @@
import os
import sys
import json
import socket
import signal
import logging
from multiprocessing import Process
# from asyncio import sleep
# import logging
# import time
from io_module import IOModule
from daemon import daemon
from commands import Command
from typing import Dict, Optional, List, Union, Tuple, Literal
from asyncio.unix_events import _UnixSelectorEventLoop
from asyncio import get_event_loop, sleep
from pprint import pprint
LOG_LEVELS = {logging.ERROR: "ERROR",
logging.INFO: "INFO",
logging.WARNING: "WARNING",
logging.DEBUG: "DEBUG",
logging.CRITICAL: "CRITICAL",
}
class WorkerException(Exception):
pass
class UnexpectedWorkerFinish(WorkerException):
def __init__(self, reason: List[str], output: Optional[List[str]] = []):
"""Исключение кидаемое в случае, если воркер неожиданно умер."""
self.reason: List[str] = reason
self.output: List[dict] = output
class UnexpectedWorkerState(WorkerException):
"""Исключение кидаемое в случае, если состояние воркера не соответствует
ожидаемому."""
def __init__(self, state: str, expected: str):
self.state: str = state
self.expected: str = expected
class DaemonIsDead(WorkerException):
"""Исключение кидаемое в случае, если демон-воркер неожиданно оказался
мертв."""
def __init__(self, info: str, output: Optional[List[str]] = []):
self.info: str = info
self.output: List[dict] = output
class IncorrectConfiguration(WorkerException):
"""Исключение кидаемое при попытке запустить выполнение конфигурации, в
которой есть ошибки."""
def __init__(self, errors: Dict[str, str]):
self._errors = errors
class WorkersManager:
def __init__(self, loop: Optional[_UnixSelectorEventLoop] = None):
if loop is None:
self._loop = get_event_loop()
else:
self._loop = loop
self._configs: Dict[int, Worker] = {}
self._execs: Dict[int, Worker] = {}
self._wids: List[int] = []
async def make_worker(self, command: Command) -> Tuple[Union[int, None],
Union[str, None]]:
wid = self._get_wid()
try:
worker = Worker(wid, loop=self._loop)
await worker.create_daemon(command)
self._configs[wid] = worker
except Exception as error:
if wid in self._configs:
self._configs.pop(wid)
return None, str(error)
return wid, None
async def configure_worker(self, wid: int, parameters: List[dict],
reset: bool = False) -> Union[dict, None]:
"""Метод для модификации или сброса параметров конфигурации указанными
значениями."""
if not parameters:
return {}
if wid not in self._configs:
return None
worker: Worker = self._configs[wid]
try:
result = await worker.configure(parameters)
except (UnexpectedWorkerFinish, DaemonIsDead):
worker.kill()
self._configs.pop(wid)
raise
return result
async def cancel_worker_configuration(self, wid: int):
if wid not in self._configs:
return None
worker: Worker = self._configs[wid]
output = await worker.cancel_configuration()
self._configs.pop(wid)
return output
def clean(self):
"""Метод для убийства всех работающих на данный момент демонов."""
# TODO доработать его логику так, чтобы при перезапуске сервера этот
# метод никого не убивал.
print("KILLING WORKERS:")
for wid, worker in {**self._configs, **self._execs}.items():
result = False
try:
result = worker.kill()
finally:
if result:
print(f"[*] Worker {wid} is killed.")
else:
print(f"[x] Worker {wid} is not killed.")
continue
def get_config(self, wid: int):
if wid in self._configs:
return self._configs[wid]
return None
def get_execution(self, wid: int):
if wid in self._execs:
return self._execs[wid]
return None
def run_execution(self, wid: int) -> Tuple[int, Union[dict, None]]:
'''Метод для запуска исполнения команды, используя указанную
конфигурацию.
Вывод:
0, None -- исполнение успешно запущено;
1, None -- конфигурация с данным wid не найдена;
2, errors -- конфигурация с данным wid содержит ошибки;
3, None -- исполнение с данным wid уже существует;
'''
if wid not in self._configs:
return 1, None
if self._configs[wid].status > 0:
# Берем ошибки, как-то.
errors = {}
return 2, errors
if wid in self._execs:
return 3, None
worker = self._configs.pop(wid)
return 0, None
def _get_wid(self):
if not self._wids:
worker_id = 0
else:
worker_id = self._wids[-1] + 1
self._wids.append(worker_id)
return worker_id
def __repr__(self):
configs = ["Configurations:"]
configs.extend([f"wid={wid}" for wid in self._configs.keys()])
configs_repr = "\n\t".join(configs)
execs = ["Executions:"]
execs.extend([f"wid={wid}" for wid in self._execs.keys()])
execs_repr = "\n\t".join(execs)
return f"{configs_repr}\n{execs_repr}"
class Worker:
def __init__(self, wid: int,
loop: Optional[_UnixSelectorEventLoop] = None):
if loop is None:
self._loop = get_event_loop()
else:
self._loop = loop
self._wid: int = wid
self._socket_path: str = os.path.join(os.getcwd(),
f"worker_{self._wid}.sock")
if os.path.exists(self._socket_path):
os.remove(self._socket_path)
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self._daemon_pid = None
# Контекст для чтения данных из сокета.
self._context: str = ""
self._state: str = ""
self._status: int = 0
@property
def state(self):
return self._state
@property
def status(self):
return self._status
async def configure(self, parameters: List[dict],
reset: bool = False) -> Union[dict, list]:
'''Метод для отправки в демона-воркера занчений параметров во время
конфигурации.'''
if self._state != "config":
raise UnexpectedWorkerState(self._state, "config")
message = self._make_worker_message("config", int(reset), parameters)
await self.send(message)
answers = await self.read()
print("WORKER'S ANSWERS")
pprint(answers)
config_answer = answers[0]
if config_answer["state"] == "finish":
raise UnexpectedWorkerFinish(config_answer["data"],
output=answers[1:])
return config_answer["data"]
async def cancel_configuration(self) -> List[dict]:
if self._state != "config":
raise UnexpectedWorkerState(self._state, "config")
stop_message = self._make_worker_message("finish", 1, None)
await self.send(stop_message)
output = await self.read()
print("OUTPUT")
pprint(output)
if output:
return output
return []
async def send(self, data: dict):
await self._loop.run_in_executor(None, self._send,
json.dumps(data) + '\0')
async def read(self):
messages = await self._loop.run_in_executor(None, self._read)
return messages
async def create_daemon(self, command: Command) -> None:
# Путь к временному файлу, в котором будет лежать pid демона-рабочего.
pid_file: str = os.path.join(os.getcwd(), f"PID_file_{self._wid}")
io = IOModule(self._socket_path, command.id)
runner_process = Process(target=daemonize_worker,
args=(io, self._wid, pid_file,
command)
)
runner_process.start()
self.sock.connect(self._socket_path)
runner_process.join()
self._daemon_pid = 0
while not self._daemon_pid:
await sleep(0.5)
self._daemon_pid = self._get_daemon_pid(pid_file)
self._delete_pid_file(pid_file)
print("DAEMON PID =", self._daemon_pid)
print("IS_ALIVE:", self.is_daemon_alive())
self._state = "config"
def kill(self) -> bool:
"""Метод для убийства демона."""
print("GONNA KILLA DAEMON:", self._daemon_pid)
if self.is_daemon_alive():
try:
os.kill(self._daemon_pid, signal.SIGKILL)
except OSError:
return False
else:
print("ALREADY DEAD")
# self._delete_socket()
self._daemon_pid = None
return True
def _stop_daemon(self) -> bool:
print("STOP DAEMON:", self._daemon_pid)
if self.is_daemon_alive():
try:
# Для остановки демона посылаем в него KeyboardInterrupt.
os.kill(self._daemon_pid, signal.SIGINT)
except OSError as error:
print(f"ERROR: Failed stopping daemon PID={self._daemon_pid}. "
f"Reason: {str(error)}")
return False
return True
def is_daemon_alive(self) -> bool:
try:
os.kill(self._daemon_pid, 0)
except OSError:
return False
else:
return True
def _send(self, data: str) -> bool:
try:
self.sock.sendall(data.encode())
except socket.error as error:
print(f"ERROR: can not send data to worker {self._wid}."
f" Reason: {str(error)}")
return False
return True
def _read(self) -> Union[dict, None]:
input_queue = []
while True:
try:
data = self.sock.recv(1024)
except Exception as error:
print(f"Connection is broken. Error: {str(error)}")
return None
raise DaemonIsDead(f"Connection is broken. "
f"Error: {str(error)}")
if data:
# Если данные получены делим их по нулевому байту.
data = data.decode().split('\0')
break
# Иначе считаем, что соединение нарушилось.
print("Connection is broken. Empty data.")
return None
raise DaemonIsDead("Connection is broken.")
if self._context:
self._context += data.pop(0)
# Если после взятия первого значения еще что-то есть в списке
# данных -- значит среди них был нулевой байт. И из значения
# контекста можно получить прочитанные данные.
if data:
request = json.loads(self._context)
input_queue.append(request)
self._context = ""
if data:
# Если данные еще есть в списке -- значит последнее значение в
# списке, даже если это пустая строка, является котекстом.
self._context = data.pop()
if data:
# Остальное добавляем в очередь.
for request in data:
input_queue.append(json.loads(request))
return input_queue
def _make_worker_message(self, state: Literal["config", "finish",
"exec", "input"],
status: int, data: Union[list, dict]) -> dict:
return {"state": state,
"status": status,
"data": data}
def _get_daemon_pid(self, pid_file_path: str) -> int:
if not os.path.exists(pid_file_path):
return 0
try:
with open(pid_file_path, "r") as pid_file:
daemon_pid = int(pid_file.readline())
return daemon_pid
except Exception:
return 0
def _delete_socket(self) -> None:
"""Метод для удаления сокета."""
try:
self.sock.close()
os.remove(self._socket_path)
except OSError:
pass
def _delete_pid_file(self, pid_file_path: str) -> None:
try:
os.remove(pid_file_path)
except OSError:
pass
def daemonize_worker(io: IOModule, wid: int, pid_file: str,
command: Command) -> None:
'''Функция запускаемая в отдельном процессе и порождающая демона при помощи
магии двойного форка.'''
base_dir = os.getcwd()
new_fork()
os.chdir("/")
os.setsid()
os.umask(0)
new_fork()
# Создаем файл, с помощью которого будем передавать pid демона-рабочего.
daemon_pid = str(os.getpid())
with open(pid_file, "w") as pid_file:
pid_file.write(daemon_pid)
daemon(io, wid, base_dir, command)
def new_fork() -> None:
'''Функция для создания форка текущего процесса.'''
try:
middle_pid = os.fork()
if middle_pid > 0:
sys.exit(0)
except OSError as error:
print(f"FORK ERROR: {error.errno} - {str(error)}")
sys.exit(1)

@ -1,56 +1,66 @@
from fastapi import APIRouter, Depends
from starlette.requests import Request
from ..utils.responses import (
ResponseStructure,
validate_response,
get_base_url,
get_command_not_found,
get_cl_command_not_found,
)
from typing import Optional
from ..utils.dependencies import right_checkers
from ..utils.dependencies import get_current_user
from ..utils.users import check_user_rights
from ..server_data import ServerData
from ..schemas.users import User
from ..schemas.responses import (
GetRootResponse,
GetCommandsResponse,
FoundCommandInfo,
GetCommandParametersResponse,
)
data = ServerData()
router = APIRouter()
@router.get("/commands", tags=["Commands management"],
dependencies=[Depends(right_checkers["read"])])
async def get_commands() -> dict:
'''Обработчик, отвечающий на запросы списка команд.'''
response = {}
for command_id, command_object in data.commands.items():
response.update({command_id: {"title": command_object.title,
"category": command_object.category,
"icon": command_object.icon,
"command": command_object.command}})
return response
@router.get("/commands/{cid}", tags=["Commands management"],
dependencies=[Depends(right_checkers["read"])])
async def get_command(cid: int) -> dict:
'''Обработчик запросов списка команд.'''
if cid not in data.commands_instances:
# TODO добавить какую-то обработку ошибки.
pass
return {'id': cid,
'name': f'command_{cid}'}
@router.get("/commands/{cid}/groups", tags=["Commands management"],
dependencies=[Depends(right_checkers["read"])])
async def get_command_parameters_groups(cid: int) -> dict:
'''Обработчик запросов на получение групп параметров указанной команды.'''
pass
@router.get("/commands/{cid}/parameters", tags=["Commands management"],
dependencies=[Depends(right_checkers["read"])])
async def get_command_parameters(cid: int) -> dict:
'''Обработчик запросов на получение параметров указанной команды.'''
pass
@router.post("/commands/{command_id}", tags=["Commands management"],
dependencies=[Depends(right_checkers["write"])])
async def post_command(command_id: str) -> int:
if command_id not in data.commands:
# TODO добавить какую-то обработку ошибки.
pass
return
@router.get("/commands", response_model=GetCommandsResponse, tags=["Commands"])
async def get_available_commands(request: Request,
gui: Optional[bool] = False):
response_data = commands.get_commands(get_base_url(request))
return validate_response(response_data, GetCommandsResponse,
media_type="application/hal+json")
@router.get("/commands/{command}",
response_model=FoundCommandInfo,
tags=["Commands"])
async def find_command_data(command: str, request: Request,
gui: Optional[bool] = False,
by_id: Optional[bool] = False):
base_url = get_base_url(request)
if by_id:
command_data = commands.get_by_id(command, base_url)
if command_data is None:
raise get_command_not_found(command)
else:
command_data = commands.find_command(command, base_url)
if command_data is None:
raise get_cl_command_not_found(command)
return validate_response(command_data, FoundCommandInfo,
media_type="application/hal+json")
@router.get("/commands/{command_id}/parameters",
response_model=GetCommandParametersResponse,
tags=["Commands"])
async def get_command_parameters(command_id: str, request: Request):
parameters_data = commands.get_parameters(command_id,
get_base_url(request))
if parameters_data is None:
raise get_command_not_found(command_id)
return parameters_data

@ -0,0 +1,22 @@
from typing import Any, List
from pydantic import BaseModel
# PATCH /configs/{WID}/parameters
class ParameterValue(BaseModel):
id: str
value: Any
class ModifyConfigRequest(BaseModel):
__root__: List[ParameterValue]
# PUT /configs/{WID}/parameters
class ResetConfigRequest(BaseModel):
__root__: List[ParameterValue]
# PATCH /executions/{WID}/input
class WriteToExecutionRequest(BaseModel):
data: Any

@ -0,0 +1,180 @@
from typing import Any, List, Dict, Optional, Literal, Union
from pydantic import BaseModel, Field, HttpUrl
class NotTemplatedLinkData(BaseModel):
href: HttpUrl = "URI"
class Config:
extra = "forbid"
class TemplatedLinkData(NotTemplatedLinkData):
templated: bool = False
LinkData = Union[NotTemplatedLinkData, TemplatedLinkData]
# GET /
class ServerInfo(BaseModel):
name: str = "Server name"
version: str = "Server version"
class GetRootResponse(BaseModel):
data: ServerInfo
links: Dict[str, LinkData] = Field(..., alias="_links",
description="Links to resources")
# GET /commands/{CID}/parameters
class ParameterInfo(BaseModel):
id: str
default: Any
class ParametersGroup(BaseModel):
group_id: str
parameters: List[ParameterInfo]
class GetCommandParametersResponse(BaseModel):
data: List[ParametersGroup]
links: Dict[str, LinkData] = Field(..., alias="_links")
# GET /commands/{CID}
class CommandData(BaseModel):
id: str
title: str
category: str
command: str
class CommandInfo(BaseModel):
data: CommandData
links: Dict[str, LinkData] = Field(..., alias="_links")
class FoundCommandInfo(CommandInfo):
embedded: Dict[str,
GetCommandParametersResponse] = Field(...,
alias="_embedded")
# GET /commands/
class GetCommandsResponse(BaseModel):
__root__: Dict[str, CommandInfo]
# class Config:
# @staticmethod
# def schema_extra(schema, model):
# example = {}
# for num in range(2):
# resp = ResponseStructure()
# resp.add_data(id=f"command_{num}",
# command=f"cl-command-{num}",
# title=f"Command {num}",
# category="cool-category"
# )
# resp.add_link("parameters",
# f"/commands/command_{num}/parameters")
# resp.add_link("configure", f"/configs/command_{num}")
# example[f"command_{num}"] = resp.get_dict()
# schema["example"] = example
# POST /configs/{command}
class CreateConfigResponse(BaseModel):
links: Dict[str, LinkData] = Field(..., alias="_links")
# PATCH /configs/{WID}/parameters
class ModifyConfigCorrectResponse(BaseModel):
links: Dict[str, LinkData] = Field(..., alias="_links")
class Config:
extra = "forbid"
class ModifyConfigUncorrectResponse(ModifyConfigCorrectResponse):
data: Optional[Dict[str, str]]
links: Dict[str, LinkData] = Field(..., alias="_links")
ModifyConfigResponse = Union[ModifyConfigCorrectResponse,
ModifyConfigUncorrectResponse]
# PUT /configs/{WID}/parameters
class ResetConfigResponse(BaseModel):
data: Optional[Dict[str, str]]
links: Dict[str, LinkData] = Field(..., alias="_links")
# POST /executions/{WID}
class CreateExecutionResponse(BaseModel):
links: Dict[str, LinkData] = Field(..., alias="_links")
# DELETE /executions/{WID}
class OutputData(BaseModel):
type: Literal["output"]
logging: Union[int, None]
text: str
source: str
class OutputMessage(BaseModel):
data: OutputData
class StopExecutionsResponse(BaseModel):
__root__: List[OutputMessage]
# DELETE /configs/{WID}
class StopConfigResponse(BaseModel):
__root__: List[OutputMessage]
# GET /executions/{WID}/output
class InputRequestData(BaseModel):
type: Literal["input"]
text: str
source: str
class InputMessage(BaseModel):
data: InputRequestData
links: Dict[str, LinkData] = Field(..., alias="_links")
class GetExecutionOutputResponse(BaseModel):
__root__: List[Union[OutputMessage, InputMessage]]
# GET /workers/
class WorkerData(BaseModel):
socket: str
command: str
pid: int
class WorkerInfo(BaseModel):
data: WorkerData
links: Dict[str, LinkData] = Field(..., alias="_links")
class GetWorkersResponse(BaseModel):
__root__: Dict[int, WorkerInfo]
# DELETE /workers/{WID}
# Сообщения протокола взаимодействия сервера и воркеров.
# class WorkerMessageBase(BaseModel):
# state: str
# status: int

@ -1,15 +1,33 @@
from fastapi import FastAPI, Depends
from starlette.requests import Request
from .server_data import ServerData
from .utils.dependencies import right_checkers
from .models.database import database
from .database.db import database
# from .utils.dependencies import right_checkers, get_current_user
from .utils.dependencies import get_current_user
from .utils.responses import (
ResponseStructure,
get_base_url,
validate_response,
)
from .utils.users import check_user_rights
from .schemas.responses import GetRootResponse
from .schemas.users import User
from .routers.users import router as users_router
from .routers.commands import router as commands_router
# TODO
# 1. Разобраться с описанием команд как ресурсов и со всем, что от них зависит.
# 2. Разобраться с объектами воркеров. И способом их функционирования.
# 1. Добавить список отклоняемых токенов и их обработку.
# 2. Добавить предварительный вариант генерации access_tokens.
# 3. Продумать, наконец, концепцию cid и wid, в ключе их применения при
# настройке команд и запуске воркеров.
# 4. Решить сколько команд может запускать пользователь единовременно.
# 5. Добавить способ указания принадлежности воркеров и команд пользователям.
# 6. Разобраться с местом раcположения базы данных.
data = ServerData()
@ -26,30 +44,39 @@ async def shutdown():
await database.disconnect()
@app.get("/", tags=["Root"],
dependencies=[Depends(right_checkers["read"])])
async def get_root() -> dict:
'''Обработчик корневых запросов.'''
return {'msg': 'root msg'}
@app.get("/", response_model=GetRootResponse, tags=["Home"])
async def get_primary_commands(request: Request,
user: User = Depends(get_current_user)):
check_user_rights(user, "read")
resp = ResponseStructure(get_base_url(request))
resp.add_data(name="Calculate Server", version="0.1")
resp.add_link("commands", "/commands?gui={is_gui}", templated=True)
resp.add_link("find_command", "/commands/{console_command}?gui={is_gui}",
templated=True)
resp.add_link("workers", "/workers")
return validate_response(resp.get_dict(), GetRootResponse,
media_type="application/hal+json")
@app.get("/workers/{wid}", tags=["Workers management"],
dependencies=[Depends(right_checkers["write"])])
async def get_worker(wid: int):
'''Тестовый обработчик.'''
worker = data.get_worker_object(wid=wid)
worker.run()
print(f"worker: {type(worker)} object {worker}")
# @app.get("/workers/{wid}", tags=["Workers management"],
# dependencies=[Depends(right_checkers["write"])])
# async def get_worker(wid: int):
# '''Тестовый обработчик.'''
# worker = data.get_worker_object(wid=wid)
# worker.run()
# print(f"worker: {type(worker)} object {worker}")
await worker.send({"text": "INFO"})
worker_data = await worker.get()
# await worker.send({"text": "INFO"})
# worker_data = await worker.get()
if worker_data['type'] == 'log':
data.log_message[data['level']](data['msg'])
return worker_data
# if worker_data['type'] == 'log':
# data.log_message[data['level']](data['msg'])
# return worker_data
# Authentification and users management.
app.include_router(users_router)
# Commands creation and management.
# Commands running and management.
app.include_router(commands_router)

@ -1,20 +1,25 @@
import os
import asyncio
import importlib
from typing import Dict, Optional
from typing import Dict, Optional, List, Union
from logging.config import dictConfig
from logging import getLogger, Logger
from ..variables.loader import Datavars
from ..commands.commands import Command, CommandRunner
from .utils.workers import Worker
from .utils.responses import ResponseStructure
# from .utils.workers import Worker
from .schemas.config import ConfigSchema
from calculate.utils.tools import Singleton
# Получаем конфигурацию сервера.
TESTING = bool(os.environ.get("TESTING", False))
if not TESTING:
from .config import config
server_config = ConfigSchema(**config)
@ -23,7 +28,7 @@ else:
server_config = ConfigSchema(**config)
class ServerData:
class ServerData(metaclass=Singleton):
def __init__(self, config: ConfigSchema = server_config):
self.event_loop = asyncio.get_event_loop()
@ -48,7 +53,8 @@ class ServerData:
self.commands_runners: Dict[str, CommandRunner] = {}
# Словарь WID и экземпляров процессов-воркеров, передаваемых воркерам.
self.workers: Dict[int, Worker] = {}
# TODO добавить менеджера воркеров.
# self.workers: Dict[int, Worker] = {}
@property
def datavars(self):
@ -87,18 +93,105 @@ class ServerData:
def make_command(self, command_id: str, ) -> int:
'''Метод для создания команды по ее описанию.'''
command_description = self.commands[command_id]
def _get_worker_object(self, wid: Optional[int] = None) -> Worker:
'''Метод для получения воркера для команды.'''
if wid is not None:
worker = Worker(wid, self._event_loop, self._datavars)
self._workers[wid] = worker
elif not self._workers:
worker = Worker(0, self._event_loop, self._datavars)
self._workers[0] = worker
# command_description = self.commands[command_id]
# def _get_worker_object(self, wid: Optional[int] = None) -> Worker:
# '''Метод для получения воркера для команды.'''
# if wid is not None:
# worker = Worker(wid, self._event_loop, self._datavars)
# self._workers[wid] = worker
# elif not self._workers:
# worker = Worker(0, self._event_loop, self._datavars)
# self._workers[0] = worker
# else:
# wid = max(self._workers.keys()) + 1
# worker = Worker(wid, self._event_loop, self._datavars)
# self._workers[wid] = worker
# return worker
class Commands:
'''Предварительная реализация контейнера описаний команд.'''
def __init__(self, *commands: List[Command]):
self._by_id: dict = dict()
self._by_command: dict = dict()
for command in commands:
self._by_id[command.id] = command
self._by_command[command.command] = command
def get_commands(self, base_url: str) -> Dict[str, dict]:
response = dict()
for command in self._by_id.values():
data = ResponseStructure(base_url)
data.add_data(id=command.id,
title=command.title,
category=command.category,
command=command.command)
data.add_link("self", f"/commands/{command.id}?by_id=true")
data.add_link("parameters", f"/commands/{command.id}/parameters")
data.add_link("configure", f"/configs/{command.id}")
response[command.id] = data.get_dict()
return response
def get_by_id(self, command_id: str, base_url: str) -> Dict[str, Dict]:
if command_id in self._by_id:
command = self._by_id[command_id]
data = ResponseStructure(base_url)
data.add_data(id=command.id,
title=command.title,
category=command.category,
command=command.command)
data.add_link("self", f"/commands/{command.id}?by_id=1")
data.add_link("parameters", f"/commands/{command.id}/parameters")
data.add_link("configure", f"/configs/{command.id}")
parameters_data = self._get_parameters_data(command.parameters,
command.id, base_url)
data.embed("parameters", parameters_data)
return data.get_dict()
return None
def find_command(self, console_command: str,
base_url: str) -> Union[dict, None]:
if console_command in self._by_command:
command = self._by_command[console_command]
data = ResponseStructure(base_url)
data.add_data(id=command.id,
title=command.title,
category=command.category,
command=command.command)
data.add_link("self", f"/commands/{command.id}?by_id=1")
data.add_link("parameters", f"/commands/{command.id}/parameters")
data.add_link("configure", f"/configs/{command.id}")
parameters_data = self._get_parameters_data(command.parameters,
command.id, base_url)
data.embed("parameters", parameters_data)
return data.get_dict()
else:
return None
def get_parameters(self, command_id: str, base_url: str
) -> Union[dict, None]:
if command_id in self._by_id:
command = self._by_id[command_id]
parameters_data = self._get_parameters_data(command.parameters,
command.id, base_url)
return parameters_data
else:
wid = max(self._workers.keys()) + 1
worker = Worker(wid, self._event_loop, self._datavars)
self._workers[wid] = worker
return worker
return None
def get_command_object(self, command_id: str) -> Command:
if command_id in self._by_id:
return self._by_id[command_id]
return None
def _get_parameters_data(self, parameters: list, command_id: str,
base_url: str) -> List[dict]:
data = ResponseStructure(base_url)
data.add_data(parameters)
data.add_link("self", f"/commands/{command_id}/parameters")
return data.get_dict()

@ -14,7 +14,7 @@ from .users import get_user_by_username
from calculate.utils.files import Process
def make_secret_key():
def make_secret_key() -> str:
openssl_process = Process("/usr/bin/openssl", "rand", "-hex", "32")
secret_key = openssl_process.read()
return secret_key.strip()

@ -0,0 +1,5 @@
from multiprocessing.connection import Listener
def daemon(listener: Listener, wid: int, base_dir: str, command):
pass

@ -0,0 +1,122 @@
from fastapi import HTTPException, status
from fastapi.encoders import jsonable_encoder
from starlette.responses import JSONResponse
from starlette.requests import Request
from pydantic.main import ModelMetaclass
from pydantic import parse_obj_as
from typing import Dict, Union, _GenericAlias, Optional
class ResponseStructure:
'''Класс структуры данных, для получения HATEOAS ответа на запросы
клиента.'''
def __init__(self, base_url: str):
self._base_url: str = base_url.strip("/")
self._data: dict = dict()
self._links: Dict[str, Dict[str, str]] = dict()
self._embedded: Dict[str, dict] = dict()
def get_dict(self) -> dict:
dictionary = dict()
if self._data:
dictionary["data"] = self._data
if self._links:
dictionary["_links"] = self._links
if self._embedded:
dictionary["_embedded"] = self._embedded
return dictionary
def add_data(self, *args, **kwargs) -> "ResponseStructure":
if args:
if isinstance(self._data, dict):
if len(args) > 1:
self._data = list(args)
else:
self._data = args[0]
elif isinstance(self._data, list):
if len(args) > 1:
self._data.extend(args)
else:
self._data.append(args[0])
else:
self._data = [self._data, *args]
elif kwargs:
if not isinstance(self._data, dict):
self._data = {}
self._data.update(kwargs)
return self
def embed(self, resource: str,
content: Union[dict, list]) -> "ResponseStructure":
self._embedded[resource] = content
return self
def add_link(self, action: str, uri: str,
templated: bool = False) -> "ResponseStructure":
uri = self._get_uri(uri)
if action in self._links:
link_section = self._links[action]
if link_section["href"] != uri:
link_section["href"] = uri
else:
link_section = {"href": uri}
self._links[action] = link_section
if templated:
print("TEMPLATED:", action)
link_section["templated"] = templated
return self
def _get_uri(self, uri: str) -> str:
if uri.startswith(self._base_url):
return uri
return f"{self._base_url}/{uri.strip('/')}"
def get_base_url(request: Request) -> str:
base_url = f"{request.base_url.scheme}://{request.base_url.netloc}"
return base_url
def get_command_not_found(command_id: str) -> HTTPException:
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=(f'Command with id "{command_id}"'
' is not found.'))
def get_cl_command_not_found(console_command: str) -> HTTPException:
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=(f'Console command "{console_command}" is not'
' found.'))
def get_configuration_not_found(wid: int) -> HTTPException:
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Configuration with id={wid} is not found.")
def validate_response(data: Union[dict, list],
schema: Union[ModelMetaclass, _GenericAlias, type],
media_type: Optional[str] = None,
status_code: int = 200
) -> JSONResponse:
"""Функция для валидации данных ответа сервера по указанной схеме с учетом
наличия псевдонимов полей и возможности того, что схема задана обычным
типом, типом из typing или BaseModel с __root__. Возвращает объект ответа.
"""
if isinstance(schema, ModelMetaclass):
if "__root__" in schema.__annotations__:
validated = schema(__root__=data)
else:
validated = schema(**data)
elif isinstance(schema, (_GenericAlias, type)):
validated = parse_obj_as(schema, data)
return JSONResponse(content=jsonable_encoder(validated, by_alias=True),
media_type=media_type, status_code=status_code)

@ -1,30 +1,31 @@
from fastapi import HTTPException, status
from sqlalchemy import func, and_
from typing import List
from ..models.database import database
from ..models.users import users_table, users_rights, rights_table
from ..database.db import database
from ..database.users import users_table as ut, users_rights, rights_table
from ..schemas.users import UserCreate
from ..schemas.users import UserCreate, User
async def get_user_by_username(username: str):
'''Метод для получения строки с данными пользователя из базы данных по
username.'''
query = users_table.select().where(users_table.c.login == username)
query = ut.select().where(ut.c.login == username)
user_data = await database.fetch_one(query)
query = (users_table.
query = (ut.
join(users_rights).
join(rights_table).
select().
where(and_(users_table.c.id == users_rights.c.user_id,
users_table.c.login == username,
where(and_(ut.c.id == users_rights.c.user_id,
ut.c.login == username,
rights_table.c.id == users_rights.c.right_id)).
with_only_columns([users_table.c.id,
users_table.c.login,
users_table.c.password,
with_only_columns([ut.c.id,
ut.c.login,
ut.c.password,
func.group_concat(rights_table.c.name,
' ').label("rights")]).
group_by(users_table.c.id))
group_by(ut.c.id))
response = await database.fetch_one(query)
user_data = dict(response)
@ -33,6 +34,16 @@ async def get_user_by_username(username: str):
async def create_user(username: str, hashed_password: str, rights: List[str]):
user = UserCreate(login=username,
password=hashed_password,
rights=rights)
UserCreate(login=username,
password=hashed_password,
rights=rights)
def check_user_rights(user_data: User, *rights: List[str]) -> None:
user_rights = user_data.rights
for right in user_rights:
if right not in user_rights:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Not enough permissions',
headers={"WWW-Authenticate": "Bearer"}
)

@ -1,12 +1,415 @@
import os
import sys
import json
import socket
import struct
import signal
import asyncio
import logging
from typing import Union
from calculate.variables.loader import Datavars
from multiprocessing import Queue, Process
from calculate.commands.commands import CommandRunner, Command
# from time import sleep
from multiprocessing import Process
from multiprocessing.connection import Listener, Connection
# import logging
# import time
from .daemon import daemon
# from commands import Command
from typing import Dict, Optional, List, Union, Tuple, Literal
from asyncio.unix_events import _UnixSelectorEventLoop
from asyncio import get_event_loop
LOG_LEVELS = {logging.ERROR: "ERROR",
logging.INFO: "INFO",
logging.WARNING: "WARNING",
logging.DEBUG: "DEBUG",
logging.CRITICAL: "CRITICAL",
}
class WorkerException(Exception):
pass
class UnexpectedWorkerFinish(WorkerException):
def __init__(self, reason: List[str], output: Optional[List[str]] = []):
"""Исключение кидаемое в случае, если воркер неожиданно умер."""
self.reason: List[str] = reason
self.output: List[dict] = output
class UnexpectedWorkerState(WorkerException):
"""Исключение кидаемое в случае, если состояние воркера не соответствует
ожидаемому."""
def __init__(self, state: str, expected: str):
self.state: str = state
self.expected: str = expected
class DaemonIsDead(WorkerException):
"""Исключение кидаемое в случае, если демон-воркер неожиданно оказался
мертв."""
def __init__(self, info: str, output: Optional[List[str]] = []):
self.info: str = info
self.output: List[dict] = output
class IncorrectConfiguration(WorkerException):
"""Исключение кидаемое при попытке запустить выполнение конфигурации, в
которой есть ошибки."""
def __init__(self, errors: Dict[str, str]):
self._errors = errors
class WorkersManager:
def __init__(self, loop: Optional[_UnixSelectorEventLoop] = None):
if loop is None:
self._loop = get_event_loop()
else:
self._loop = loop
self._configs: Dict[int, WorkerProtocol] = {}
self._execs: Dict[int, WorkerProtocol] = {}
self._wids: List[int] = []
async def make_worker(self, command) -> Tuple[Union[int, None],
Union[str, None]]:
"""Метод для создания воркеров."""
wid = self._get_wid()
try:
# Создаем воркера и получаем интерфейс для взаимодействия с ним.
worker_api = await self._create_worker(wid, command)
self._configs[wid] = worker_api
except Exception as error:
if wid in self._configs:
self._configs.pop(wid)
return None, str(error)
return wid, None
async def configure_worker(self, wid: int, parameters: List[dict],
reset: bool = False) -> Union[dict, None]:
"""Метод для модификации или сброса параметров конфигурации указанными
значениями.
"""
if not parameters:
return {}
if wid not in self._configs:
return None
worker_api: WorkerProtocol = self._configs[wid]
try:
result = await worker_api.configure(parameters)
except (UnexpectedWorkerFinish, DaemonIsDead):
worker_api.kill()
self._configs.pop(wid)
raise
return result
async def cancel_worker_configuration(self, wid: int):
"""Метод для завершения конфигурации воркера и завершения его работы.
"""
if wid not in self._configs:
return None
worker_api: WorkerProtocol = self._configs[wid]
output = await worker_api.cancel_configuration()
worker_api.delete_socket()
self._configs.pop(wid)
return output
def clean(self):
"""Метод для убийства всех работающих на данный момент демонов."""
# TODO доработать его логику так, чтобы при перезапуске сервера этот
# метод никого не убивал.
for wid, worker in {**self._configs, **self._execs}.items():
result = False
try:
result = worker.kill()
finally:
if result:
print(f"[*] Worker {wid} is killed.")
else:
print(f"[x] Worker {wid} is not killed.")
continue
def get_config(self, wid: int):
if wid in self._configs:
return self._configs[wid]
return None
def get_execution(self, wid: int):
if wid in self._execs:
return self._execs[wid]
return None
def run_execution(self, wid: int) -> Tuple[int, Union[dict, None]]:
'''Метод для запуска исполнения команды, используя указанную
конфигурацию.
Вывод:
0, None -- исполнение успешно запущено;
1, None -- конфигурация с данным wid не найдена;
2, errors -- конфигурация с данным wid содержит ошибки;
3, None -- исполнение с данным wid уже существует;
'''
if wid not in self._configs:
return 1, None
if self._configs[wid].status > 0:
# Берем ошибки, как-то.
errors = {}
return 2, errors
if wid in self._execs:
return 3, None
# worker = self._configs.pop(wid)
return 0, None
async def _create_worker(self, wid: int, command) -> "WorkerProtocol":
# Путь к временному файлу, в котором будет лежать pid демона-рабочего.
pid_file_path: str = os.path.join(os.getcwd(), f"pid_file_{wid}")
socket_path: str = os.path.join(os.getcwd(),
f"worker_{wid}.sock")
if os.path.exists(socket_path):
os.remove(socket_path)
process_runner = Process(target=daemonize_worker,
args=(socket_path, wid, pid_file_path,
command)
)
process_runner.start()
process_runner.join()
daemon_pid = await self._get_daemon_pid(pid_file_path)
self._delete_pid_file(pid_file_path)
transport, worker_interface = await self._loop.create_unix_connection(
lambda: WorkerProtocol(daemon_pid,
socket_path),
socket_path)
return worker_interface
def _get_wid(self):
if not self._wids:
worker_id = 0
else:
worker_id = self._wids[-1] + 1
self._wids.append(worker_id)
return worker_id
async def _get_daemon_pid(self, pid_file_path: str) -> int:
while True:
if not os.path.exists(pid_file_path):
# Если файла нет -- уступаем работу другим воркерам.
await asyncio.sleep(0)
with open(pid_file_path, "r") as pid_file:
daemon_pid = int(pid_file.readline())
return daemon_pid
def _delete_pid_file(self, pid_file_path: str) -> None:
try:
os.remove(pid_file_path)
except OSError:
pass
def __repr__(self):
configs = ["Configurations:"]
configs.extend([f"wid={wid}" for wid in self._configs.keys()])
configs_repr = "\n\t".join(configs)
execs = ["Executions:"]
execs.extend([f"wid={wid}" for wid in self._execs.keys()])
execs_repr = "\n\t".join(execs)
return f"{configs_repr}\n{execs_repr}"
def daemonize_worker(socket_path: str, wid: int, pid_file: str,
command) -> None:
'''Функция запускаемая в отдельном процессе и порождающая демона при помощи
магии двойного форка.'''
base_dir = os.getcwd()
new_fork()
os.chdir("/")
os.setsid()
os.umask(0)
new_fork()
# Создаем файл, с помощью которого будем передавать pid демона-рабочего.
daemon_pid = str(os.getpid())
with open(pid_file, "w") as pid_file:
pid_file.write(daemon_pid)
with Listener(socket_path) as listener:
daemon(listener, wid, base_dir, command)
def new_fork() -> None:
'''Функция для создания форка текущего процесса.'''
try:
middle_pid = os.fork()
if middle_pid > 0:
sys.exit(0)
except OSError as error:
print(f"FORK ERROR: {error.errno} - {str(error)}")
sys.exit(1)
class WorkerProtocol(asyncio.Protocol):
"""Класс протокола управления воркером."""
def __init__(self, pid: int, socket_path: str):
self.loop = asyncio.get_running_loop()
self._pid: int = pid
self._socket_path: str = socket_path
# Объект Event, предназначенный для остановки записи при переполнении
# буффера записи в трспорте.
self._can_write = asyncio.Event()
self._can_write.clear()
self._msg_queue = asyncio.Queue()
# Контекст чтения данных воркера.
self._buffer = bytearray()
self._msg_size: int = 0
self._connection_closed: bool = True
async def configure(self, parameters: List[dict],
reset: bool = False) -> list:
"""Метод для конфигурации воркера указанными значениями параметров.
Возвращает список ошибок, если таковые есть, или пустой список, если
никаких ошибок нет.
"""
message = self._make_worker_message("config", int(reset), parameters)
await self._write(message)
response = await self._read()
response = json.loads(response)
return response["data"]
async def cancel_configuration(self) -> List[dict]:
"""Метод для остановки конфигурации воркера и его работы в целом."""
stop_message: bytes = self._make_worker_message("finish", 1, None)
await self._write(stop_message)
response = await self._read()
response = json.loads(response)
return response["data"]
def connection_made(self, transport: asyncio.Transport) -> None:
self.transport = transport
self._can_write.set()
self._connection_closed: bool = False
print("Connection made.")
def connection_lost(self, exc: BaseException) -> None:
if exc:
print("Connection lost. Error:", str(exc))
else:
print("Connection lost.")
self._connection_closed: bool = True
def data_received(self, data: bytes) -> None:
self._buffer.extend(data)
while True:
if not self._msg_size:
if len(self._buffer) < 4:
return
size_length = struct.calcsize(">i")
self._msg_size, = struct.unpack(">i",
self._buffer[:size_length])
self._buffer = self._buffer[size_length:]
if len(self._buffer) >= self._msg_size:
data = self._buffer[:self._msg_size]
self._buffer = self._buffer[self._msg_size:]
self._msg_size = 0
self._msg_queue.put_nowait(data.decode())
else:
break
def pause_writing(self) -> None:
self._can_write.clear()
def resume_writing(self) -> None:
self._can_write.set()
async def _read(self) -> str:
if self._connection_closed and self._msg_queue.empty():
return None
return await self._msg_queue.get()
async def read_all(self) -> List[str]:
if self._msg_queue.empty():
if self._connection_closed:
return None
else:
return await self._msg_queue.get()
output = []
while not self._msg_queue.empty():
output.append(self._msg_queue.get_nowait())
return output
async def _write(self, data: Union[str, bytes]) -> None:
await self._can_write.wait()
if isinstance(data, str):
data = data.encode()
self.transport.write(struct.pack(">i", len(data)))
self.transport.write(data)
def _make_worker_message(
self,
state: Literal["config", "finish", "exec", "input"],
status: int, data: Union[list, dict]) -> dict:
return json.dumps({"state": state,
"status": status,
"data": data}).encode()
def delete_socket(self) -> None:
"""Метод для удаления сокета."""
try:
os.remove(self._socket_path)
self.transport.close()
self._connection_closed = True
except OSError:
pass
def kill(self) -> bool:
"""Метод для убийства демона."""
print("GONNA KILLA DAEMON:", self._pid)
if self.is_daemon_alive():
try:
os.kill(self._pid, signal.SIGKILL)
except OSError:
return False
else:
print("ALREADY DEAD")
return True
def stop(self) -> bool:
print("STOP DAEMON:", self._daemon_pid)
if self.is_daemon_alive():
try:
# Для остановки демона посылаем в него KeyboardInterrupt.
os.kill(self._pid, signal.SIGINT)
except OSError as error:
print(f"ERROR: Failed stopping daemon PID={self._pid}. "
f"Reason: {str(error)}")
return False
return True
def is_daemon_alive(self) -> bool:
try:
os.kill(self._pid, 0)
return True
except OSError:
return False
class WorkerIOError(KeyboardInterrupt):
@ -15,24 +418,24 @@ class WorkerIOError(KeyboardInterrupt):
class IOModule:
'''Класс модуля ввода/вывода для воркеров.'''
def __init__(self, socket_path: str):
self._sock = socket.socket(socket.AF_UNIX,
socket.SOCK_STREAM)
self._sock.bind(socket_path)
self._sock.listen()
self._connection = None
def __init__(self, listener: Listener, command_name: str):
self._listener: Listener = listener
self._connection: Union[Connection, None] = None
def input(self, msg: str) -> str:
'''Метод через который возможен ввод данных в скрипт.'''
input_request = json.dumps({"type": "input",
"msg": msg}) + '\0'
answer = None
while answer is None:
if not self._check_connection(self._connection):
self._connection = self._make_connection()
self._connection.sendall(input_request.encode())
answer = self._get()
return answer['data']
self._command: str = command_name
self._script: str = ""
@property
def command(self) -> str:
return self._command
@property
def script(self) -> str:
return self._script
@script.setter
def script(self, script: str) -> None:
self._script = script
def set_debug(self, msg: str) -> None:
self.output(msg, level=logging.DEBUG)
@ -49,142 +452,57 @@ class IOModule:
def set_critical(self, msg: str) -> None:
self.output(msg, level=logging.CRITICAL)
def output(self, msg: str, level: int = logging.INFO) -> None:
def output(self, text: str, level: int = logging.INFO) -> None:
'''Метод для отправки серверу вывода с указанием уровня.'''
if not self._check_connection(self._connection):
self._connection = self._make_connection()
output_request = json.dumps({"type": "output",
"level": level,
"msg": msg}) + '\0'
self._connection.sendall(output_request.encode())
output_request = {"state": "exec",
"status": 0,
"data": {
"type": "message",
"logging": level,
"text": text,
"source": self._script
}
}
self.send(output_request)
def input(self, msg: str) -> str:
'''Метод через который возможен ввод данных в скрипт.'''
input_request = {"state": "input",
"status": 0,
"data": {"text": msg,
"source": f"{self.command}:{self.script}"}}
answer = None
while answer is None:
self.send(input_request)
answer = self.receive()
return answer['data']
def send(self, data: dict) -> None:
'''Метод для отправки данных серверу.'''
if not self._check_connection(self._connection):
self._connection = self._make_connection()
data = json.dumps(data) + '\0'
self._connection.sendall(data.encode())
if self._connection is None:
raise WorkerIOError("No one is connected now.")
data = json.dumps(data).encode()
self._connection.send_bytes(data)
def receive(self) -> dict:
'''Метод для получения данных от сервера.'''
data = None
while data is None:
if not self._check_connection(self._connection):
self._connection = self._make_connection()
data = self._get()
return data
def _get(self) -> Union[None, dict]:
'''Метод для считывания данных, возможно, поделенных на кадры.'''
try:
data = b''
while True:
chunk = self._connection.recv(1024)
if not chunk:
return None
data += chunk
if not data.endswith(b'\0'):
if b'\0' in data:
# Если после символа конца сообщения есть еще какие-то
# данные -- считаем их наличие ошибкой.
raise WorkerIOError("Unexpected message.")
continue
return json.loads(data[:-1].decode())
except ConnectionResetError:
return None
def _make_connection(self) -> socket.socket:
'''Метод для создания подключения.'''
connection, parent_address = self._sock.accept()
return connection
if self._connection is None:
raise WorkerIOError("No one is connected now.")
data: bytes = self._connection.recv_bytes()
return json.loads(data.decode())
def _check_connection(self, connection: socket.socket) -> bool:
'''Метод для проверки соединения путем отправки на сокет пустого
сообщения.'''
if connection is None:
return False
def __enter__(self):
self._connection = self._listener.accept()
return self
try:
connection.sendall(b'')
return True
except BrokenPipeError:
return False
def __del__(self) -> None:
if self._connection is not None:
def __exit__(self, exc_type, exc_value, exc_traceback):
if exc_type:
print("exc_type =", exc_type)
print("exc_value =", exc_value)
print("exc_traceback =", exc_traceback)
if exc_type is KeyboardInterrupt:
print("correctly")
if not self._connection.closed:
self._connection.close()
self._sock.close()
class Worker:
def __init__(self, wid: int, command: Command, datavars: Datavars):
self._wid: int = wid
self._datavars: Datavars = datavars
self._socket_path: str = f"./worker_{self._wid}.sock"
if os.path.exists(self._socket_path):
os.remove(self._socket_path)
self._input_queue: list = []
def run(self):
io = IOModule(self._socket_path)
title = io.receive()
print(title['msg'])
while True:
msg = "What is your desire?"
print(">", msg)
answer = io.input(msg)
print(f'> {answer}')
if answer == "stop":
break
elif answer == "output":
msg = "What kind of output you wanna see?"
print(">", msg)
answer = io.input(msg)
io.set_info(answer)
else:
msg = "I am sorry, but I could not help you("
print(">", msg)
io.output(msg)
print('STOPPED')
def initialize(self, io: IOModule):
pass
def run_command(self):
pass
class DeprecatedWorker:
def __init__(self, wid, loop, sockets_path: str = 'calculate/server/'):
self._wid = wid
self._event_loop = loop
# Создаем сокет для взаимодействия воркера и сервера.
socket_path = os.path.join(sockets_path, f'worker_{self._wid}')
if os.path.exists(socket_path):
os.remove(socket_path)
self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self._socket.bind(socket_path)
@staticmethod
def _get_output(output_queue: Queue) -> dict:
return output_queue.get()
@staticmethod
def _main_loop(command, in_queue: Queue, out_queue: Queue):
data = in_queue.get()
print('\nworker for command:', command)
output = {"type": "log",
"level": "INFO",
"msg": f"recieved message {data['text']}"}
out_queue.put(output)
def run(self, command):
'''Метод для запуска процесса воркера с заданным '''
worker_process = Process(target=self._main_loop,
args=(command,
self._in_queue,
self._output_queue))
worker_process.start()

@ -6,7 +6,7 @@ from ...utils.images import ImageMagick, ImageMagickError
from ...variables.loader import Datavars
from .base_format import Format, FormatError
from typing import Union, List, Tuple, NoReturn
from typing import Union, List, Tuple
import hashlib
import re
import os
@ -296,7 +296,7 @@ class BackgroundsFormat(Format):
return md5_object.hexdigest()
def _create_md5sum_file(self, target_path: str, action_md5sum: str
) -> NoReturn:
) -> None:
"""Метод для создания файла с md5-суммой действия, выполненного
данным шаблоном."""
if not self._empty_name:

@ -23,6 +23,8 @@ class Format:
CALCULATE_VERSION: Union[str, None] = None
SHEBANG_PATTERN: str = r"^(?P<shebang>#!\s*[\w\d\/]+\n)"
comment_symbol = None
def __init__(self, processing_methods: List[Callable]):
self._processing_methods: List[Callable] = processing_methods
self._document_dictionary: OrderedDict = OrderedDict()

@ -1,13 +1,27 @@
# vim: fileencoding=utf-8
#
# ToDo: добавить проверку того, полностью ли парсился документ. Если отпарсился
# TODO: добавить проверку того, полностью ли парсился документ. Если отпарсился
# не весь файл -- выдаем ошибку.
#
from .base_format import Format
from collections import OrderedDict
from pyparsing import originalTextFor, OneOrMore, Word, alphanums, Literal,\
ZeroOrMore, Forward, Optional, Group, restOfLine,\
cppStyleComment, Keyword, printables, nums, SkipTo
from pyparsing import (
originalTextFor,
OneOrMore,
Word,
alphanums,
Literal,
ZeroOrMore,
Forward,
Optional,
Group,
restOfLine,
cppStyleComment,
Keyword,
printables,
nums,
SkipTo
)
class BINDFormat(Format):

@ -1,6 +1,6 @@
# vim: fileencoding=utf-8
#
# ToDo: написать счетчик скобок для финальной оценки корректности
# TODO: написать счетчик скобок для финальной оценки корректности
# документа.
#
from .base_format import Format

@ -1,11 +1,20 @@
# vim: fileencoding=utf-8
#
from .base_format import Format
from ..template_engine import ParametersContainer
from collections import OrderedDict
from pyparsing import originalTextFor, Literal, ZeroOrMore, Word, printables,\
OneOrMore, alphanums, ParseException, restOfLine,\
Group, Optional, Regex
from pyparsing import (
originalTextFor,
Literal,
ZeroOrMore,
Word,
printables,
OneOrMore,
alphanums,
ParseException,
restOfLine,
Group,
Optional
)
class KDEFormat(Format):

@ -1,11 +1,18 @@
# vim: fileencoding=utf-8
#
from .base_format import Format
from ..template_engine import ParametersContainer
from collections import OrderedDict
from pyparsing import Word, Literal, alphanums, originalTextFor,\
OneOrMore, ParseException, restOfLine, Group, Optional,\
Regex
from pyparsing import (
Word,
Literal,
alphanums,
originalTextFor,
ParseException,
restOfLine,
Group,
Optional,
Regex
)
class KernelFormat(Format):

@ -2,7 +2,6 @@
#
from .base_format import Format, FormatError
from calculate.utils.files import Process
from typing import NoReturn
from os import path
@ -25,7 +24,7 @@ class PatchFormat(Format):
# Предупреждения.
self._warnings: list = []
def _init_command(self) -> NoReturn:
def _init_command(self) -> None:
self._patch_cmd = "/usr/bin/patch"
if not path.exists(self._patch_cmd):
self._patch_cmd = None

@ -2,9 +2,19 @@
#
from .base_format import Format
from collections import OrderedDict
from pyparsing import Word, Literal, alphanums, printables, originalTextFor,\
OneOrMore, ParseException, restOfLine, Group, Optional,\
Regex
from pyparsing import (
Word,
Literal,
alphanums,
printables,
originalTextFor,
OneOrMore,
ParseException,
restOfLine,
Group,
Optional,
Regex
)
class ProcmailFormat(Format):

@ -1,7 +1,6 @@
# vim: fileencoding=utf-8
#
from .base_format import Format
from ..template_engine import ParametersContainer
from collections import OrderedDict
from pyparsing import originalTextFor, Literal, Word, printables, OneOrMore,\
alphanums, ParseException, Optional, Group, restOfLine
@ -26,7 +25,6 @@ class SambaFormat(Format):
join_before=False,
add_header=False,
already_changed=False,
parameters=ParametersContainer(),
**kwargs):
processing_methods = [self._parse_comment_line,
self._parse_section_line,

@ -4,17 +4,19 @@ from jinja2.ext import Extension
from jinja2.lexer import Token
from jinja2.parser import Parser
from jinja2 import (
nodes,
Template,
Environment,
contextfunction,
FileSystemLoader,
TemplateSyntaxError,
nodes,
contextfunction,
Template,
)
from jinja2.utils import missing
from jinja2.runtime import Context, Undefined
from collections.abc import MutableMapping
from collections import OrderedDict
from importlib import import_module
from pprint import pprint
import copy
@ -22,37 +24,38 @@ import re
import os
import stat
from typing import (
Optional,
Union,
Any,
List,
Any,
Dict,
Tuple,
NoReturn,
Optional,
Iterator,
)
from ..utils.package import (
PackageAtomName,
PackageAtomParser,
PackageAtomError,
Package,
PackageAtomName,
NOTEXIST,
Version
Package,
Version,
)
from ..utils.files import (
join_paths,
check_directory_link,
check_command,
FilesError
join_paths,
FilesError,
)
from calculate.variables.datavars import (
VariableNotFoundError,
HashType,
NamespaceNode,
VariableNode,
IniType,
IntegerType,
FloatType,
ListType
ListType,
HashType,
IniType,
)
from calculate.utils.fs import readFile
from calculate.variables.loader import Datavars
@ -116,10 +119,10 @@ class Variables(MutableMapping):
def __getitem__(self, name: str) -> Any:
return self.__attrs[name]
def __setitem__(self, name: str, value: Any) -> NoReturn:
def __setitem__(self, name: str, value: Any) -> None:
self.__attrs[name] = value
def __delitem__(self, name: str) -> NoReturn:
def __delitem__(self, name: str) -> None:
del self.__attrs[name]
def __iter__(self) -> Iterator:
@ -254,7 +257,7 @@ class ParametersProcessor:
def set_parameters_container(self,
parameters_container: "ParametersContainer"
) -> NoReturn:
) -> None:
'''Метод для установки текущего контейнера параметров.'''
self._parameters_container = parameters_container
self._added_parameters = set()
@ -264,7 +267,7 @@ class ParametersProcessor:
return self._for_package
@for_package.setter
def for_package(self, package: Package) -> NoReturn:
def for_package(self, package: Package) -> None:
self._for_package = package
def __getattr__(self, parameter_name: str) -> Any:
@ -278,7 +281,7 @@ class ParametersProcessor:
def check_template_parameter(self, parameter_name: str,
parameter_value: Any,
template_type: int, lineno: int) -> NoReturn:
template_type: int, lineno: int) -> None:
'''Метод, проверяющий указанный параметр.'''
self.lineno = lineno
self.template_type = template_type
@ -313,7 +316,7 @@ class ParametersProcessor:
self._parameters_container.set_parameter({parameter_name:
checked_value})
def check_postparse_parameters(self) -> NoReturn:
def check_postparse_parameters(self) -> None:
'''Метод, запускающий проверку параметров после их разбора.'''
for parameter, parameter_checker in\
self.postparse_checkers_list.items():
@ -326,7 +329,7 @@ class ParametersProcessor:
result)
def check_template_parameters(self, parameters: dict,
template_type: int, lineno: int) -> NoReturn:
template_type: int, lineno: int) -> None:
'''Метод, запускающий проверку указанных параметров.'''
self.template_type = template_type
self.lineno = lineno
@ -638,7 +641,7 @@ class ParametersProcessor:
return parameter_value
# Методы для проверки параметров после разбора всего шаблона.
def check_postparse_append(self, parameter_value: str) -> NoReturn:
def check_postparse_append(self, parameter_value: str) -> None:
if parameter_value == 'link':
if 'source' not in self._parameters_container:
raise IncorrectParameter("append = 'link' without source "
@ -652,7 +655,7 @@ class ParametersProcessor:
raise IncorrectParameter("'append' parameter is not 'compatible' "
"with the 'exec' parameter")
def check_postparse_run(self, parameter_value: str) -> NoReturn:
def check_postparse_run(self, parameter_value: str) -> None:
if self._parameters_container.append:
raise IncorrectParameter("'run' parameter is not 'compatible' "
"with the 'append' parameter")
@ -661,7 +664,7 @@ class ParametersProcessor:
raise IncorrectParameter("'run' parameter is not 'compatible' "
"with the 'exec' parameter")
def check_postparse_exec(self, parameter_value: str) -> NoReturn:
def check_postparse_exec(self, parameter_value: str) -> None:
if self._parameters_container.append:
raise IncorrectParameter("'exec' parameter is not 'compatible' "
"with the 'append' parameter")
@ -672,7 +675,7 @@ class ParametersProcessor:
def check_postparse_source(self,
parameter_value: Union[str, Tuple[bool, str]]
) -> NoReturn:
) -> None:
# Если файл по пути source не существует, но присутствует параметр
# mirror -- пропускаем шаблон для того, чтобы целевой файл мог быть
# удален в исполнительном модуле.
@ -692,12 +695,12 @@ class ParametersProcessor:
"append = 'link' for directory template")
)
def check_postparse_autoupdate(self, parameter_value: bool) -> NoReturn:
def check_postparse_autoupdate(self, parameter_value: bool) -> None:
if self._parameters_container.unbound:
raise IncorrectParameter("'unbound' parameter is incompatible"
" with 'autoupdate' parameter")
def check_postparse_handler(self, parameter_value: bool) -> NoReturn:
def check_postparse_handler(self, parameter_value: bool) -> None:
if self._parameters_container.merge:
raise IncorrectParameter("'merge' parameter is not available"
" in handler templates")
@ -706,7 +709,7 @@ class ParametersProcessor:
raise IncorrectParameter("'package' parameter is not available"
" in handler templates")
def check_postparse_package(self, parameter_value: str) -> NoReturn:
def check_postparse_package(self, parameter_value: str) -> None:
groups = []
package_atom = PackageAtomParser.parse_atom_name(parameter_value)
@ -760,19 +763,19 @@ class ParametersProcessor:
return True
return False
def check_postparse_convert(self, parameter_value: str) -> NoReturn:
def check_postparse_convert(self, parameter_value: str) -> None:
template_format = self._parameters_container.format
if not template_format or template_format != "backgrounds":
raise IncorrectParameter("'convert' parameter available for"
" 'backgrounds' format only.")
def check_postparse_stretch(self, parameter_value: str) -> NoReturn:
def check_postparse_stretch(self, parameter_value: str) -> None:
template_format = self._parameters_container.format
if not template_format or template_format != "backgrounds":
raise IncorrectParameter("'stretch' parameter available for"
" 'backgrounds' format only.")
def check_postparse_execsql(self, parameter_value: str) -> NoReturn:
def check_postparse_execsql(self, parameter_value: str) -> None:
template_format = self._parameters_container.format
if not template_format or template_format != "sqlite":
raise IncorrectParameter("'execsql' parameter available for"
@ -876,7 +879,7 @@ class ParametersProcessor:
format(group_file_path))
@classmethod
def _inspect_formats_package(cls) -> NoReturn:
def _inspect_formats_package(cls) -> None:
'''Метод для определения множества доступных форматов и
предоставляемых ими параметров.'''
if cls.format_is_inspected:
@ -901,6 +904,7 @@ class ParametersProcessor:
module = import_module('calculate.templates.format.{}'.
format(module_name))
for obj in dir(module):
# TODO изменить способ выборки классов форматов.
if obj.endswith('Format') and obj != 'BaseFormat':
format_class = getattr(module, obj, False)
@ -954,6 +958,11 @@ class CalculateContext(Context):
сохранять их.'''
_env_set = set()
def __init__(self, environment: Environment,
parent: Union[Context, Dict[str, Any]],
name: str, blocks: Dict[str, Any], **kwargs):
super().__init__(environment, parent, name, blocks, **kwargs)
def resolve(self, key: str) -> Any:
if self._legacy_resolve_mode:
rv = resolve_or_missing(self, key,
@ -987,19 +996,19 @@ class ParametersContainer(MutableMapping):
else:
self.__inheritable: dict = {}
def set_parameter(self, item_to_add: dict) -> NoReturn:
def set_parameter(self, item_to_add: dict) -> None:
self.__parameters.update(item_to_add)
def set_inheritable(self, item_to_add: dict) -> NoReturn:
def set_inheritable(self, item_to_add: dict) -> None:
self.__inheritable.update(item_to_add)
def get_inheritables(self) -> "ParametersContainer":
return ParametersContainer(copy.deepcopy(self.__inheritable))
def remove_not_inheritable(self) -> NoReturn:
def remove_not_inheritable(self) -> None:
self.__parameters.clear()
def print_parameters_for_debug(self) -> NoReturn:
def print_parameters_for_debug(self) -> None:
print('Parameters:')
pprint(self.__parameters)
@ -1010,19 +1019,19 @@ class ParametersContainer(MutableMapping):
return (parameter_name not in self.__parameters
and parameter_name in self.__inheritable)
def remove_parameter(self, parameter_name: str) -> NoReturn:
def remove_parameter(self, parameter_name: str) -> None:
if parameter_name in self.__parameters:
self.__parameters.pop(parameter_name)
elif parameter_name in self.__inheritable:
self.__inheritable.pop(parameter_name)
def change_parameter(self, parameter: str, value: Any) -> NoReturn:
def change_parameter(self, parameter: str, value: Any) -> None:
if parameter in self.__parameters:
self.__parameters.update({parameter: value})
elif parameter in self.__inheritable:
self.__inheritable.update({parameter: value})
def _clear_container(self) -> NoReturn:
def _clear_container(self) -> None:
self.__parameters.clear()
self.__inheritable.clear()
@ -1047,10 +1056,10 @@ class ParametersContainer(MutableMapping):
else:
return False
def __setitem__(self, name: str, value: Any) -> NoReturn:
def __setitem__(self, name: str, value: Any) -> None:
self.__parameters[name] = value
def __delitem__(self, name: str) -> NoReturn:
def __delitem__(self, name: str) -> None:
if name in self.__parameters:
del self.__parameters[name]
@ -1092,9 +1101,9 @@ class CalculateExtension(Extension):
self.environment: Environment = environment
self.package_atom_parser = PackageAtomParser(chroot_path=chroot_path)
self.environment.globals.update({'pkg': self.pkg})
self.environment.globals.update({'grep': self.grep})
self.environment.globals.update({'exists': self.exists})
self.environment.globals.update({'pkg': self.pkg,
'grep': self.grep,
'exists': self.exists})
self._datavars = datavars_module
self.parameters_processor = parameters_processor
@ -1104,7 +1113,7 @@ class CalculateExtension(Extension):
# того, чтобы проверять единственность тега calculate.
self.calculate_parsed: bool = False
self.tags = {'calculate', 'save', 'set_var'}
self.tags = {'calculate', 'save'}
self.CONDITION_TOKENS_TYPES = {'eq', 'ne', 'lt', 'gt', 'lteq', 'gteq'}
self.CONDITION_NAME_TOKENS = {'not'}
self.LITERAL_TOKENS_TYPES = {'string', 'integer', 'float'}
@ -1121,6 +1130,8 @@ class CalculateExtension(Extension):
def __call__(self, env: Environment) -> "CalculateExtension":
# Необходимо для обеспечения возможности передать готовый объект
# расширения, а не его класс.
# Через функцию-фабрику такое не получается устроить, потому что при
# добавлении расширения jinja2 ищет у его класса атрибут identifier.
return self
def parse(self, parser: Parser) -> List[nodes.Output]:
@ -1285,8 +1296,10 @@ class CalculateExtension(Extension):
.format(str(error)),
lineno=self.stream.current.lineno)
def check_conditions(self, conditions: List[Template]) -> NoReturn:
def check_conditions(self, conditions: List[Template]) -> None:
for condition in conditions:
# TODO Добавить различие провала условие из синтаксической ошибки
# или при отрицательном условии.
self.condition_result = False
try:
condition.render(__datavars__=self._datavars)
@ -1423,7 +1436,7 @@ class CalculateExtension(Extension):
def _modify_variables(self, variable: List[str], namespace: NamespaceNode,
new_value: Any, optype: int,
target_file: Optional[str] = None,
modify_only: bool = True) -> NoReturn:
modify_only: bool = True) -> None:
'''Метод для модификации значения переменной.'''
variable_name = variable[-1]
@ -1457,7 +1470,7 @@ class CalculateExtension(Extension):
# DEPRECATED
def _modify_hash(self, variable_name: List[str],
hash_variable: VariableNode, new_value, optype,
target_file: Optional[str] = None) -> NoReturn:
target_file: Optional[str] = None) -> None:
'''Метод для модификации значения в переменной-хэше.'''
value_name = variable_name[-1]
hash_value = hash_variable.get_value().get_hash()
@ -1477,7 +1490,7 @@ class CalculateExtension(Extension):
def _save_to_target(self, namespace_name: List[str],
variable_name: str, value: Any, target_file: str
) -> NoReturn:
) -> None:
'''Метод для добавления переменной в список переменных, значение
которых было установлено через тег save и при этом должно быть
сохранено в указанном файле: save.target_file.'''
@ -1677,7 +1690,7 @@ class CalculateExtension(Extension):
class TemplateEngine:
def __init__(self, directory_path: Union[str, None] = None,
def __init__(self, directory_path: Optional[str] = None,
datavars_module: Union[Datavars,
NamespaceNode,
Variables] = Variables(),
@ -1728,16 +1741,16 @@ class TemplateEngine:
return self.parameters_processor.for_package
@for_package.setter
def for_package(self, package: Package) -> NoReturn:
def for_package(self, package: Package) -> None:
self.parameters_processor.for_package = package
def change_directory(self, directory_path: str) -> NoReturn:
def change_directory(self, directory_path: str) -> None:
'''Метод для смены директории в загрузчике.'''
self.environment.loader = FileSystemLoader(directory_path)
def process_template(self, template_path: str, template_type: str,
parameters: Optional[ParametersContainer] = None
) -> NoReturn:
) -> None:
'''Метод для обработки файла шаблона, расположенного по указанному
пути.'''
if parameters is not None:
@ -1767,7 +1780,7 @@ class TemplateEngine:
def process_template_from_string(
self, string: str, template_type: int,
parameters: Optional[ParametersContainer] = None
) -> NoReturn:
) -> None:
'''Метод для обработки текста шаблона.'''
if parameters is not None:
self._parameters_object = parameters

@ -43,7 +43,6 @@ from typing import (
List,
Tuple,
Iterator,
NoReturn,
Optional,
Callable
)
@ -123,21 +122,21 @@ class CalculateConfigFile:
self._unsaved_changes = False
return config_dictionary
def set_files_md5(self, file_path: str, file_md5: str) -> NoReturn:
def set_files_md5(self, file_path: str, file_md5: str) -> None:
'''Метод для установки в config соответствия файла некоторой
контрольной сумме.'''
file_path = self._remove_chroot(file_path)
self._config_dictionary[file_path] = file_md5
self._unsaved_changes = True
def remove_file(self, file_path: str) -> NoReturn:
def remove_file(self, file_path: str) -> None:
'''Метод для удаления файла из config.'''
file_path = self._remove_chroot(file_path)
if file_path in self._config_dictionary:
self._config_dictionary.pop(file_path)
self._unsaved_changes = True
def compare_md5(self, file_path: str, file_md5: str) -> NoReturn:
def compare_md5(self, file_path: str, file_md5: str) -> None:
'''Метод для сравнения хэш-суммы из config и некоторой заданной.'''
file_path = self._remove_chroot(file_path)
if file_path in self._config_dictionary:
@ -145,7 +144,7 @@ class CalculateConfigFile:
else:
return False
def save_changes(self) -> NoReturn:
def save_changes(self) -> None:
'''Метод для записи изменений, внессенных в файл config.'''
if not self._unsaved_changes:
return
@ -168,7 +167,6 @@ class CalculateConfigFile:
class TemplateWrapper:
'''Класс связывающий шаблон с целевым файлом и определяющий параметры
наложения шаблона, обусловленные состоянием целевого файла.'''
type_checks: Dict[int,
Callable[[str], bool]] = {DIR: os.path.isdir,
FILE: os.path.isfile}
@ -264,7 +262,7 @@ class TemplateWrapper:
raise TemplateExecutorError("'format' parameter is not set"
" file template.")
# Если по этому пути что-то есть -- проверяем конфликты.
# Если по этому пути что-то есть -- проверяем тип этого.
if os.path.exists(target_file_path):
for file_type, checker in self.type_checks.items():
if checker(target_file_path):
@ -312,7 +310,7 @@ class TemplateWrapper:
# self.parameters.append == "replace"):
# self.remove_original = True
def _check_type_conflicts(self) -> NoReturn:
def _check_type_conflicts(self) -> None:
'''Метод для проверки конфликтов типов.'''
if self.parameters.append == 'link':
if self.parameters.force:
@ -386,7 +384,7 @@ class TemplateWrapper:
raise TemplateTypeConflict("the target file is a directory"
" while the template is a file")
def _check_package_collision(self) -> NoReturn:
def _check_package_collision(self) -> None:
'''Метод для проверки на предмет коллизии, то есть конфликта пакета
шаблона и целевого файла.'''
if self.parameters.package:
@ -475,7 +473,9 @@ class TemplateWrapper:
def _compare_packages(self, lpackage: PackageAtomName,
rpackage: PackageAtomName
) -> Union[None, PackageAtomName]:
'''Метод, сравнивающий пакеты по их именам, возвращает старший.'''
'''Метод, сравнивающий пакеты по их именам, возвращает старший, если
пакеты с одинаковыми именами, но разными версиями, или None, если их
имена и категории не равны.'''
if lpackage.category != rpackage.category:
return None
@ -487,7 +487,7 @@ class TemplateWrapper:
else:
return rpackage
def _check_user_changes(self) -> NoReturn:
def _check_user_changes(self) -> None:
'''Метод для проверки наличия пользовательских изменений в
конфигурационных файлах.'''
# Эта проверка только для файлов.
@ -520,8 +520,7 @@ class TemplateWrapper:
# Путь к архивной версии файла.
self.archive_path = self._get_archive_path(self.target_path)
self.contents_matching = (self.parameters.autoupdate
or self.parameters.force)
self.contents_matching = (self.parameters.autoupdate)
if not self.protected:
self.contents_matching = True
@ -545,6 +544,8 @@ class TemplateWrapper:
# Если файл по целевому пути не относится к какому-либо пакету.
# self.contents_matching = False
pass
elif self.target_type is DIR and self.parameters.force:
self.contents_matching = True
elif not self.contents_matching:
# Если файл есть и он относится к текущему пакету.
# Если по каким-то причинам уже нужно считать, что хэш-суммы
@ -613,7 +614,7 @@ class TemplateWrapper:
return new_cfg_path
def remove_from_contents(self) -> NoReturn:
def remove_from_contents(self) -> None:
'''Метод для удаления целевого файла из CONTENTS.'''
if self.target_package is None:
return
@ -623,13 +624,13 @@ class TemplateWrapper:
elif self.template_type == FILE:
self.target_package.remove_obj(self.target_path)
def clear_dir_contents(self) -> NoReturn:
def clear_dir_contents(self) -> None:
'''Метод для удаления из CONTENTS всего содержимого директории после
применения append = "clear".'''
if self.template_type == DIR and self.target_package is not None:
self.target_package.clear_dir(self.target_path)
def add_to_contents(self, file_md5: Optional[str] = None) -> NoReturn:
def add_to_contents(self, file_md5: Optional[str] = None) -> None:
'''Метод для добавления целевого файла в CONTENTS.'''
if self.target_package is None:
return
@ -652,7 +653,7 @@ class TemplateWrapper:
elif self.template_type == FILE:
self.target_package.add_obj(source_path, file_md5=file_md5)
def update_contents_from_list(self, changed_list: dict) -> NoReturn:
def update_contents_from_list(self, changed_list: dict) -> None:
'''Метод для изменения CONTENTS по списку измененных файлов.'''
print("UPDATE CONTENTS FROM LIST")
if self.target_package is None:
@ -676,7 +677,7 @@ class TemplateWrapper:
self.target_package.add_dir(file_path)
@classmethod
def _set_protected(cls, chroot_path: str) -> NoReturn:
def _set_protected(cls, chroot_path: str) -> None:
'''Метод для получения множества защищенных директорий.'''
cls._protected_set = set()
cls._unprotected_set = set()
@ -699,7 +700,7 @@ class TemplateWrapper:
cls._protected_is_set = True
def save_changes(self) -> NoReturn:
def save_changes(self) -> None:
'''Метод для сохранения изменений внесенных в CONTENTS.'''
if self.target_package:
self.target_package.remove_empty_directories()
@ -864,7 +865,7 @@ class TemplateExecutor:
return self.executor_output
def save_changes(self) -> NoReturn:
def save_changes(self) -> None:
'''Метод для сохранения чего-нибудь после выполнения всех шаблонов.'''
# Пока сохраняем только получившееся содержимое config-файла.
self.calculate_config_file.save_changes()
@ -892,7 +893,7 @@ class TemplateExecutor:
template_object.add_to_contents()
def _append_remove_directory(self, template_object: TemplateWrapper
) -> NoReturn:
) -> None:
'''Метод описывающий действия для append = "remove", если шаблон --
директория. Удаляет директорию со всем содержимым, если она есть.'''
if template_object.target_type is not None:
@ -914,11 +915,11 @@ class TemplateExecutor:
template_object.remove_from_contents()
def _append_skip_directory(self,
template_object: TemplateWrapper) -> NoReturn:
template_object: TemplateWrapper) -> None:
pass
def _append_clear_directory(self,
template_object: TemplateWrapper) -> NoReturn:
template_object: TemplateWrapper) -> None:
'''Метод описывающий действия для append = "clear", если шаблон --
директория. Удаляет все содержимое директории, если она есть.'''
if template_object.target_type is not None:
@ -949,7 +950,7 @@ class TemplateExecutor:
template_object.clear_dir_contents()
def _append_link_directory(self, template_object: TemplateWrapper
) -> NoReturn:
) -> None:
'''Метод описывающий действия для append = "link", если шаблон --
директория. Создает ссылку на директорию, если она есть.'''
self._link_directory(template_object.parameters.source,
@ -974,7 +975,7 @@ class TemplateExecutor:
template_object.add_to_contents()
def _append_replace_directory(self, template_object: TemplateWrapper
) -> NoReturn:
) -> None:
'''Метод описывающий действия для append = "replace", если шаблон --
директория. Очищает директорию или создает, если ее нет.'''
if template_object.target_type is None:
@ -1006,7 +1007,7 @@ class TemplateExecutor:
def _append_join_file(self, template_object: TemplateWrapper,
join_before: bool = False, replace: bool = False
) -> NoReturn:
) -> None:
'''Метод описывающий действия при append = "join", если шаблон -- файл.
Объединяет шаблон с целевым файлом.'''
output_path = template_object.output_path
@ -1105,6 +1106,7 @@ class TemplateExecutor:
chroot_path=self.chroot_path)
# Удаляем форматный объект входного файла.
# del(parsed_template)
# Если исполняемый формат выдал список измененных файлов для
# изменения CONTENTS и при этом задан пакет -- обновляем
# CONTENTS.
@ -1279,32 +1281,32 @@ class TemplateExecutor:
chown = self.file_default_parameters.get('chown', False)
return chown
def _append_after_file(self, template_object: TemplateWrapper) -> NoReturn:
def _append_after_file(self, template_object: TemplateWrapper) -> None:
'''Метод описывающий действия при append = "after", если шаблон --
файл. Объединяет шаблон с целевым файлом так, чтобы текст добавлялся
в конец файла и в конец каждой секции файла.'''
self._append_join_file(template_object, join_before=False)
def _append_before_file(self, template_object: TemplateWrapper
) -> NoReturn:
) -> None:
'''Метод описывающий действия при append = "after", если шаблон --
файл. Объединяет шаблон с целевым файлом так, чтобы текст добавлялся
в начало файла и в начало каждой секции файла.'''
self._append_join_file(template_object, join_before=True)
def _append_skip_file(self, template_object: TemplateWrapper) -> NoReturn:
def _append_skip_file(self, template_object: TemplateWrapper) -> None:
'''Метод описывающий действия при append = "skip". Пока никаких
действий.'''
pass
def _append_replace_file(self, template_object: TemplateWrapper
) -> NoReturn:
) -> None:
'''Метод описывающий действия при append = "replace", если шаблон --
файл. Очищает файл и затем накладывает на него шаблон.'''
self._append_join_file(template_object, replace=True)
def _append_remove_file(self, template_object: TemplateWrapper
) -> NoReturn:
) -> None:
'''Метод описывающий действия при append = "remove", если шаблон --
файл. Удаляет файл.'''
if template_object.target_type is not None:
@ -1320,7 +1322,7 @@ class TemplateExecutor:
if self.dbpkg:
template_object.remove_from_contents()
def _append_clear_file(self, template_object: TemplateWrapper) -> NoReturn:
def _append_clear_file(self, template_object: TemplateWrapper) -> None:
'''Метод описывающий действия при append = "clear", если шаблон --
файл. Очищает файл.'''
if template_object.target_type is not None:
@ -1342,7 +1344,7 @@ class TemplateExecutor:
if self.dbpkg:
template_object.add_to_contents()
def _append_link_file(self, template_object: TemplateWrapper) -> NoReturn:
def _append_link_file(self, template_object: TemplateWrapper) -> None:
'''Метод описывающий действия при append = "link", если шаблон --
файл. Создает ссылку на файл, указанный в параметре source.'''
input_path = template_object.input_path
@ -1424,7 +1426,7 @@ class TemplateExecutor:
return hashlib.md5(source_path.encode()).hexdigest()
def _create_directory(self, template_object: TemplateWrapper,
path_to_create: Optional[str] = None) -> NoReturn:
path_to_create: Optional[str] = None) -> None:
'''Метод для создания директории и, при необходимости, изменения
владельца и доступа все директорий на пути к целевой.'''
if path_to_create is None:
@ -1481,7 +1483,7 @@ class TemplateExecutor:
'Failed to create directory: {}, reason: {}'.
format(create_path, str(error)))
def _remove_directory(self, target_path: str) -> NoReturn:
def _remove_directory(self, target_path: str) -> None:
'''Метод для удаления директории.'''
if os.path.exists(target_path):
if os.path.isdir(target_path):
@ -1505,7 +1507,7 @@ class TemplateExecutor:
"reason: {1}").format(target_path,
error_message))
def _clear_directory(self, target_path: str) -> NoReturn:
def _clear_directory(self, target_path: str) -> None:
'''Метод для очистки содержимого целевой директории.'''
if os.path.exists(target_path):
if os.path.isdir(target_path):
@ -1526,9 +1528,9 @@ class TemplateExecutor:
error_message))
def _link_directory(self, source: str, target_path: str,
force: bool = False) -> NoReturn:
force: bool = False) -> None:
'''Метод для создания по целевому пути ссылки на директорию
расположенную на пути, указанному в source.'''
расположенную на пути, указанном в source.'''
try:
os.symlink(source, target_path, target_is_directory=True)
except OSError as error:
@ -1536,7 +1538,7 @@ class TemplateExecutor:
"failed to create symlink: {0} -> {1}, reason: {2}".
format(target_path, source, str(error)))
def _remove_file(self, target_path: str) -> NoReturn:
def _remove_file(self, target_path: str) -> None:
'''Метод для удаления файлов.'''
if os.path.exists(target_path):
if os.path.isfile(target_path):
@ -1567,7 +1569,7 @@ class TemplateExecutor:
"reason: {1}").format(target_path,
error_message))
def _clear_file(self, target_path: str) -> NoReturn:
def _clear_file(self, target_path: str) -> None:
'''Метод для очистки файлов.'''
if os.path.exists(target_path):
if os.path.isfile(target_path):
@ -1586,7 +1588,7 @@ class TemplateExecutor:
"reason: {1}").format(target_path,
error_message))
def _link_file(self, source: str, target_path: str) -> NoReturn:
def _link_file(self, source: str, target_path: str) -> None:
'''Метод для создания по целевому пути ссылки на файл расположенный на
пути, указанному в source.'''
try:
@ -1596,7 +1598,7 @@ class TemplateExecutor:
"failed to create symlink to the file: {0} -> {1}, reason: {2}".
format(target_path, source, str(error)))
def _run_template(self, template_object: TemplateWrapper) -> NoReturn:
def _run_template(self, template_object: TemplateWrapper) -> None:
'''Метод для сохранения текста шаблонов, который должен быть исполнен
интерпретатором указанным в run прямо во время обработки шаблонов.'''
text_to_run = template_object.template_text
@ -1634,7 +1636,7 @@ class TemplateExecutor:
" interpreter '{}', reason: {}").
format(interpreter, str(error)))
def _exec_template(self, template_object: TemplateWrapper) -> NoReturn:
def _exec_template(self, template_object: TemplateWrapper) -> None:
'''Метод для сохранения текста шаблонов, который должен быть исполнен
интерпретатором указанным в exec после выполнения всех прочих шаблонов.
'''
@ -1720,7 +1722,7 @@ class TemplateExecutor:
format(interpreter, str(error)))
def _chown_directory(self, target_path: str, chown_value: dict
) -> NoReturn:
) -> None:
"""Метод для смены владельца директории."""
try:
if os.path.exists(target_path):
@ -1738,7 +1740,7 @@ class TemplateExecutor:
chown_value['gid']),
str(error)))
def _chmod_directory(self, target_path: str, chmod_value: int) -> NoReturn:
def _chmod_directory(self, target_path: str, chmod_value: int) -> None:
'''Метод для смены прав доступа к директории.'''
if isinstance(chmod_value, tuple) and not chmod_value[1]:
chmod_value = chmod_value[0]
@ -1759,7 +1761,7 @@ class TemplateExecutor:
'Can not chmod directory: {0}, reason: {1}'.
format(target_path, str(error)))
def _chown_file(self, target_path: str, chown_value: dict) -> NoReturn:
def _chown_file(self, target_path: str, chown_value: dict) -> None:
'''Метод для смены владельца файла.'''
try:
if os.path.exists(target_path):
@ -1777,7 +1779,7 @@ class TemplateExecutor:
chown_value['gid']),
str(error)))
def _chmod_file(self, target_path: str, chmod_value: int) -> NoReturn:
def _chmod_file(self, target_path: str, chmod_value: int) -> None:
'''Метод для смены прав доступа к директории.'''
try:
if not os.path.exists(target_path):
@ -1802,13 +1804,15 @@ class TemplateExecutor:
current_mode: Optional[int] = None) -> int:
'''Метод для наложения X-маски, необходимой для получения значения
chmod, c учетом возможности наличия в нем значения "X".'''
if not chmod[1]:
return chmod[0]
chmod, x_mask = chmod
if not x_mask:
return chmod
if current_mode is None:
return chmod[0] ^ chmod[1]
return chmod ^ x_mask
else:
return chmod[0] ^ (current_mode & chmod[1])
return chmod ^ (current_mode & x_mask)
def _get_file_mode(self, file_path: str) -> int:
'''Метод для получения прав доступа для указанного файла.'''
@ -1847,7 +1851,7 @@ class TemplateExecutor:
if self.chroot_path == '/':
group_name = grp.getgrgid(gid).gr_name
else:
group_name = self._get_group_name_form_gid(gid)
group_name = self._get_group_name_from_gid(gid)
except (TypeError, KeyError):
group_name = str(gid)
@ -1875,7 +1879,7 @@ class TemplateExecutor:
else:
return str(uid)
def _get_group_name_form_gid(self, gid: int) -> str:
def _get_group_name_from_gid(self, gid: int) -> str:
'''Метод для получения названия группы по его gid.'''
group_file_path = os.path.join(self.chroot_path, 'etc/group')
group_dictionary = dict()
@ -1927,7 +1931,7 @@ class DirectoryTree:
self.base_directory = base_directory
self._tree = {}
def update_tree(self, tree: dict) -> NoReturn:
def update_tree(self, tree: dict) -> None:
'''Метод, инициирующий наложение заданного дерева каталогов на данный
экземпляр дерева.'''
self._update(self._tree, tree)
@ -1943,7 +1947,7 @@ class DirectoryTree:
original_tree[parent] = child
return original_tree
def show_tree(self) -> NoReturn:
def show_tree(self) -> None:
pprint(self._tree)
def get_directory_tree(self, directory: str) -> "DirectoryTree":
@ -1956,13 +1960,13 @@ class DirectoryTree:
directory_tree._tree = self._tree[directory]
return directory_tree
def __getitem__(self, name: str) -> dict:
def __getitem__(self, name: str) -> Union[None, dict]:
if name in self._tree:
return self._tree[name]
else:
return None
def __setitem__(self, name: str, value: Union[None, dict]) -> NoReturn:
def __setitem__(self, name: str, value: Union[None, dict]) -> None:
self._tree[name] = value
def __iter__(self) -> Iterator[str]:
@ -2149,7 +2153,7 @@ class DirectoryProcessor:
return path_to_add
def _add_package_to_group(self, group_name: str,
package_atom: str) -> NoReturn:
package_atom: str) -> None:
try:
groups_namespace = self.datavars_module.main.cl.groups
except (VariableNotFoundError, AttributeError):
@ -2230,7 +2234,7 @@ class DirectoryProcessor:
return -1
return -2
def _make_current_template_var(self) -> NoReturn:
def _make_current_template_var(self) -> None:
var_path = ['main', 'cl']
namespace = self.datavars_module
@ -2251,7 +2255,7 @@ class DirectoryProcessor:
else:
namespace['current_template'] = ""
def process_template_directories(self) -> NoReturn:
def process_template_directories(self) -> None:
'''Метод для обхода шаблонов, содержащихся в каталогах из
main.cl_template.path.'''
# Режим заполнения очередей директорий пакетов, необходимых для более
@ -2319,12 +2323,11 @@ class DirectoryProcessor:
PackageCreator.save_all()
return self.template_executor.changed_files
def _run_template_from_base_directory(
self, template_names: List[str],
base_directory: str,
package: Optional[Package] = None,
directory_tree: Optional[dict] = None
) -> NoReturn:
def _run_template_from_base_directory(self, template_names: List[str],
base_directory: str,
package: Optional[Package] = None,
directory_tree: Optional[dict] = None
) -> None:
'''Метод для запуска шаблонов файлов находящихся в базовой директории.
'''
self.template_engine.change_directory(base_directory)
@ -2380,6 +2383,7 @@ class DirectoryProcessor:
else:
template_package = package
# Дефолтные значения разные для упрощения написания шаблонов.
if not parameters.run and not parameters.exec:
if not parameters.format:
parameters.set_parameter({'format': 'raw'})
@ -2396,7 +2400,7 @@ class DirectoryProcessor:
template_text=template_text,
package=template_package)
def _execute_handlers(self) -> NoReturn:
def _execute_handlers(self) -> None:
'''Метод для запуска обработчиков добавленных в очередь обработчиков
с помощью параметра notify.'''
self.output.set_info('Processing handlers...')
@ -2462,7 +2466,7 @@ class DirectoryProcessor:
FILE, handler_path,
template_text=handler_text)
def _merge_packages(self) -> NoReturn:
def _merge_packages(self) -> None:
'''Метод для выполнения шаблонов относящихся к пакетам, указанным во
всех встреченных значениях параметра merge.'''
not_merged_packages = []
@ -2527,7 +2531,7 @@ class DirectoryProcessor:
else:
self.output.set_success('All packages are merged.')
def _run_exec_files(self) -> NoReturn:
def _run_exec_files(self) -> None:
'''Метод для выполнения скриптов, полученных в результате обработки
шаблонов с параметром exec.'''
for exec_file_path, exec_info in\
@ -2554,7 +2558,7 @@ class DirectoryProcessor:
current_target_path: str,
directory_parameters: ParametersContainer,
directory_tree: Union[dict, DirectoryTree] = {},
package: Optional[Package] = None) -> NoReturn:
package: Optional[Package] = None) -> None:
'''Метод для рекурсивного обхода директорий с шаблонами, а также, при
необходимости, заполнения деревьев директорий шаблонов, с помощью
которых далее выполняются шаблоны пакетов из merge.'''
@ -2805,7 +2809,6 @@ class DirectoryProcessor:
if self.fill_trees:
directory_tree = {}
return
def _scan_directory(self, directory_path: str
) -> Tuple[List[str], List[str]]:

@ -1,89 +0,0 @@
from pyparsing import Literal, Word, ZeroOrMore, Group, Dict, Optional,\
restOfLine, empty, printables, OneOrMore, oneOf, nums,\
lineno, line, col, Keyword, SkipTo, LineEnd, Combine
from enum import Enum
class CalculateIniParser:
'''Класс парсера calculate.ini файлов.'''
class Define(Enum):
assign = 0
append = 1
remove = 2
def __init__(self):
self.operations = {"=": self.Define.assign,
"+=": self.Define.append,
"-=": self.Define.remove}
lbrack = Literal("[")
rbrack = Literal("]")
# comma = Literal(",").suppress()
comment_symbol = Literal(';') | Literal('#')
# Define = self.Define
value_operation = (Literal("=") | Combine(Literal("+") + Literal("="))
| Combine(Literal("-") + Literal("=")))
comment = comment_symbol + Optional(restOfLine)
section_name = Word(printables+'\t', excludeChars='[]')
value_name = Word(printables+'\t', excludeChars='=-+')
# non_comma = Word(printables+'\t', excludeChars=',')
clear_section = lbrack.suppress() + Group(empty) + rbrack.suppress()
section_start = Group(OneOrMore(lbrack.suppress() + section_name
+ rbrack.suppress())
+ (clear_section | ~lbrack())
+ LineEnd().suppress())
# Если содержимое ini-файла не предваряется заголовком секции,
# значит эта строка ошибочна.
unexpected = Group(~section_start + SkipTo(LineEnd(),
include=True))("error")
unexpected.setParseAction(self._unexpected_token)
key_value = (~lbrack + value_name
+ value_operation + empty
+ restOfLine + LineEnd().suppress())
def strip_key_value(tokens):
tokens[0] = tokens[0].strip()
tokens[1] = tokens[1].strip()
key_value.setParseAction(strip_key_value)
self.ini_section_parser = (section_start
+ Group(ZeroOrMore(
Group(key_value | unexpected)))
| unexpected)
self.ini_section_parser.ignore(comment)
def _unexpected_token(self, string, location, tokens):
'''Метод вызываемый парсером, если обнаружена некорректная строка,
предназначен для получения некорректной строки и ее дальнейшего
разбора.'''
error_line = line(location, string).strip()
if error_line:
return (error_line, lineno(location, string),
col(location, string))
def parse(self, data):
for tokens, start, end in self.ini_section_parser.scanString(data):
if tokens.getName() == "error":
print('error tokens: {}'.format(tokens))
yield {'error': tokens}
section, defkeys = tokens
section_list = section.asList()
if section_list[-1] == []:
self.clear_section(section_list[:-1])
yield {'clear_section': (section_list[:-1], )}
self.start_section(section.asList())
for defkey in defkeys:
yield {'define_key': (section.asList(), defkey[0], defkey[2],
self.operations[defkey[1]])}

@ -1,14 +0,0 @@
{% for file_name, contents_values in document_dictionary.items() %}
{% if contents_values is mapping %}
{% set contents_values = (contents_values.values()|list) %}
{% else %}
{% set contents_values = (contents_values[0]|list) %}
{% set file_name = file_name[-1] %}
{% endif %}
{% if contents_values[0] == 'sym' %}
{{ contents_values[0] }} {{ file_name }} -> {{ contents_values[1:]|join(' ') }}
{% else %}
{{ contents_values[0] }} {{ file_name }}{% if contents_values[1:] %} {{ contents_values[1:]|join(' ') }}{% endif %}
{% endif %}
{% endfor %}

@ -1,22 +1,23 @@
import os
import re
from typing import Dict
from calculate.utils.files import read_file_lines
class ProfileWalker:
'''Объект обходящий все директории профиля через parent файлы.'''
def __init__(self, filename, repositories):
self.repositories = repositories
self.filename = filename
self.re_reppath = re.compile("^({0})+:".format(
def __init__(self, filename: str, repositories: Dict[str, str]):
self.repositories: Dict[str, str] = repositories
self.filename: str = filename
self.re_reppath: re.Pattern = re.compile("^({0})+:".format(
"|".join(self.repositories.keys())))
def interpolate(self, path):
def interpolate(self, path: str):
def subfunc(m):
return "{0}/profiles/".format(self.repositories.get(m.group(1)))
return self.re_reppath.sub(subfunc, path)
def find(self, directory):
def find(self, directory: str):
'''Метод для поиска по профилю всех файлов с именем, указанным в
self.filename.'''
parent_file_path = os.path.join(directory, "parent")

@ -7,7 +7,6 @@ from collections import OrderedDict
from .files import read_file, read_link, join_paths, FilesError
from typing import (
Generator,
NoReturn,
Optional,
Dict,
Tuple,
@ -15,16 +14,6 @@ from typing import (
List,
Any
)
from pyparsing import (
Literal,
Regex,
Word,
nums,
alphanums,
LineEnd,
SkipTo,
)
from jinja2 import PackageLoader, Environment
from calculate.utils.tools import Singleton
import hashlib
@ -59,6 +48,10 @@ class PackageNotFound(Exception):
class PackageCreator(type):
"""Метакласс для создания классов пакетов, напоминающий метакласс
Singleton. Следит за тем, чтобы для каждого пакета создавался только один
экземляр объекта Package."""
# TODO Перенести этот функционал в класс Package.
_instances: Dict["PackageAtomName", "Package"] = {}
def __call__(cls, *args, **kwargs):
@ -74,14 +67,14 @@ class PackageCreator(type):
return cls._instances[args[0]]
@classmethod
def save_all(cls) -> NoReturn:
def save_all(cls) -> None:
for atom, pkg in cls._instances.items():
pkg.remove_empty_directories()
pkg.write_contents_file()
@classmethod
def clear_instances(cls) -> NoReturn:
"""Метод для очистки текущего кэша пакетный объектов.
def clear_instances(cls) -> None:
"""Метод для очистки текущего кэша пакетных объектов.
Необходим при тестировании."""
cls._instances.clear()
@ -370,62 +363,6 @@ class ContentsParser(metaclass=Singleton):
return "\n".join(lines)
class OldContentsParser(metaclass=Singleton):
def __init__(self):
sym_keyword = Literal('sym')
dir_keyword = Literal('dir')
obj_keyword = Literal('obj')
symlink_arrow = Literal('->')
file_path = Regex(r'\S+')
time_value = Word(nums)
md5 = Word(alphanums)
sym_line = (sym_keyword('type') + file_path('path')
+ symlink_arrow.suppress() + file_path('target')
+ time_value('mtime') + LineEnd().suppress())
dir_line = (dir_keyword('type') + file_path('path')
+ LineEnd().suppress())
obj_line = (obj_keyword('type') + file_path('path')
+ md5('md5') + time_value('mtime') + LineEnd().suppress())
unexpected = SkipTo(LineEnd(), include=True)
self._parser = (dir_line | sym_line |
obj_line | unexpected('unexpected')
).setParseAction(self._parser_method)
def _parser_method(self, parse_result):
if parse_result.getName() == 'unexpected':
return [None]
result_dict = parse_result.asDict()
path = result_dict.pop('path')
value = ({path: result_dict})
return value
def parse(self, contents_text: str) -> OrderedDict:
output_dictionary = OrderedDict()
for tokens, start, end in self._parser.scanString(contents_text):
parse_result = tokens[0]
if parse_result is None:
continue
output_dictionary.update(parse_result)
return output_dictionary
def render(self, contents_dictionary: dict) -> str:
file_loader = PackageLoader('calculate', 'utils')
environment = Environment(loader=file_loader,
trim_blocks=True,
lstrip_blocks=True)
template = environment.get_template('contents_template')
text = template.render(document_dictionary=contents_dictionary)
return text
class PackageAtomName:
'''Класс для хранения результата определения пакета. Для определения пакета
использует путь к его pkg директории.'''
@ -686,7 +623,7 @@ class PackageAtomParser:
return atom_dictionary
def _check_slot_value(self, pkg_path: str,
atom_dictionary: dict) -> NoReturn:
atom_dictionary: dict) -> None:
'''Метод для проверки полученного из параметра package значения slot.
'''
if atom_dictionary['slot']:
@ -699,7 +636,7 @@ class PackageAtomParser:
errno=NOTEXIST)
def _check_use_flags_value(self, pkg_path: str,
atom_dictionary: dict) -> NoReturn:
atom_dictionary: dict) -> None:
'''Метод для проверки полученных из параметра package значений
use-флагов.'''
if atom_dictionary['use_flags']:
@ -742,7 +679,7 @@ class PackageAtomParser:
yield path
def _check_version(self, atom_dictionary: dict, pkg_version: Version
) -> NoReturn:
) -> None:
condition = atom_dictionary['condition']
if condition == '=':
@ -769,6 +706,7 @@ class PackageAtomParser:
def get_file_package(self, file_path: str) -> PackageAtomName:
'''Метод для определения пакета, которому принадлежит файл.'''
# Удаляем часть пути соответствующую chroot_path
# TODO предусмотреть кэширование результата.
if self.chroot_path != '/' and file_path.startswith(self.chroot_path):
file_path = file_path[len(self.chroot_path):]
@ -919,7 +857,7 @@ class Package(metaclass=PackageCreator):
else:
return False
def write_contents_file(self) -> NoReturn:
def write_contents_file(self) -> None:
'''Метод для записи файла CONTENTS.'''
with open(self.contents_file_path, 'w') as contents_file:
contents_text = self.render_contents_file()
@ -943,7 +881,7 @@ class Package(metaclass=PackageCreator):
return self.contents_dictionary[file_path]['type']
return None
def sort_contents_dictionary(self) -> NoReturn:
def sort_contents_dictionary(self) -> None:
'''Метод для сортировки словаря, полученного в результате разбора и
изменения CONTENTS-файла.'''
tree = {}
@ -980,7 +918,7 @@ class Package(metaclass=PackageCreator):
if level[part]:
yield from self._make_paths(part_path, level[part])
def add_dir(self, file_name: str) -> NoReturn:
def add_dir(self, file_name: str) -> None:
'''Метод для добавления в CONTENTS директорий.'''
file_name = self.remove_chroot_path(file_name)
@ -992,7 +930,7 @@ class Package(metaclass=PackageCreator):
self.contents_dictionary[file_name] = contents_item
def add_sym(self, file_name: str, target_path: Optional[str] = None,
mtime: Optional[str] = None) -> NoReturn:
mtime: Optional[str] = None) -> None:
'''Метод для добавления в CONTENTS символьных ссылок.'''
real_path = file_name
@ -1018,7 +956,7 @@ class Package(metaclass=PackageCreator):
self.contents_dictionary[file_name] = contents_item
def add_obj(self, file_name: str, file_md5: Optional[str] = None,
mtime: Optional[str] = None) -> NoReturn:
mtime: Optional[str] = None) -> None:
'''Метод для добавления в CONTENTS обычных файлов как obj.'''
real_path = file_name
file_name = self.remove_chroot_path(file_name)
@ -1043,7 +981,7 @@ class Package(metaclass=PackageCreator):
'mtime': mtime})
self.contents_dictionary[file_name] = contents_item
def add_file(self, file_name: str) -> NoReturn:
def add_file(self, file_name: str) -> None:
'''Метод для добавления в CONTENTS файла любого типа.'''
if file_name != '/':
real_path = file_name
@ -1197,7 +1135,7 @@ class Package(metaclass=PackageCreator):
return '<Package: {}/{}>'.format(self.package_name.category,
self.package_name.fullname)
# def __del__(self) -> NoReturn:
# def __del__(self) -> None:
# if self.autosave and os.path.exists(self.contents_file_path):
# self.remove_empty_directories()
# self.write_contents_file()

@ -1,6 +1,7 @@
from calculate.utils.files import grep_file
import os
class SystemType:
"""
Тип контейнера текущей системы
@ -21,12 +22,12 @@ class SystemType:
if grep_file("/proc/cpuinfo", "UML"):
return cls.Uml
elif grep_file("/proc/self/status",
"(s_context|VxID):\s*[1-9]"):
r"(s_context|VxID):\s*[1-9]"):
return cls.VServer
elif (os.path.exists("/proc/vz/veinfo")
and not os.path.exists("/proc/vz/version")):
return cls.OpenVZ
elif grep_file("/proc/self/status", "envID:\s*[1-9]"):
elif grep_file("/proc/self/status", r"envID:\s*[1-9]"):
return cls.OpenVZ
elif grep_file("/proc/1/environ", "container=lxc"):
return cls.LXC

@ -117,12 +117,15 @@ def get_traceback_caller(exception_type, exception_object,
if module_name.endswith('.py'):
module_name = module_name[:-3]
full_module_name = [module_name]
while (module_path and module_path != '/' and not
module_path.endswith('site-packages')):
module_path, package_name = os.path.split(module_path)
full_module_name.insert(0, package_name)
if module_path.endswith('site-packages'):
module_name = '.'.join(full_module_name)
return module_name, line_number

@ -1,13 +1,15 @@
# vim: fileencoding=utf-8
#
import re
import ast
# import ast
import dis
from contextlib import contextmanager
from inspect import signature, getsource
from types import FunctionType, LambdaType
# from inspect import signature, getsource
from inspect import signature
# from types import FunctionType, LambdaType
from types import FunctionType
from calculate.utils.tools import Singleton
from typing import List, Any, Union, Generator, Callable
from typing import List, Any, Union, Generator, Callable, Optional
class DependenceError(Exception):
@ -436,7 +438,7 @@ class VariableWrapper:
class DependenceSource:
'''Класс зависимости как источника значения переменной.'''
def __init__(self, variables: tuple,
depend: Union[Callable, None] = None):
depend: Optional[Callable] = None):
self._args: Union[tuple, list] = variables
self.depend_function: Union[Callable, None] = depend
self._subscriptions: set = set()
@ -736,7 +738,7 @@ class VariableNode:
subscriber._invalidate()
@contextmanager
def _start_calculate(self):
def _start_calculate(self) -> Generator["VariableNode", None, None]:
'''Менеджер контекста устанавливающий флаг, указывающий, что данная
переменная в состоянии расчета. В данном случае необходима только для
того, чтобы при получении значения параметра внутри метода для расчета
@ -782,7 +784,7 @@ class VariableNode:
class NamespaceNode:
'''Класс ноды соответствующей пространству имен в дереве переменных.'''
def __init__(self, name: str = '',
parent: Union["NamespaceNode", None] = None):
parent: Optional["NamespaceNode"] = None):
self._name = name
self._variables = dict()
self._namespaces = dict()
@ -943,9 +945,10 @@ class FormatAPI(metaclass=Singleton):
def __call__(self, string: str) -> "Dependence":
vars_list = []
def subfunc(matchobj):
def subfunc(matchobj: re.Match):
vars_list.append(matchobj.group(0)[1:-1].strip())
return '{}'
format_string = self.pattern.sub(subfunc, string)
def depend_function(*args):
@ -1013,7 +1016,7 @@ class NamespaceAPI(metaclass=Singleton):
'''Класс для создания пространств имен при задании переменных через
python-скрипты.'''
def __init__(self, var_fabric: VariableAPI,
dependence_fabric: DependenceAPI(),
dependence_fabric: DependenceAPI,
datavars_root=NamespaceNode('<root>')):
self._datavars = datavars_root
self.current_namespace = self._datavars
@ -1028,7 +1031,7 @@ class NamespaceAPI(metaclass=Singleton):
self._dependence_fabric.datavars_root = self._datavars
@property
def datavars(self):
def datavars(self) -> NamespaceNode:
'''Метод для получения корневого пространства имен, через которое далее
можно получить доступ к переменным.'''
return self._datavars
@ -1058,7 +1061,8 @@ class NamespaceAPI(metaclass=Singleton):
self._dependence_fabric.current_namespace = namespace
@contextmanager
def __call__(self, namespace_name: str):
def __call__(self, namespace_name: str
) -> Generator["NamespaceAPI", None, None]:
'''Метод для создания пространств имен с помощью with.'''
if namespace_name not in self.current_namespace._namespaces:
namespace = NamespaceNode(namespace_name,

@ -5,6 +5,7 @@ import logging
import importlib
import importlib.util
from jinja2 import Environment, PackageLoader
from typing import Dict, Optional, Any, List, Generator
from calculate.variables.datavars import (
NamespaceNode,
VariableNode,
@ -22,11 +23,12 @@ from calculate.utils.gentoo import ProfileWalker
from calculate.utils.files import read_file, FilesError
from calculate.utils.tools import Singleton
from pyparsing import (
ParseResults,
Literal,
Word,
ZeroOrMore,
Group,
Optional,
Optional as OptionalPattern,
restOfLine,
empty,
printables,
@ -65,14 +67,12 @@ class CalculateIniParser(metaclass=Singleton):
lbrack = Literal("[")
rbrack = Literal("]")
# comma = Literal(",").suppress()
comment_symbol = Literal(';') | Literal('#')
# Define = self.Define
value_operation = (Literal("=") | Combine(Literal("+") + Literal("="))
| Combine(Literal("-") + Literal("=")))
comment = comment_symbol + Optional(restOfLine)
comment = comment_symbol + OptionalPattern(restOfLine)
section_name = (lbrack.suppress() + (~Word(nums)
+ Word(printables+'\t',
@ -81,7 +81,6 @@ class CalculateIniParser(metaclass=Singleton):
value_name = Word(printables+'\t', excludeChars='=-+')
# non_comma = Word(printables+'\t', excludeChars=',')
clear_section = lbrack.suppress() + Group(empty) + rbrack.suppress()
row_index = lbrack.suppress() + Word(nums) + rbrack.suppress()
@ -93,7 +92,8 @@ class CalculateIniParser(metaclass=Singleton):
+ (row_index | clear_section | ~lbrack)
+ LineEnd().suppress())
def add_lineno(string, location, tokens):
def add_lineno(string: str, location: int,
tokens: ParseResults) -> None:
tokens.append(lineno(location, string))
section_start = (namespace_start('namespace') |
@ -110,7 +110,8 @@ class CalculateIniParser(metaclass=Singleton):
+ value_operation + empty
+ restOfLine + LineEnd().suppress())
def process_key_value(string, location, tokens):
def process_key_value(string: str, location: int,
tokens: ParseResults) -> None:
tokens[0] = tokens[0].strip()
tokens[1] = tokens[1].strip()
tokens.append(lineno(location, string))
@ -124,7 +125,7 @@ class CalculateIniParser(metaclass=Singleton):
| unexpected)
self.ini_section_parser.ignore(comment)
def parse(self, data: str):
def parse(self, data: str) -> Generator[Dict[str, tuple], None, None]:
for tokens, start, end in self.ini_section_parser.scanString(data):
if tokens.getName() == "error":
if tokens[1].strip():
@ -161,7 +162,8 @@ class CalculateIniParser(metaclass=Singleton):
yield {'start_table': (table_list, table_values,
section_lineno)}
def _unexpected_token(self, string, location, tokens):
def _unexpected_token(self, string: str, location: int,
tokens: ParseResults) -> list:
'''Метод вызываемый парсером, если обнаружена некорректная строка,
предназначен для получения некорректной строки и ее дальнейшего
разбора.'''
@ -173,14 +175,14 @@ class NamespaceIniFiller:
из calculate.ini файла.'''
available_sections = {'custom'}
def __init__(self, restrict_creation=True):
def __init__(self, restrict_creation: bool = True):
self.ini_parser = CalculateIniParser()
self._errors = []
# Флаги, определяющие возможность создания новых переменных и новых
# пространств имен в данном пространстве имен.
self.restricted = restrict_creation
self.modify_only = False
self.restricted: bool = restrict_creation
self.modify_only: bool = False
def fill(self, namespace: NamespaceNode, ini_file_text: str) -> None:
'''Метод для разбора calculate.ini файла и добавления всех его
@ -192,11 +194,12 @@ class NamespaceIniFiller:
for parsed_line in self.ini_parser.parse(ini_file_text):
self._line_processor(**parsed_line)
def _line_processor(self, start_section=None,
clear_section=None,
start_table=None,
define_key=None,
error=None, **kwargs):
def _line_processor(self, start_section: Optional[tuple] = None,
clear_section: Optional[tuple] = None,
start_table: Optional[tuple] = None,
define_key: Optional[tuple] = None,
error: Optional[tuple] = None,
**kwargs) -> None:
'''Метод вызывающий обработку токенов, выдаваемых парсером в
зависимости от их типа.'''
if start_section is not None:
@ -210,7 +213,7 @@ class NamespaceIniFiller:
elif error is not None:
self._set_error(error[0], 'SyntaxError', error[1])
def start_section(self, sections: str, lineno) -> None:
def start_section(self, sections: str, lineno: int) -> None:
'''Метод для получения доступа и создания пространств имен.'''
if self.restricted:
self.modify_only = sections[0] not in self.available_sections
@ -248,7 +251,7 @@ class NamespaceIniFiller:
self.current_namespace = self.current_namespace.\
_namespaces[section]
def clear_section(self, sections: list, lineno) -> None:
def clear_section(self, sections: list, lineno: int) -> None:
'''Метод для очистки пространства имен.'''
if self.restricted:
self.modify_only = sections[0] not in self.available_sections
@ -291,7 +294,7 @@ class NamespaceIniFiller:
" namespace.".format(current_namespace.
get_fullname()))
def start_table(self, sections: str, row, lineno) -> None:
def start_table(self, sections: str, row: dict, lineno: int) -> None:
'''Метод для создания и модификации таблиц.'''
if self.restricted:
self.modify_only = sections[0] not in self.available_sections
@ -335,7 +338,8 @@ class NamespaceIniFiller:
table.add_row(row)
table_variable.source = table
def define_key(self, key: str, value: str, optype, lineno) -> None:
def define_key(self, key: str, value: str, optype: Define,
lineno: int) -> None:
'''Метод для создания и модификации переменных.'''
if self.current_namespace is None:
return
@ -360,7 +364,7 @@ class NamespaceIniFiller:
else:
self.remove_value(key, value, lineno)
def change_value(self, key: str, value: str, lineno) -> None:
def change_value(self, key: str, value: str, lineno: int) -> None:
'''Метод для изменения значения переменной.'''
variable = self.current_namespace[key]
if variable.readonly:
@ -370,7 +374,7 @@ class NamespaceIniFiller:
return
variable.source = value
def define_variable(self, key: str, value: str, lineno) -> None:
def define_variable(self, key: str, value: str, lineno: int) -> None:
'''Метод для создания переменных в calculate.ini файле.'''
if not self.modify_only:
VariableNode(key, self.current_namespace, variable_type=IniType,
@ -382,7 +386,7 @@ class NamespaceIniFiller:
self.current_namespace.get_fullname(),
key))
def append_value(self, key: str, value: str, lineno) -> None:
def append_value(self, key: str, value: str, lineno: int) -> None:
'''Метод выполняющий действия возложенные на оператор +=.'''
variable = self.current_namespace[key]
if variable.readonly:
@ -416,7 +420,7 @@ class NamespaceIniFiller:
variable.source = variable_value
def remove_value(self, key: str, value: str, lineno) -> None:
def remove_value(self, key: str, value: str, lineno: int) -> None:
'''Метод выполняющий действия возложенные на оператор -=.'''
variable = self.current_namespace[key]
if variable.readonly:
@ -450,7 +454,8 @@ class NamespaceIniFiller:
variable.source = variable_value
def update_hash(self, key: str, value: str, optype, lineno):
def update_hash(self, key: str, value: str, optype: Define,
lineno: int) -> None:
'''Метод для изменения переменных хэшей через calculate.ini.'''
if self.current_namespace.readonly:
self._set_error(lineno, 'VariableError',
@ -496,12 +501,12 @@ class NamespaceIniFiller:
self.current_namespace.source = hash_to_update
def _set_error(self, lineno, error_type, line):
def _set_error(self, lineno: int, error_type: str, line: str) -> None:
'''Метод для добавления ошибки в лог.'''
self._errors.append("{}:{}: {}".format(error_type, lineno, line))
@property
def errors(self):
def errors(self) -> List[str]:
errors = self._errors
self._errors = []
return errors
@ -511,7 +516,8 @@ class VariableLoader:
'''Класс загрузчика переменных из python-файлов и из ini-файлов.'''
ini_basename = "calculate.ini"
def __init__(self, datavars, variables_path, repository_map=None):
def __init__(self, datavars: "Datavars", variables_path: str,
repository_map: Optional[Dict[str, str]] = None):
self.datavars = datavars
self.logger = datavars.logger
self.ini_filler = NamespaceIniFiller()
@ -533,7 +539,7 @@ class VariableLoader:
self.datavars.root.add_namespace(package_namespace)
self._fill_from_package(package_namespace, directory_path, package)
def load_from_profile(self):
def load_from_profile(self) -> None:
'''Метод для загрузки переменных из calculate.ini профиля.'''
# Проверяем наличие таблицы репозиториев в переменных.
if self.repository_map == {}:
@ -566,7 +572,7 @@ class VariableLoader:
profile_path))
self._fill_from_profile_ini(profile_path)
def load_user_variables(self):
def load_user_variables(self) -> None:
'''Метод для загрузки переменных из calculate.ini указанных в
переменных env_order и env_path.'''
try:
@ -625,7 +631,7 @@ class VariableLoader:
'{}.{}'.format(package,
directory_node.name))
def _fill_from_profile_ini(self, profile_path):
def _fill_from_profile_ini(self, profile_path: str) -> None:
'''Метод для зaполнения переменных из ini-файла.'''
profile_walker = ProfileWalker(self.ini_basename,
self.repository_map)
@ -637,13 +643,13 @@ class VariableLoader:
self.logger.error("Can not load profile variables from"
" unexisting file: {}".format(file_path))
def _get_repository_map(self, datavars):
def _get_repository_map(self, datavars: "Datavars") -> Dict[str, str]:
'''Метод для получения из переменной словаря с репозиториями и путями
к ним.'''
return {repo['name']: repo['path']
for repo in datavars.os.gentoo.repositories}
def fill_from_custom_ini(self, file_path: str):
def fill_from_custom_ini(self, file_path: str) -> None:
'''Метод для заполнения переменных из конкретного указанного файла.'''
if os.path.exists(file_path):
ini_file_text = read_file(file_path)
@ -661,7 +667,7 @@ class VariableLoader:
" not exist.".format(file_path))
@contextmanager
def test(self, file_name, namespace):
def test(self, file_name: str, namespace: NamespaceNode):
'''Контекстный менеджер для тестирования.'''
print('IMPORT: {}.{}'.format(namespace.get_fullname(), file_name))
try:
@ -673,7 +679,7 @@ class VariableLoader:
class CalculateIniSaver:
'''Класс для сохранения значений переменных в указанные ini-файлы.'''
def __init__(self, ini_parser=None):
def __init__(self):
self.ini_parser = CalculateIniParser()
self.operations = {Define.assign: '=',
Define.append: '+=',
@ -683,8 +689,10 @@ class CalculateIniSaver:
environment = Environment(loader=file_loader)
self.ini_template = environment.get_template('ini_template')
def save_to_ini(self, target_path, variables_to_save):
'''Метод для сохранения переменных в указанный ini-файл.'''
def save_to_ini(self, target_path: str,
variables_to_save: Dict[str, Dict[str, Any]]) -> None:
'''Метод для сохранения значений переменных, переданных в словаре, в
указанный ini-файл.'''
ini_file_text = read_file(target_path)
ini_dictionary = self._parse_ini(ini_file_text)
@ -698,7 +706,7 @@ class CalculateIniSaver:
with open(target_path, 'w') as ini_file:
ini_file.write(ini_file_text)
def _parse_ini(self, ini_file_text):
def _parse_ini(self, ini_file_text: str) -> Dict[str, Dict[str, str]]:
'''Метод для разбора текста ini-файла в словарь, в который далее будут
добавляться измененные переменные.'''
current_namespace = None
@ -725,7 +733,7 @@ class CalculateIniSaver:
line_content[1])})
return ini_dictionary
def _get_ini_text(self, ini_dictionary):
def _get_ini_text(self, ini_dictionary: Dict[str, Dict[str, Any]]) -> str:
'''Метод для получения текста ini файла, полученного в результате
наложения изменений из тегов save в шаблонах.'''
ini_text = self.ini_template.render(ini_dictionary=ini_dictionary)
@ -734,9 +742,10 @@ class CalculateIniSaver:
class Datavars:
'''Класс для хранения переменных и управления ими.'''
def __init__(self, variables_path='calculate/vars', repository_map=None,
logger=None):
self._variables_path = variables_path
def __init__(self, variables_path: str = 'calculate/vars',
repository_map: Optional[Dict[str, str]] = None,
logger: Optional[logging.Logger] = None):
self._variables_path: str = variables_path
self._available_packages = self._get_available_packages()
if logger is not None:
self.logger = logger
@ -765,15 +774,16 @@ class Datavars:
except VariableNotFoundError:
self.variables_to_save = dict()
def reset(self):
def reset(self) -> None:
'''Метод для сброса модуля переменных.'''
self.root.clear()
self.root = NamespaceNode('<root>')
self._available_packages.clear()
self._available_packages = self._get_available_packages()
self._available_packages: Dict[str,
str] = self._get_available_packages()
Namespace.set_datavars(self)
def _get_available_packages(self) -> dict:
def _get_available_packages(self) -> Dict[str, str]:
'''Метод для получения словаря с имеющимися пакетами переменных
и путями к ним.'''
variables_path = os.path.join(
@ -788,7 +798,7 @@ class Datavars:
available_packages.update({file_name: file_path})
return available_packages
def _load_package(self, package_name):
def _load_package(self, package_name: str) -> None:
'''Метод для загрузки переменных содержащихся в указанном пакете.'''
self.logger.info("Loading datavars package '{}'".format(package_name))
try:
@ -798,12 +808,12 @@ class Datavars:
format(error))
@property
def available_packages(self):
def available_packages(self) -> set:
packages = set(self._available_packages)
packages.update({'custom'})
return packages
def __getattr__(self, package_name: str):
def __getattr__(self, package_name: str) -> NamespaceNode:
'''Метод возвращает ноду пространства имен, соответствующего искомому
пакету.'''
if package_name in self.root._namespaces:
@ -819,7 +829,7 @@ class Datavars:
self._load_package(package_name)
return self.root[package_name]
def __getitem__(self, package_name: str) -> None:
def __getitem__(self, package_name: str) -> NamespaceNode:
'''Метод возвращает ноду пространства имен, соответствующего искомому
пакету.'''
if package_name in self.root:
@ -838,7 +848,7 @@ class Datavars:
self._load_package(package_name)
return self.root[package_name]
def __contains__(self, package_name):
def __contains__(self, package_name: str) -> bool:
if package_name in self.root._namespaces:
return True
elif package_name == 'custom':
@ -855,10 +865,10 @@ class Datavars:
self._load_package(package_name)
return True
def add_namespace(self, namespace_node):
def add_namespace(self, namespace_node: NamespaceNode) -> None:
self.root.add_namespace(namespace_node)
def create_tasks(self):
def create_tasks(self) -> None:
'''Метод для создания всех необходимых пространств имен для работы
задач.'''
tasks = NamespaceNode('tasks')
@ -870,10 +880,10 @@ class Datavars:
env.add_namespace('loop')
@property
def _namespaces(self):
def _namespaces(self) -> NamespaceNode:
return self.root._namespaces
def save_variables(self):
def save_variables(self) -> None:
'''Метод для сохранения значений переменных в calculate.ini файлах.'''
target_paths = self.main.cl.system.env_path
saver = CalculateIniSaver()

@ -16,7 +16,8 @@ Variable('cl_root_path', type=StringType.readonly, source='/')
Variable('cl_template_path', type=ListType.readonly,
source=['./tests/templates/testfiles/test_runner'])
Variable('cl_ignore_files', type=ListType.readonly, source=['*.swp'])
Variable('cl_ignore_files', type=ListType.readonly,
source=['*.swp', '.git'])
Variable('cl_config_path', type=StringType.readonly,
source='/var/lib/calculate/config')
@ -30,8 +31,5 @@ Variable('cl_exec_dir_path', type=StringType.readonly,
Variable('cl_resolutions', type=ListType.readonly,
source=["1680x1050", "1024x768"])
Variable('cl_resolutions', type=ListType.readonly,
source=["1680x1050", "1024x768"])
Variable('cl_image_formats', type=ListType.readonly,
source=["GIF", "JPG", "JPEG", "PNG", "GFXBOOT"])

@ -107,6 +107,7 @@ def get_isoscan_fullpath(base_path, filename):
def import_variables():
Variable("chroot_path", type=StringType,
source=Calculate(lambda x: x.value, "main.cl_chroot_path"))
Variable("root_path", type=StringType,
source=Calculate(lambda x: x.value, "main.cl_root_path"))

@ -92,16 +92,21 @@ def import_variables():
Variable("shortname", type=StringType,
source=Calculate(detect_other_shortname,
"main.cl.chroot_path"))
Variable("name", type=StringType,
source=Calculate(dict_value, dictLinuxName,
".shortname", Static("Linux")))
Variable("subname", type=StringType,
source=Calculate(dict_value, dictLinuxSubName,
".shortname", Static("")))
Variable("system", type=StringType,
source=Calculate(dict_value, dictNameSystem,
".shortname", Static("")))
Variable("ver", type=StringType,
source=Calculate(get_linux_version, "main.cl.chroot_path",
"main.os.gentoo.make_profile"))
Variable("build", type=StringType, source="")

1339
output

File diff suppressed because it is too large Load Diff

@ -46,7 +46,9 @@ markers =
scripts: marker for testing of the scripts.
commands: marker for testing of the commands.
server: marker for testing of the server.
responses: marker for testing of the responses.
chroot: marker for testing running by chroot
needs_root: marker for tests that needs root rights.

@ -1,6 +1,9 @@
from calculate.server.server import Server
import os
os.environ["TESTING"] = "True"
import uvicorn
from calculate.server.server import app
if __name__ == "__main__":
server = Server()
server.run()
uvicorn.run(app, host="127.0.0.1", port=5000, log_level="info")

@ -1,7 +1,10 @@
#! /usr/bin/python3
import argparse
from calculate.templates.template_processor import DirectoryProcessor
from calculate.variables.loader import Datavars
from calculate.utils.io_module import IOModule
from calculate.utils.package import NonePackage
from calculate.utils.tools import flat_iterable

@ -181,7 +181,7 @@ class TestParameters:
full='Test parameters are needed for tests.'),
shortname='t').bind('os.linux.fullname',
'os.linux.shortname')
.to_set('os.linux.subname'))
.set_to('os.linux.subname'))
assert PARAMS['test'].value == 'Calculate Linux Desktop -> CLD'
datavars.os.linux['fullname'].set('Gentoo Linux')
@ -189,7 +189,7 @@ class TestParameters:
datavars.os.linux['shortname'].set('GL')
assert PARAMS['test'].value == 'Gentoo Linux -> GL'
# Переменные, указанные в to_set не меняются если используется
# Переменные, указанные в set_to не меняются если используется
# дефолтное значение, рассчитанное с помощью переменных.
assert datavars.os.linux.subname == 'KDE'
@ -222,7 +222,7 @@ class TestParameters:
short='Test parameter',
full='Test parameters are needed for tests.'),
shortname='t'
).bind('os.linux.fullname').to_set(
).bind('os.linux.fullname').set_to(
'os.linux.shortname',
'os.hashvar.value2'))
@ -265,7 +265,7 @@ class TestParameters:
short='Test bool parameter',
full=('Test bool parameter, that can'
' disactivate other parameter')),
shortname='b').to_set('os.linux.test_2'),
shortname='b').set_to('os.linux.test_2'),
StringTestParameter('test-string', 'Strings',
Description(
short='Test string parameter',
@ -586,7 +586,7 @@ class TestParameters:
Description(short='Test bool',
full=('Test parameters are'
' needed for tests.')
)).to_set('os.linux.test_2'))
)).set_to('os.linux.test_2'))
assert PARAMS['test-1'].disactivity_comment is None
PARAMS.set_parameters({'test-1': 'choice_2', 'test-2': True})
@ -1001,7 +1001,7 @@ class TestParameters:
Description(short=('Test table'),
full=('Test parameters are'
' needed for tests.')),
shortname='t').to_set('os.dev_table'))
shortname='t').set_to('os.dev_table'))
assert PARAMS['test'].value.get_for_var() ==\
[{'dev': '/dev/sdb1', 'mount': '/'},
{'dev': '/dev/sdb2', 'mount': '/var/calculate'},
@ -1075,7 +1075,7 @@ class TestParameters:
Description(short=('Test table'),
full=('Test parameters are'
' needed for tests.')),
shortname='t').to_set('os.dev_table'))
shortname='t').set_to('os.dev_table'))
assert PARAMS['test'].value.get_for_var() ==\
[{'dev': '/dev/sdb1', 'mount': '/',
'name': 'Device 1', 'status': 'mounted'},

@ -1,14 +1,38 @@
import os
import shutil
import pytest
from calculate.scripts.scripts import Var, Task, Static, TaskError, Done,\
DoneAny, Success, SuccessAny, Failed,\
FailedAny, Block, For, While, Until,\
Handler, Script, Run, ScriptError,\
ActionError, ScriptLauncher, RunTemplate
from calculate.variables.datavars import Namespace, Variable, IntegerType,\
StringType, BooleanType, ListType,\
HashType
from calculate.scripts.scripts import (
Var,
Task,
Static,
TaskError,
Done,
DoneAny,
Success,
SuccessAny,
Failed,
FailedAny,
Block,
For,
While,
Until,
Handler,
Script,
Run,
ScriptError,
ActionError,
ScriptLauncher,
RunTemplates
)
from calculate.variables.datavars import (
Namespace,
Variable,
IntegerType,
StringType,
BooleanType,
ListType,
HashType
)
from calculate.variables.loader import Datavars
from calculate.utils.io_module import IOModule
@ -164,7 +188,7 @@ class TestTasks():
'success': True,
'error_message': None}
def test_if_script_object_is_created_with_one_correct_task_with_a_unfulfilled_condition__the_task_will_be_successfully_completed(self):
def test_if_script_object_is_created_with_one_correct_task_with_a_unfulfilled_condition__the_task_will_not_be_completed(self):
Namespace.reset()
datavars = Namespace.datavars
@ -1874,11 +1898,11 @@ class TestTasks():
'variables'))
Script('test_script',
).tasks(RunTemplate(id="templates_1",
action='action_1',
package="test-category/test-package",
chroot_path=TESTFILES_PATH,
root_path="/etc"),
).tasks(RunTemplates(id="templates_1",
action='action_1',
package="test-category/test-package",
chroot_path=TESTFILES_PATH,
root_path="/etc"),
).make_launcher(IOModule(), datavars, None)()
assert 'test_script' in datavars.scripts
@ -1899,11 +1923,11 @@ class TestTasks():
assert 'test_3' in datavars.os.linux
Script('test_script',
).tasks(RunTemplate(id="templates_1",
action='action_2',
package="test-category/test-package",
chroot_path=TESTFILES_PATH,
root_path="/etc"),
).tasks(RunTemplates(id="templates_1",
action='action_2',
package="test-category/test-package",
chroot_path=TESTFILES_PATH,
root_path="/etc"),
).make_launcher(IOModule(), datavars, datavars.os)()
assert 'test_script' in datavars.scripts

@ -0,0 +1,232 @@
import pytest
from calculate.server.utils.responses import ResponseStructure
@pytest.mark.responses
@pytest.mark.parametrize('case', [
{
"name": "One positional argument",
"args": ["Very important data"],
"kwargs": {},
"result": {"data": "Very important data"}
},
{
"name": "multiple positional args",
"args": [1, 2, 3],
"kwargs": {},
"result": {"data": [1, 2, 3]}
},
{
"name": "only keyword args",
"args": [],
"kwargs": {"name": "Calculate Server",
"version": "0.1"},
"result": {"data": {"name": "Calculate Server",
"version": "0.1"}}
},
{
"name": "Both positional and keyword args",
"args": [1, 2, 3],
"kwargs": {"name": "Calculate Server",
"version": "0.1"},
"result": {"data": [1, 2, 3]}
},
],
ids=lambda case: case["name"])
def test_creation_response_dictionary_using_only_add_data_method(case):
response = ResponseStructure("http://0.0.0.0:2007/")
response.add_data(*case["args"], **case["kwargs"])
assert response.get_dict() == case["result"]
@pytest.mark.responses
@pytest.mark.parametrize('case', [
{
"name": "Two calls with positional values",
"data": [
{"args": ["Very important data 1"],
"kwargs": {}},
{"args": ["Very important data 2"],
"kwargs": {}},
],
"result": {"data": ["Very important data 1",
"Very important data 2"]
},
},
{
"name": "Call with positional argument and with keywords one",
"data": [
{"args": ["Very important data"],
"kwargs": {}},
{"args": [],
"kwargs": {"name": "Calculate Server",
"version": "0.1"}},
],
"result": {"data": {"name": "Calculate Server",
"version": "0.1"}
},
},
{
"name": "Call with keyword arguments and with positional one",
"data": [
{"args": [],
"kwargs": {"name": "Calculate Server",
"version": "0.1"}},
{"args": ["Very important data"],
"kwargs": {}},
],
"result": {"data": "Very important data"},
},
{
"name": "Two calls with keyword arguments",
"data": [
{"args": [],
"kwargs": {"name": "Calculate Server"}},
{"args": [],
"kwargs": {"version": "0.1"}},
],
"result": {"data": {"name": "Calculate Server",
"version": "0.1"}
},
},
],
ids=lambda case: case["name"])
def test_multiple_add_data_method_callings(case):
response = ResponseStructure("http://0.0.0.0:2007/")
for data in case["data"]:
response.add_data(*data["args"], **data["kwargs"])
assert response.get_dict() == case["result"]
@pytest.mark.responses
@pytest.mark.parametrize('case', [
{
"name": "Passing not templated uri",
"action": "commands",
"uri": "/commands/",
"templated": False,
"result": {
"_links": {
"commands": {
"href": "http://0.0.0.0:2007/commands"
}
}
}
},
{
"name": "Passing templated uri",
"action": "find_command",
"uri": "commands/{cl_command}",
"templated": True,
"result": {
"_links": {
"find_command": {
"href": "http://0.0.0.0:2007/commands/{cl_command}",
"templated": True
}
}
}
},
],
ids=lambda case: case["name"])
def test_creation_response_dictionary_using_only_add_links_method(case):
response = ResponseStructure("http://0.0.0.0:2007/")
response.add_link(case["action"], case["uri"], case["templated"])
assert response.get_dict() == case["result"]
@pytest.mark.responses
@pytest.mark.parametrize('case', [
{
"name": "One embed method call",
"embed": {
"menu":
(ResponseStructure("http://0.0.0.0:2007/").
add_data(name="Calculate Server", version="0.1").
add_link("get_commands", "commands").
add_link("find_command", "commands/{cl_command}",
templated=True)
).get_dict(),
},
"result": {
"_embedded": {
"menu": {
"data": {
"name": "Calculate Server",
"version": "0.1"
},
"_links": {
"get_commands": {
"href": "http://0.0.0.0:2007/commands"
},
"find_command": {
"href": "http://0.0.0.0:2007/commands/{cl_command}",
"templated": True
},
}
}
}
}
},
{
"name": "Two embed method calls",
"embed": {
"first":
(ResponseStructure("http://0.0.0.0:2007/").
add_data(id=1, name="first", description="First value").
add_link("self", "values/1").
add_link("find_value", "values/search/{value_name}",
templated=True)
).get_dict(),
"second":
(ResponseStructure("http://0.0.0.0:2007/").
add_data(id=2, name="second", description="Second value").
add_link("self", "values/2").
add_link("find_value", "values/search/{value_name}",
templated=True)
).get_dict(),
},
"result": {
"_embedded": {
"first": {
"data": {
"id": 1,
"name": "first",
"description": "First value",
},
"_links": {
"self": {
"href": "http://0.0.0.0:2007/values/1"
},
"find_value": {
"href": "http://0.0.0.0:2007/values/search/{value_name}",
"templated": True
},
}
},
"second": {
"data": {
"id": 2,
"name": "second",
"description": "Second value",
},
"_links": {
"self": {
"href": "http://0.0.0.0:2007/values/2"
},
"find_value": {
"href": "http://0.0.0.0:2007/values/search/{value_name}",
"templated": True
},
}
}
}
}
},
],
ids=lambda case: case["name"])
def test_creation_response_dictionary_using_only_embed_method(case):
response = ResponseStructure("http://0.0.0.0:2007/")
for embed_name, embed_data in case["embed"].items():
response.embed(embed_name, embed_data)
assert response.get_dict() == case["result"]

@ -13,8 +13,10 @@ test_client = TestClient(app)
def authenticate(username: str, password: str):
request_headers = {"accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded"}
request_data = {"username": username, "password": password,
"grant_type": "password", "scope": None, "client_id": None,
request_data = {"username": username,
"password": password,
"grant_type": "password",
"scope": None,
"client_id": None}
token = test_client.post("/auth", data=request_data, json=request_headers)
token_header = token.json()
@ -33,29 +35,29 @@ def test_to_make_testfiles():
symlinks=True)
@pytest.mark.server
def test_get_root_message():
authorization_headers = authenticate("denis", "secret")
response = test_client.get("/", headers=authorization_headers)
assert response.status_code == 200
assert response.json() == {"msg": "root msg"}
# @pytest.mark.server
# def test_get_root_message():
# authorization_headers = authenticate("denis", "secret")
# response = test_client.get("/", headers=authorization_headers)
# assert response.status_code == 200
# assert response.json() == {"msg": "root msg"}
@pytest.mark.server
def test_get_commands_list():
authorization_headers = authenticate("denis", "secret")
response = test_client.get("/commands", headers=authorization_headers)
assert response.status_code == 200
assert response.json() == {"test_1":
{"title": "Test 1",
"category": "Test Category",
"icon": "/path/to/icon_1.png",
"command": "test_1"},
"test_2":
{"title": "Test 2",
"category": "Test Category",
"icon": "/path/to/icon_2.png",
"command": "cl_test_2"}}
# @pytest.mark.server
# def test_get_commands_list():
# authorization_headers = authenticate("denis", "secret")
# response = test_client.get("/commands", headers=authorization_headers)
# assert response.status_code == 200
# assert response.json() == {"test_1":
# {"title": "Test 1",
# "category": "Test Category",
# "icon": "/path/to/icon_1.png",
# "command": "test_1"},
# "test_2":
# {"title": "Test 2",
# "category": "Test Category",
# "icon": "/path/to/icon_2.png",
# "command": "cl_test_2"}}
# @pytest.mark.server

@ -1666,7 +1666,7 @@ def test_if_append_join_file_method_s_input_is_a_template_with_protected_target_
@pytest.mark.template_executor
def test_if_append_join_file_method_s_input_is_a_template_with_protected_target_path_to_an_unexisting_file_that_should_exist_and_autoupdate_parameters_is_set__the_method_creates_new_empty_file_joins_a_template_with_them_and_adds_it_to_config_file():
def test_if_append_join_file_method_s_input_is_a_template_with_protected_target_path_to_an_unexisting_file_that_should_exist_and_autoupdate_parameter_is_set__the_method_creates_new_empty_file_joins_a_template_with_them_and_adds_it_to_config_file():
target_path = join_paths(
CHROOT_PATH,
'/etc/append_join_file_testfiles/file_2')

@ -1,10 +1,12 @@
import pytest
import calculate.templates.template_filters as filters
class TestObj:
def __str__(self):
return "TestObj_value"
@pytest.mark.parametrize('case',
[
{

@ -56,6 +56,7 @@ dir /etc/append_link_dir_testfiles/link_dir_6
obj /etc/append_link_dir_testfiles/link_dir_6/file d41d8cd98f00b204e9800998ecf8427e 1592491327
dir /etc/append_join_file_testfiles
obj /etc/append_join_file_testfiles/file_1 ee090b452dbf92d697124eb424f5de5b 1592552158
obj /etc/append_join_file_testfiles/file_2 ee090b452dbf92d697124eb424f5de5b 1592552158
obj /etc/append_join_file_testfiles/file_4 ee090b452dbf92d697124eb424f5de5b 1592552158
obj /etc/append_join_file_testfiles/file_5 ee090b452dbf92d697124eb424f5de5b 1592574626
obj /etc/append_join_file_testfiles/file_6 ee090b452dbf92d697124eb424f5de5b 1592574626

@ -3,6 +3,7 @@ from calculate.variables.loader import CalculateIniParser, Define
@pytest.mark.vars
@pytest.mark.calculateini
class TestCalculateIni:
def test_section_values(self):
ini_parser = CalculateIniParser()

Loading…
Cancel
Save