Fixed force parameter. Added new worker ipc, some pydantic response and requests schemas and server api description.
parent
91690f73e2
commit
c0a552a1cf
@ -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 @@
|
||||
client.py
|
@ -0,0 +1 @@
|
||||
client.py
|
@ -0,0 +1 @@
|
||||
client.py
|
@ -0,0 +1 @@
|
||||
client.py
|
@ -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,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,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)
|
@ -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
|
@ -0,0 +1,5 @@
|
||||
from multiprocessing.connection import Listener
|
||||
|
||||
|
||||
def daemon(listener: Listener, wid: int, base_dir: str, command):
|
||||
pass
|
@ -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,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")
|
||||
|
@ -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"]
|
Loading…
Reference in new issue