From d331224b455efb8a7da0b58b2f5e847e0011c00b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 8 Mar 2024 12:42:22 +0000 Subject: [PATCH] Start introducing RequestResponse --- src/primaite/interface/__init__.py | 0 src/primaite/interface/request.py | 29 +++++++++++++++++++++ src/primaite/simulator/core.py | 24 ++++++++++------- src/primaite/simulator/domain/controller.py | 1 + src/primaite/simulator/sim_container.py | 4 ++- 5 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 src/primaite/interface/__init__.py create mode 100644 src/primaite/interface/request.py diff --git a/src/primaite/interface/__init__.py b/src/primaite/interface/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/interface/request.py b/src/primaite/interface/request.py new file mode 100644 index 00000000..10ce6254 --- /dev/null +++ b/src/primaite/interface/request.py @@ -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. diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 6ab7c6e3..9ea59305 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -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: """ diff --git a/src/primaite/simulator/domain/controller.py b/src/primaite/simulator/domain/controller.py index bc428743..0936b5f8 100644 --- a/src/primaite/simulator/domain/controller.py +++ b/src/primaite/simulator/domain/controller.py @@ -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]), ), ) diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index a2285d92..2f603f3a 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -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: