Start introducing RequestResponse

This commit is contained in:
Marek Wolan
2024-03-08 12:42:22 +00:00
parent 59bbc0e733
commit d331224b45
5 changed files with 47 additions and 11 deletions

View File

View File

@@ -0,0 +1,29 @@
from typing import Dict, Literal
from pydantic import BaseModel, ConfigDict
class RequestResponse(BaseModel):
"""Schema for generic request responses."""
model_config = ConfigDict(extra="forbid")
"""Cannot have extra fields in the response. Anything custom goes into the data field."""
status: Literal["pending", "success", "failure"] = "pending"
"""
What is the current status of the request:
- pending - the request has not been received yet, or it has been received but it's still being processed.
- success - the request has successfully been received and processed.
- failure - the request could not reach it's intended target or it was rejected.
Note that the failure status should only be used when the request cannot be processed, for instance when the
target SimComponent doesn't exist, or is in an OFF state that prevents it from accepting requests. If the
request is received by the target and the associated action is executed, but couldn't be completed due to
downstream factors, the request was still successfully received, it's just that the result wasn't what was
intended.
"""
data: Dict = {}
"""Catch-all place to provide any additional data that was generated as a response to the request."""
# TODO: currently, status and data have default values, because I don't want to interrupt existing functionality too
# much. However, in the future we might consider making them mandatory.

View File

@@ -1,12 +1,13 @@
# flake8: noqa
"""Core of the PrimAITE Simulator."""
from abc import abstractmethod
from typing import Callable, Dict, List, Optional, Union
from typing import Callable, Dict, List, Literal, Optional, Union
from uuid import uuid4
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, validate_call
from primaite import getLogger
from primaite.interface.request import RequestResponse
_LOGGER = getLogger(__name__)
@@ -22,7 +23,7 @@ class RequestPermissionValidator(BaseModel):
@abstractmethod
def __call__(self, request: List[str], context: Dict) -> bool:
"""Use the request and context paramters to decide whether the request should be permitted."""
"""Use the request and context parameters to decide whether the request should be permitted."""
pass
@@ -42,7 +43,7 @@ class RequestType(BaseModel):
the request can be performed or not.
"""
func: Callable[[List[str], Dict], None]
func: Callable[[List[str], Dict], RequestResponse]
"""
``func`` is a function that accepts a request and a context dict. Typically this would be a lambda function
that invokes a class method of your SimComponent. For example if the component is a node and the request type is for
@@ -71,7 +72,8 @@ class RequestManager(BaseModel):
request_types: Dict[str, RequestType] = {}
"""maps request name to an RequestType object."""
def __call__(self, request: Callable[[List[str], Dict], None], context: Dict) -> None:
@validate_call
def __call__(self, request: List[str], context: Dict) -> RequestResponse:
"""
Process an request request.
@@ -84,23 +86,25 @@ class RequestManager(BaseModel):
:raises RuntimeError: If the request parameter does not have a valid request name as the first item.
"""
request_key = request[0]
request_options = request[1:]
if request_key not in self.request_types:
msg = (
f"Request {request} could not be processed because {request_key} is not a valid request name",
"within this RequestManager",
)
_LOGGER.error(msg)
raise RuntimeError(msg)
# _LOGGER.error(msg)
# raise RuntimeError(msg)
_LOGGER.debug(msg)
return RequestResponse(status="failure", data={"reason": msg})
request_type = self.request_types[request_key]
request_options = request[1:]
if not request_type.validator(request_options, context):
_LOGGER.debug(f"Request {request} was denied due to insufficient permissions")
return
return RequestResponse(status="failure", data={"reason": "request validation failed"})
request_type.func(request_options, context)
return request_type.func(request_options, context)
def add_request(self, name: str, request_type: RequestType) -> None:
"""

View File

@@ -87,6 +87,7 @@ class DomainController(SimComponent):
"account",
RequestType(
func=lambda request, context: self.accounts[request.pop(0)].apply_request(request, context),
# TODO: not sure what should get returned here, revisit
validator=GroupMembershipValidator(allowed_groups=[AccountGroup.DOMAIN_ADMIN]),
),
)

View File

@@ -1,5 +1,6 @@
from typing import Dict
from primaite.interface.request import RequestResponse
from primaite.simulator.core import RequestManager, RequestType, SimComponent
from primaite.simulator.domain.controller import DomainController
from primaite.simulator.network.container import Network
@@ -31,7 +32,8 @@ class Simulation(SimComponent):
rm.add_request("network", RequestType(func=self.network._request_manager))
# pass through domain requests to the domain object
rm.add_request("domain", RequestType(func=self.domain._request_manager))
rm.add_request("do_nothing", RequestType(func=lambda request, context: ()))
# if 'do_nothing' is requested, just return a success
rm.add_request("do_nothing", RequestType(func=lambda request, context: RequestResponse(status="success")))
return rm
def describe_state(self) -> Dict: