Start introducing RequestResponse
This commit is contained in:
0
src/primaite/interface/__init__.py
Normal file
0
src/primaite/interface/__init__.py
Normal file
29
src/primaite/interface/request.py
Normal file
29
src/primaite/interface/request.py
Normal 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.
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
"""Core of the PrimAITE Simulator."""
|
"""Core of the PrimAITE Simulator."""
|
||||||
from abc import abstractmethod
|
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 uuid import uuid4
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field, validate_call
|
||||||
|
|
||||||
from primaite import getLogger
|
from primaite import getLogger
|
||||||
|
from primaite.interface.request import RequestResponse
|
||||||
|
|
||||||
_LOGGER = getLogger(__name__)
|
_LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ class RequestPermissionValidator(BaseModel):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __call__(self, request: List[str], context: Dict) -> bool:
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ class RequestType(BaseModel):
|
|||||||
the request can be performed or not.
|
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
|
``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
|
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] = {}
|
request_types: Dict[str, RequestType] = {}
|
||||||
"""maps request name to an RequestType object."""
|
"""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.
|
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.
|
:raises RuntimeError: If the request parameter does not have a valid request name as the first item.
|
||||||
"""
|
"""
|
||||||
request_key = request[0]
|
request_key = request[0]
|
||||||
|
request_options = request[1:]
|
||||||
|
|
||||||
if request_key not in self.request_types:
|
if request_key not in self.request_types:
|
||||||
msg = (
|
msg = (
|
||||||
f"Request {request} could not be processed because {request_key} is not a valid request name",
|
f"Request {request} could not be processed because {request_key} is not a valid request name",
|
||||||
"within this RequestManager",
|
"within this RequestManager",
|
||||||
)
|
)
|
||||||
_LOGGER.error(msg)
|
# _LOGGER.error(msg)
|
||||||
raise RuntimeError(msg)
|
# raise RuntimeError(msg)
|
||||||
|
_LOGGER.debug(msg)
|
||||||
|
return RequestResponse(status="failure", data={"reason": msg})
|
||||||
|
|
||||||
request_type = self.request_types[request_key]
|
request_type = self.request_types[request_key]
|
||||||
request_options = request[1:]
|
|
||||||
|
|
||||||
if not request_type.validator(request_options, context):
|
if not request_type.validator(request_options, context):
|
||||||
_LOGGER.debug(f"Request {request} was denied due to insufficient permissions")
|
_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:
|
def add_request(self, name: str, request_type: RequestType) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class DomainController(SimComponent):
|
|||||||
"account",
|
"account",
|
||||||
RequestType(
|
RequestType(
|
||||||
func=lambda request, context: self.accounts[request.pop(0)].apply_request(request, context),
|
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]),
|
validator=GroupMembershipValidator(allowed_groups=[AccountGroup.DOMAIN_ADMIN]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
from primaite.interface.request import RequestResponse
|
||||||
from primaite.simulator.core import RequestManager, RequestType, SimComponent
|
from primaite.simulator.core import RequestManager, RequestType, SimComponent
|
||||||
from primaite.simulator.domain.controller import DomainController
|
from primaite.simulator.domain.controller import DomainController
|
||||||
from primaite.simulator.network.container import Network
|
from primaite.simulator.network.container import Network
|
||||||
@@ -31,7 +32,8 @@ class Simulation(SimComponent):
|
|||||||
rm.add_request("network", RequestType(func=self.network._request_manager))
|
rm.add_request("network", RequestType(func=self.network._request_manager))
|
||||||
# pass through domain requests to the domain object
|
# pass through domain requests to the domain object
|
||||||
rm.add_request("domain", RequestType(func=self.domain._request_manager))
|
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
|
return rm
|
||||||
|
|
||||||
def describe_state(self) -> Dict:
|
def describe_state(self) -> Dict:
|
||||||
|
|||||||
Reference in New Issue
Block a user