PyAres 1.1.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyares-1.1.0 → pyares-1.2.0}/PKG-INFO +1 -1
- pyares-1.2.0/PyAres/Analyzing/analysis_service.py +147 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Devices/device_test.py +2 -2
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Planners/planner_test.py +8 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Device/device_service.py +37 -21
- pyares-1.2.0/PyAres/Planning/planning_service.py +166 -0
- pyares-1.2.0/PyAres/Utils/ares_service_base.py +137 -0
- pyares-1.2.0/PyAres/Utils/datamodel_version_utils.py +29 -0
- pyares-1.2.0/PyAres/Utils/grpc_base.py +59 -0
- pyares-1.2.0/PyAres/Utils/logging_utils.py +17 -0
- pyares-1.2.0/PyAres/Utils/metadata_interceptors.py +69 -0
- {pyares-1.1.0 → pyares-1.2.0}/tests/test_analyzer.py +55 -10
- pyares-1.2.0/tests/test_analyzer_integration.py +86 -0
- {pyares-1.1.0 → pyares-1.2.0}/tests/test_planner.py +4 -3
- pyares-1.1.0/PyAres/Analyzing/analysis_service.py +0 -290
- pyares-1.1.0/PyAres/Planning/planning_service.py +0 -293
- {pyares-1.1.0 → pyares-1.2.0}/.github/dependabot.yml +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/.github/workflows/pypi.yaml +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/.github/workflows/python-tests.yaml +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/.gitignore +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/LICENSE +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Analyzing/__init__.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Analyzing/analyzer_models.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Analyzers/airship_analyzer.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_test.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_test_tools_demo.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_wiki.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Devices/failure_test_device.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Devices/hotplate.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Devices/random_number_device.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Devices/rotary_mixer.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Planners/airship_planner.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Planners/planner_test_tools_demo.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Planners/planner_wiki.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Device/__init__.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Device/device_models.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Device/device_warnings.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Models/__init__.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Models/ares_data_models.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Planning/__init__.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Planning/planner_models.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_data_schema_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_data_type_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_device_command_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_objective_status_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_outcome_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_plan_status_code_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_struct_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_value_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/device_status_code_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/param_history_info_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/plan_request_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/plan_response_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/planning_param_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/__init__.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/test_tools/__init__.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/test_tools/analyzer_tester.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/PyAres/test_tools/planner_tester.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/README.md +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/pyproject.toml +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/setup.cfg +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/setup.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/tests/__init__.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/tests/mock_grpc_context.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/tests/test_ares_data_schema_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/tests/test_ares_data_type_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/tests/test_ares_device.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/tests/test_ares_struct_utils.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/tests/test_ares_value_type_conversions.py +0 -0
- {pyares-1.1.0 → pyares-1.2.0}/tests/test_batch_planning_regressions.py +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Standard Imports
|
|
2
|
+
import grpc
|
|
3
|
+
from typing import Callable, Awaitable, Union, Mapping, Dict, Optional
|
|
4
|
+
|
|
5
|
+
# Import generated protobuf and gRPC stubs
|
|
6
|
+
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service
|
|
7
|
+
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2_grpc as analyzer_service_grpc
|
|
8
|
+
from ares_datamodel.analyzing import analysis_pb2
|
|
9
|
+
from ares_datamodel.analyzing import analyzer_capabilities_pb2
|
|
10
|
+
from ares_datamodel import ares_data_schema_pb2
|
|
11
|
+
from ares_datamodel import ares_outcome_enum_pb2
|
|
12
|
+
|
|
13
|
+
# Import Utilities
|
|
14
|
+
from ..Utils import ares_struct_utils
|
|
15
|
+
from ..Utils import ares_data_schema_utils
|
|
16
|
+
from ..Utils import ares_outcome_utils
|
|
17
|
+
from ..Utils.ares_service_base import AresServiceWrapperBase, AresBaseService
|
|
18
|
+
|
|
19
|
+
# Import python models
|
|
20
|
+
from ..Models import ares_data_models, RequestMetadata, AresSchemaEntry
|
|
21
|
+
from .analyzer_models import AnalysisRequest, AnalysisResponse
|
|
22
|
+
|
|
23
|
+
# Type hints for the user's custom logic
|
|
24
|
+
AnalyzeLogicFunction = Callable[[AnalysisRequest], Union[AnalysisResponse, Awaitable[AnalysisResponse]]]
|
|
25
|
+
|
|
26
|
+
class AresAnalyzerServiceWrapper(AresServiceWrapperBase, analyzer_service_grpc.AresRemoteAnalyzerServiceServicer):
|
|
27
|
+
"""
|
|
28
|
+
A wrapper around the gRPC service to expose native Python objects for analysis.
|
|
29
|
+
"""
|
|
30
|
+
def __init__(self, name: str, version: str, description: str, timeout: int, custom_analysis_logic: AnalyzeLogicFunction):
|
|
31
|
+
super().__init__(name, version, description, timeout)
|
|
32
|
+
self._custom_analysis_logic = custom_analysis_logic
|
|
33
|
+
self._analysis_parameters: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
|
|
34
|
+
|
|
35
|
+
def Analyze(self, request: analyzer_service.AnalysisRequest, context) -> analysis_pb2.Analysis:
|
|
36
|
+
print("Received an analysis request!")
|
|
37
|
+
try:
|
|
38
|
+
python_request = AnalysisRequest(
|
|
39
|
+
inputs=ares_struct_utils.ares_struct_to_dict(request.inputs),
|
|
40
|
+
settings=ares_struct_utils.ares_struct_to_dict(request.settings),
|
|
41
|
+
metadata=RequestMetadata(request.metadata))
|
|
42
|
+
|
|
43
|
+
python_response = self._custom_analysis_logic(python_request)
|
|
44
|
+
python_response = self._resolve_awaitable(python_response)
|
|
45
|
+
|
|
46
|
+
if not isinstance(python_response, AnalysisResponse):
|
|
47
|
+
print("Analysis response was an invalid type, ")
|
|
48
|
+
proto_analysis = analysis_pb2.Analysis()
|
|
49
|
+
proto_analysis.analysis_outcome = ares_outcome_enum_pb2.FAILURE
|
|
50
|
+
proto_analysis.error_string = "The user's custom analysis logic returned an invalid type, analysis cannot be processed"
|
|
51
|
+
return proto_analysis
|
|
52
|
+
|
|
53
|
+
print("Sending Analysis Response.....")
|
|
54
|
+
return analysis_pb2.Analysis(
|
|
55
|
+
result=python_response.result,
|
|
56
|
+
analysis_outcome=ares_outcome_utils.python_ares_outcome_to_proto_ares_outcome(python_response.outcome),
|
|
57
|
+
error_string=python_response.error_string
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
context.set_code(grpc.StatusCode.INTERNAL)
|
|
62
|
+
context.set_details(f"Error in custom analysis logic: {e}")
|
|
63
|
+
return analysis_pb2.Analysis(analysis_outcome=ares_outcome_enum_pb2.FAILURE, error_string=str(e))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def GetAnalysisParameters(self, request, context):
|
|
67
|
+
print("Analysis Parameters Requested")
|
|
68
|
+
try:
|
|
69
|
+
analysisParamResponse = analyzer_service.AnalysisParametersResponse()
|
|
70
|
+
|
|
71
|
+
for key, value in self._analysis_parameters.items():
|
|
72
|
+
map_entry = analysisParamResponse.parameter_schema.fields[key]
|
|
73
|
+
map_entry.CopyFrom(value)
|
|
74
|
+
|
|
75
|
+
return analysisParamResponse
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"Exception while trying to respond to ARES with analysis parameters! {e}")
|
|
79
|
+
|
|
80
|
+
def GetAnalyzerCapabilities(self, request, context) -> analyzer_capabilities_pb2.AnalyzerCapabilities:
|
|
81
|
+
print("Capabilities Requested!")
|
|
82
|
+
capabilities = analyzer_capabilities_pb2.AnalyzerCapabilities(timeout_seconds=self._timeout)
|
|
83
|
+
try:
|
|
84
|
+
for(key, value) in self._settings.items():
|
|
85
|
+
settings_entry = capabilities.settings_schema.fields[key]
|
|
86
|
+
settings_entry.CopyFrom(value)
|
|
87
|
+
|
|
88
|
+
return capabilities
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"Exception while trying to respond to ARES capabilities request! {e}")
|
|
92
|
+
return capabilities
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ValidateInputs(self, request: analyzer_service.ParameterValidationRequest, context):
|
|
96
|
+
response = analyzer_service.ParameterValidationResult(success=True)
|
|
97
|
+
provided_params: Mapping[str, ares_data_schema_pb2.AresValueSchema] = request.input_schema.fields
|
|
98
|
+
|
|
99
|
+
for stored_key, stored_schema in self._analysis_parameters.items():
|
|
100
|
+
if stored_key in provided_params:
|
|
101
|
+
matching_schema = provided_params[stored_key]
|
|
102
|
+
if stored_schema.type != matching_schema.type:
|
|
103
|
+
message = f"Schema Mismatch! {stored_key} was provided with the value type {stored_schema.type}, but the value type {matching_schema} was expected!"
|
|
104
|
+
response.messages.append(message)
|
|
105
|
+
else:
|
|
106
|
+
if not stored_schema.optional:
|
|
107
|
+
message = f"Schema Missing! {stored_key} is marked as a required piece of data for analysis, but no assignment was found in the provided schema!"
|
|
108
|
+
response.messages.append(message)
|
|
109
|
+
|
|
110
|
+
if len(response.messages) != 0:
|
|
111
|
+
response.success = False
|
|
112
|
+
|
|
113
|
+
return response
|
|
114
|
+
|
|
115
|
+
class AresAnalyzerService(AresBaseService):
|
|
116
|
+
"""
|
|
117
|
+
Manages the gRPC server for the AresAnalyzerService.
|
|
118
|
+
"""
|
|
119
|
+
def __init__(self,
|
|
120
|
+
custom_analysis_logic: AnalyzeLogicFunction,
|
|
121
|
+
name: str,
|
|
122
|
+
version: str,
|
|
123
|
+
description: str = "",
|
|
124
|
+
timeout: int = 30,
|
|
125
|
+
use_localhost: bool = True,
|
|
126
|
+
port: int = 7083,
|
|
127
|
+
max_message_size: int = -1):
|
|
128
|
+
"""
|
|
129
|
+
Initializes the AresAnalyzerService.
|
|
130
|
+
"""
|
|
131
|
+
super().__init__(
|
|
132
|
+
service_name=name,
|
|
133
|
+
description=description,
|
|
134
|
+
version=version,
|
|
135
|
+
port=port,
|
|
136
|
+
use_localhost=use_localhost,
|
|
137
|
+
max_message_size=max_message_size
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
self._service_wrapper = AresAnalyzerServiceWrapper(name=name, version=version, description=description, timeout=timeout, custom_analysis_logic=custom_analysis_logic)
|
|
141
|
+
analyzer_service_grpc.add_AresRemoteAnalyzerServiceServicer_to_server(self._service_wrapper, self.get_server())
|
|
142
|
+
|
|
143
|
+
def add_analysis_parameter(self, parameter_name: str, parameter_type: ares_data_models.AresDataType, optional: bool = False, struct_schema: Optional[Dict[str, AresSchemaEntry]] = None):
|
|
144
|
+
"""
|
|
145
|
+
Adds an analysis parameter that will be reported to ARES.
|
|
146
|
+
"""
|
|
147
|
+
self._service_wrapper._analysis_parameters[parameter_name] = ares_data_schema_utils.create_settings_schema_entry(parameter_type, optional, [], struct_schema)
|
|
@@ -30,7 +30,7 @@ if __name__ == "__main__":
|
|
|
30
30
|
device_name = "Demo Device"
|
|
31
31
|
description = "A device to demonstrate the PyAres device capabilities"
|
|
32
32
|
version = "1.0.0"
|
|
33
|
-
device_service = AresDeviceService(device.enter_safe_mode, device.get_device_state, device_name, description, version)
|
|
33
|
+
device_service = AresDeviceService(device.enter_safe_mode, device.get_device_state, device_name, description, version, False)
|
|
34
34
|
|
|
35
35
|
# Create the "Set Temperature" Command
|
|
36
36
|
quantity_schema = QuantitySchema(ureg.degree_Celsius, 40.0, 125.0)
|
|
@@ -40,7 +40,7 @@ if __name__ == "__main__":
|
|
|
40
40
|
device_service.add_new_command(set_temp_descriptor, device.set_temperature)
|
|
41
41
|
|
|
42
42
|
# Create the "Get Temperature" Command
|
|
43
|
-
output_schema = {"temperature": DeviceSchemaEntry(AresDataType.
|
|
43
|
+
output_schema = {"temperature": DeviceSchemaEntry(AresDataType.NUMBER, "The current temperature of the device")}
|
|
44
44
|
get_temp_desc = DeviceCommandDescriptor("Get Temperature", "Get's the current temperature of the demo device.", {}, output_schema)
|
|
45
45
|
device_service.add_new_command(get_temp_desc, device.get_temperature)
|
|
46
46
|
|
|
@@ -8,6 +8,14 @@ def plan(request: PlanRequest) -> PlanResponse:
|
|
|
8
8
|
gpdoods = []
|
|
9
9
|
names = []
|
|
10
10
|
|
|
11
|
+
for i in range(len(request.analysis_results)):
|
|
12
|
+
currentAnalysis = request.analysis_results[i]
|
|
13
|
+
|
|
14
|
+
for j in range(len(currentAnalysis.objectives)):
|
|
15
|
+
currentObjective : Objective = currentAnalysis.objectives[j]
|
|
16
|
+
print(f"Analysis Result {i}: Objective number {j} is named {currentObjective.objective_name} and has a value of {currentObjective.objective_value}")
|
|
17
|
+
|
|
18
|
+
|
|
11
19
|
for param in request.parameters:
|
|
12
20
|
if param.planner_name == "GPRDood":
|
|
13
21
|
gpdoods.append(param)
|
|
@@ -2,7 +2,6 @@ import grpc
|
|
|
2
2
|
import inspect
|
|
3
3
|
import time
|
|
4
4
|
import warnings
|
|
5
|
-
from concurrent import futures
|
|
6
5
|
from typing import Dict, Callable, Awaitable, Union, Any, Optional
|
|
7
6
|
|
|
8
7
|
from ares_datamodel.device.remote import ares_remote_device_service_pb2 as device_service
|
|
@@ -16,13 +15,14 @@ from google.protobuf import empty_pb2
|
|
|
16
15
|
|
|
17
16
|
from .device_models import DeviceCommandDescriptor
|
|
18
17
|
from ..Models import Limits
|
|
19
|
-
from .device_models import DeviceCommandDescriptor, DeviceCommandResponse
|
|
18
|
+
from .device_models import DeviceCommandDescriptor, DeviceCommandResponse
|
|
20
19
|
from ..Utils import ares_device_command_utils
|
|
21
20
|
from ..Utils import ares_data_schema_utils
|
|
22
21
|
from ..Utils import ares_struct_utils
|
|
23
22
|
from ..Utils import ares_value_utils
|
|
24
23
|
from ..Utils import ares_data_type_utils
|
|
25
24
|
from ..Utils import device_status_code_utils
|
|
25
|
+
from ..Utils.ares_service_base import AresServiceWrapperBase, AresBaseService
|
|
26
26
|
|
|
27
27
|
# Type hint for the user's custom methods
|
|
28
28
|
EnterSafeModeMethod = Callable[[], None]
|
|
@@ -30,15 +30,19 @@ AllowedReturns = Union[DeviceCommandResponse, Dict[str, Any], Any]
|
|
|
30
30
|
DeviceCommandMethod = Callable[..., AllowedReturns]
|
|
31
31
|
DeviceStateMethod = Callable[[], Dict[str, Any]]
|
|
32
32
|
|
|
33
|
-
class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServicer):
|
|
33
|
+
class AresDeviceServiceWrapper(AresServiceWrapperBase, device_service_grpc.AresRemoteDeviceServiceServicer):
|
|
34
34
|
"""
|
|
35
35
|
A wrapper around the gRPC service to expose native Python objects for devices
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
def __init__(self, device_name: str, description: str, version: str, enter_safe_mode: EnterSafeModeMethod, update_device_state: DeviceStateMethod):
|
|
38
|
+
def __init__(self, device_name: str, description: str, version: str, timeout: int, enter_safe_mode: EnterSafeModeMethod, update_device_state: DeviceStateMethod):
|
|
39
|
+
super().__init__(service_name=device_name, version=version, description=description, timeout=timeout)
|
|
40
|
+
|
|
41
|
+
# Preserve public attributes for backwards compatibility
|
|
39
42
|
self.device_name = device_name
|
|
40
43
|
self.description = description
|
|
41
44
|
self.version = version
|
|
45
|
+
|
|
42
46
|
self._enter_safe_mode = enter_safe_mode
|
|
43
47
|
self._update_device_state = update_device_state
|
|
44
48
|
self._setting_schema: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
|
|
@@ -48,13 +52,16 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
|
|
|
48
52
|
self._command_methods: Dict[str, Callable] = {}
|
|
49
53
|
|
|
50
54
|
def GetOperationalStatus(self, request, context) -> device_status_pb2.DeviceOperationalStatus:
|
|
51
|
-
return device_status_pb2.DeviceOperationalStatus(
|
|
55
|
+
return device_status_pb2.DeviceOperationalStatus(
|
|
56
|
+
operational_state=device_status_pb2.OperationalState.ACTIVE,
|
|
57
|
+
message=f"{self._service_name} is active!"
|
|
58
|
+
)
|
|
52
59
|
|
|
53
60
|
def GetInfo(self, request, context) -> device_service.DeviceInfoResponse:
|
|
54
61
|
info = device_service.DeviceInfoResponse()
|
|
55
|
-
info.name = self.
|
|
56
|
-
info.description = self.
|
|
57
|
-
info.version = self.
|
|
62
|
+
info.name = self._service_name
|
|
63
|
+
info.description = self._description
|
|
64
|
+
info.version = self._version
|
|
58
65
|
return info
|
|
59
66
|
|
|
60
67
|
def GetCommands(self, request, context) -> device_service.CommandsResponse:
|
|
@@ -249,7 +256,7 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
|
|
|
249
256
|
except grpc.RpcError as e:
|
|
250
257
|
print(f"gRPC error occured in device state stream")
|
|
251
258
|
|
|
252
|
-
class AresDeviceService:
|
|
259
|
+
class AresDeviceService(AresBaseService):
|
|
253
260
|
""" Manages the gRPC service for the AresDeviceSerivce """
|
|
254
261
|
def __init__(self,
|
|
255
262
|
enter_safe_mode_logic: EnterSafeModeMethod,
|
|
@@ -257,8 +264,10 @@ class AresDeviceService:
|
|
|
257
264
|
device_name: str,
|
|
258
265
|
description: str,
|
|
259
266
|
version: str,
|
|
267
|
+
timeout: int = 30,
|
|
260
268
|
use_localhost: bool = True,
|
|
261
|
-
port: int = 7100
|
|
269
|
+
port: int = 7100,
|
|
270
|
+
max_message_size: int = -1):
|
|
262
271
|
"""
|
|
263
272
|
Initializes the AresDeviceService
|
|
264
273
|
|
|
@@ -270,22 +279,28 @@ class AresDeviceService:
|
|
|
270
279
|
device_name (str): The name description of your device.
|
|
271
280
|
description (str): A brief description of your device.
|
|
272
281
|
version (str): The version associated with your device implementation.
|
|
282
|
+
timeout (int): Timeout in seconds for service calls reported to ARES.
|
|
273
283
|
use_localhost (bool): An optional value that allows the user to specify whether to host the service on the local network. Defaults to True.
|
|
274
284
|
port (int): The port that your device service will serve on. Defaults to port 7100.
|
|
285
|
+
max_message_size (int): Maximum message size for gRPC. Defaults to -1 (use library default).
|
|
275
286
|
"""
|
|
276
287
|
|
|
288
|
+
super().__init__(
|
|
289
|
+
service_name=device_name,
|
|
290
|
+
description=description,
|
|
291
|
+
version=version,
|
|
292
|
+
port=port,
|
|
293
|
+
use_localhost=use_localhost,
|
|
294
|
+
max_message_size=max_message_size
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Preserve these public attributes for backwards compatibility
|
|
277
298
|
self.device_name = device_name
|
|
278
299
|
self.description = description
|
|
279
300
|
self.version = version
|
|
280
301
|
|
|
281
|
-
self.
|
|
282
|
-
self.
|
|
283
|
-
self._service_wrapper = AresDeviceServiceWrapper(device_name, description, version, enter_safe_mode_logic, get_device_state_logic)
|
|
284
|
-
device_service_grpc.add_AresRemoteDeviceServiceServicer_to_server(self._service_wrapper, self._server)
|
|
285
|
-
if(use_localhost):
|
|
286
|
-
self._server.add_insecure_port(f'localhost:{self._port}')
|
|
287
|
-
else:
|
|
288
|
-
self._server.add_insecure_port(f'[::]:{self._port}')
|
|
302
|
+
self._service_wrapper = AresDeviceServiceWrapper(device_name, description, version, timeout, enter_safe_mode_logic, get_device_state_logic)
|
|
303
|
+
device_service_grpc.add_AresRemoteDeviceServiceServicer_to_server(self._service_wrapper, self.get_server())
|
|
289
304
|
|
|
290
305
|
def add_new_command(self, cmd_descriptor: DeviceCommandDescriptor, method):
|
|
291
306
|
"""
|
|
@@ -345,12 +360,13 @@ class AresDeviceService:
|
|
|
345
360
|
"""
|
|
346
361
|
|
|
347
362
|
print(f"Starting Ares Device Service on port {self._port}...")
|
|
348
|
-
self.
|
|
363
|
+
server = self.get_server()
|
|
364
|
+
server.start()
|
|
349
365
|
|
|
350
366
|
if wait_for_termination:
|
|
351
|
-
|
|
367
|
+
server.wait_for_termination()
|
|
352
368
|
|
|
353
369
|
def stop(self):
|
|
354
370
|
""" Stops the service, terminating the connection. """
|
|
355
371
|
print("Stopping Ares Device Service...")
|
|
356
|
-
self.
|
|
372
|
+
self.get_server().stop(0).wait()
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import grpc
|
|
2
|
+
import inspect
|
|
3
|
+
import asyncio
|
|
4
|
+
from concurrent import futures
|
|
5
|
+
from typing import Callable, Awaitable, Union, Dict
|
|
6
|
+
|
|
7
|
+
from ares_datamodel.planning.remote import ares_remote_planner_service_pb2_grpc as planner_service_grpc
|
|
8
|
+
from ares_datamodel.planning import planner_pb2
|
|
9
|
+
from ares_datamodel.planning import planner_service_capabilities_pb2
|
|
10
|
+
from ares_datamodel.planning import plan_pb2
|
|
11
|
+
from ares_datamodel import ares_data_schema_pb2
|
|
12
|
+
from ares_datamodel import ares_data_type_pb2
|
|
13
|
+
from ares_datamodel import ares_outcome_enum_pb2
|
|
14
|
+
from ares_datamodel.connection import connection_state_pb2
|
|
15
|
+
from ares_datamodel.connection import connection_info_pb2
|
|
16
|
+
from ares_datamodel import ares_struct_pb2
|
|
17
|
+
|
|
18
|
+
# Import Utilities
|
|
19
|
+
from ..Utils import ares_value_utils
|
|
20
|
+
from ..Utils import ares_data_schema_utils
|
|
21
|
+
from ..Utils import ares_data_type_utils
|
|
22
|
+
from ..Utils import ares_struct_utils
|
|
23
|
+
from ..Utils import ares_plan_status_code_utils
|
|
24
|
+
from ..Utils import plan_response_utils
|
|
25
|
+
from ..Utils.ares_service_base import AresServiceWrapperBase, AresBaseService
|
|
26
|
+
from ..Utils.logging_utils import setup_logger
|
|
27
|
+
|
|
28
|
+
# Import python models
|
|
29
|
+
from ..Models import ares_data_models, Limits
|
|
30
|
+
from .planner_models import *
|
|
31
|
+
|
|
32
|
+
# Type hint for the user's custom planning logic
|
|
33
|
+
PlanLogicFunction = Callable[[PlanRequest], Union[PlanResponse, Awaitable[PlanResponse], List[Plan], Awaitable[List[Plan]]]]
|
|
34
|
+
|
|
35
|
+
class AresPlannerServiceWrapper(AresServiceWrapperBase, planner_service_grpc.AresRemotePlannerServiceServicer):
|
|
36
|
+
"""
|
|
37
|
+
A wrapper around the gRPC service to expose native Python objects for planning
|
|
38
|
+
"""
|
|
39
|
+
def __init__(self, service_name: str, version: str, description: str, timeout: int, custom_plan_logic: PlanLogicFunction):
|
|
40
|
+
super().__init__(service_name, version, description, timeout)
|
|
41
|
+
self._custom_plan_logic: PlanLogicFunction = custom_plan_logic
|
|
42
|
+
self._current_settings: Dict[str, ares_struct_pb2.AresValue] = {}
|
|
43
|
+
self._planner_options: list[planner_pb2.Planner] = []
|
|
44
|
+
self._supported_types: list[ares_data_type_pb2.AresDataType] = []
|
|
45
|
+
|
|
46
|
+
def GetPlannerServiceCapabilities(self, request, context) -> planner_service_capabilities_pb2.PlannerServiceCapabilities:
|
|
47
|
+
print("Capabilities Requested!")
|
|
48
|
+
capabilities = planner_service_capabilities_pb2.PlannerServiceCapabilities(timeout_seconds=self._timeout)
|
|
49
|
+
capabilities.service_name = self._service_name
|
|
50
|
+
capabilities.accepted_types.extend(self._supported_types)
|
|
51
|
+
capabilities.available_planners.extend(self._planner_options)
|
|
52
|
+
|
|
53
|
+
for(key, value) in self._settings.items():
|
|
54
|
+
capabilities.settings_schema.fields[key].CopyFrom(value)
|
|
55
|
+
|
|
56
|
+
print("Capabilites Sent!")
|
|
57
|
+
return capabilities
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def Plan(self, request: plan_pb2.PlanningRequest, context) -> plan_pb2.PlanningResponse:
|
|
61
|
+
"""
|
|
62
|
+
Implements the gRPC Plan method. This method converts protobuf requests to native Python objects
|
|
63
|
+
before executing the users custom planning logic and converting their response back to protobuf.
|
|
64
|
+
"""
|
|
65
|
+
parameters = []
|
|
66
|
+
for proto_param in request.planning_parameters:
|
|
67
|
+
parameters.append(
|
|
68
|
+
PlanningParameter
|
|
69
|
+
(
|
|
70
|
+
name=proto_param.parameter_name,
|
|
71
|
+
maximum_value=proto_param.maximum_value,
|
|
72
|
+
minimum_value=proto_param.minimum_value,
|
|
73
|
+
param_history=[ParameterHistoryItem(ares_value_utils.ares_value_to_py(val.planned_value), ares_value_utils.ares_value_to_py(val.achieved_value)) for val in proto_param.parameter_history],
|
|
74
|
+
data_type=ares_data_type_utils.proto_ares_type_to_python_ares_type(proto_param.data_type),
|
|
75
|
+
is_planned=proto_param.is_planned,
|
|
76
|
+
is_result=proto_param.is_result,
|
|
77
|
+
planner_name=proto_param.planner_name,
|
|
78
|
+
initial_value=ares_value_utils.ares_value_to_py(proto_param.initial_value)
|
|
79
|
+
))
|
|
80
|
+
|
|
81
|
+
python_request = PlanRequest(parameters=parameters,
|
|
82
|
+
settings=ares_struct_utils.ares_struct_to_dict(request.adapter_settings),
|
|
83
|
+
analysis_results=list(request.analysis_results),
|
|
84
|
+
metadata=RequestMetadata(request.metadata),
|
|
85
|
+
batch_size=request.batch_size,
|
|
86
|
+
previous_plan_status_codes=[ares_plan_status_code_utils.proto_plan_status_to_python_plan_status(c) for c in request.previous_plan_status_codes])
|
|
87
|
+
|
|
88
|
+
#Handle call using the user's custom planning logic
|
|
89
|
+
response_proto = plan_pb2.PlanningResponse()
|
|
90
|
+
try:
|
|
91
|
+
python_response = self._custom_plan_logic(python_request)
|
|
92
|
+
python_response = self._resolve_awaitable(python_response)
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
#Handle errors from user's logic
|
|
96
|
+
context.set_code(grpc.StatusCode.INTERNAL)
|
|
97
|
+
context.set_details(f"Error in custom planning logic: {e}")
|
|
98
|
+
response_proto.error_string = f"{e}"
|
|
99
|
+
response_proto.planning_outcome = ares_outcome_enum_pb2.FAILURE
|
|
100
|
+
return response_proto
|
|
101
|
+
|
|
102
|
+
# This is the depricated response, and should be treated as a single plan. Maybe mention this response is depricated?
|
|
103
|
+
if isinstance(python_response, PlanResponse):
|
|
104
|
+
planned_parameters = []
|
|
105
|
+
|
|
106
|
+
for i in range(len(python_response.parameter_names)):
|
|
107
|
+
current_name = python_response.parameter_names[i]
|
|
108
|
+
current_value = python_response.parameter_values[i]
|
|
109
|
+
|
|
110
|
+
new_param = PlannedParameter(current_name, current_value)
|
|
111
|
+
planned_parameters.append(new_param)
|
|
112
|
+
|
|
113
|
+
python_plan = Plan(planned_parameters, python_response.outcome, python_response.error_string, python_response.objective_status)
|
|
114
|
+
response_proto.plans.append(plan_response_utils.python_plan_to_proto_plan(python_plan))
|
|
115
|
+
|
|
116
|
+
elif isinstance(python_response, List) and all(isinstance(item, Plan) for item in python_response):
|
|
117
|
+
response_proto.plans.extend(plan_response_utils.python_plan_to_proto_plan(p) for p in python_response)
|
|
118
|
+
|
|
119
|
+
else:
|
|
120
|
+
response_proto.error_string = "The returned response from the user planning method was not valid, users must return either a list of plans or a plan response."
|
|
121
|
+
response_proto.planning_outcome = ares_outcome_enum_pb2.FAILURE
|
|
122
|
+
|
|
123
|
+
print("Sending Plan Response.....")
|
|
124
|
+
return response_proto
|
|
125
|
+
|
|
126
|
+
class AresPlannerService(AresBaseService):
|
|
127
|
+
"""
|
|
128
|
+
Manages the gRPC server for the AresPlannerService
|
|
129
|
+
"""
|
|
130
|
+
def __init__(self, custom_plan_logic: PlanLogicFunction,
|
|
131
|
+
service_name: str,
|
|
132
|
+
service_description: str,
|
|
133
|
+
service_version: str,
|
|
134
|
+
timeout: int = 30,
|
|
135
|
+
use_localhost: bool = True,
|
|
136
|
+
port: int = 7082,
|
|
137
|
+
max_message_size: int = -1):
|
|
138
|
+
"""
|
|
139
|
+
Initializes the AresPlannerService
|
|
140
|
+
"""
|
|
141
|
+
super().__init__(
|
|
142
|
+
service_name=service_name,
|
|
143
|
+
description=service_description,
|
|
144
|
+
version=service_version,
|
|
145
|
+
port=port,
|
|
146
|
+
use_localhost=use_localhost,
|
|
147
|
+
max_message_size=max_message_size
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# For backwards compatibility with anyone accessing service_description directly
|
|
151
|
+
self.service_description = service_description
|
|
152
|
+
|
|
153
|
+
self._service_wrapper = AresPlannerServiceWrapper(service_name, service_version, service_description, timeout, custom_plan_logic)
|
|
154
|
+
planner_service_grpc.add_AresRemotePlannerServiceServicer_to_server(self._service_wrapper, self.get_server())
|
|
155
|
+
|
|
156
|
+
def add_planner_option(self, planner_name: str, planner_description: str, planner_version: str):
|
|
157
|
+
"""
|
|
158
|
+
Adds a planner option that is reported to ARES when your services capabilities are requested.
|
|
159
|
+
"""
|
|
160
|
+
self._service_wrapper._planner_options.append(planner_pb2.Planner(planner_name=planner_name, description=planner_description, version=planner_version))
|
|
161
|
+
|
|
162
|
+
def add_supported_type(self, type: ares_data_models.AresDataType):
|
|
163
|
+
"""
|
|
164
|
+
Adds the specified type to the list of value types your planenr service accepts.
|
|
165
|
+
"""
|
|
166
|
+
self._service_wrapper._supported_types.append(ares_data_type_utils.python_ares_type_to_proto_ares_type(type))
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import grpc
|
|
2
|
+
import asyncio
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any, Optional, Union, Dict
|
|
5
|
+
|
|
6
|
+
from ares_datamodel.connection import connection_state_pb2
|
|
7
|
+
from ares_datamodel.connection import connection_info_pb2
|
|
8
|
+
from ares_datamodel.connection import connection_status_pb2
|
|
9
|
+
from ares_datamodel import ares_data_schema_pb2
|
|
10
|
+
|
|
11
|
+
from .grpc_base import AresGrpcServiceBase
|
|
12
|
+
from . import ares_value_utils
|
|
13
|
+
from . import ares_data_schema_utils
|
|
14
|
+
|
|
15
|
+
class AresServiceWrapperBase:
|
|
16
|
+
"""
|
|
17
|
+
Base class for ARES gRPC service wrappers to provide common connection and info methods.
|
|
18
|
+
"""
|
|
19
|
+
def __init__(self, service_name: str, version: str, description: str, timeout: int):
|
|
20
|
+
self._service_name = service_name
|
|
21
|
+
self._version = version
|
|
22
|
+
self._description = description
|
|
23
|
+
self._timeout = timeout
|
|
24
|
+
self._settings: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
|
|
25
|
+
|
|
26
|
+
def GetInfo(self, request, context) -> connection_info_pb2.InfoResponse:
|
|
27
|
+
try:
|
|
28
|
+
return connection_info_pb2.InfoResponse(
|
|
29
|
+
name=self._service_name,
|
|
30
|
+
version=self._version,
|
|
31
|
+
description=self._description
|
|
32
|
+
)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
return connection_info_pb2.InfoResponse(
|
|
35
|
+
name="ERROR",
|
|
36
|
+
version="ERROR",
|
|
37
|
+
description=f"Error fetching information: {e}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def GetState(self, request, context) -> connection_state_pb2.StateResponse:
|
|
41
|
+
try:
|
|
42
|
+
return connection_state_pb2.StateResponse(
|
|
43
|
+
state=connection_state_pb2.State.ACTIVE,
|
|
44
|
+
state_message=f"{self._service_name} is active!"
|
|
45
|
+
)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return connection_state_pb2.StateResponse(
|
|
48
|
+
state=connection_state_pb2.State.ERROR,
|
|
49
|
+
state_message=f"Exception while trying to respond to ARES with state! {e}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def GetConnectionStatus(self, request, context) -> connection_status_pb2.ConnectionStatus:
|
|
53
|
+
try:
|
|
54
|
+
return connection_status_pb2.ConnectionStatus(
|
|
55
|
+
status=connection_status_pb2.AresStatus.CONNECTED
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
# Note: connection_status_pb2 might not have a way to return an error status in the same way.
|
|
59
|
+
# We return a disconnected status or let gRPC handle the exception.
|
|
60
|
+
return connection_status_pb2.ConnectionStatus(
|
|
61
|
+
status=connection_status_pb2.AresStatus.DISCONNECTED
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _resolve_awaitable(self, result: Any) -> Any:
|
|
65
|
+
"""
|
|
66
|
+
Helper to resolve awaitables if the custom logic is async.
|
|
67
|
+
"""
|
|
68
|
+
if inspect.isawaitable(result):
|
|
69
|
+
async def resolve(awaitable):
|
|
70
|
+
return await awaitable
|
|
71
|
+
return asyncio.run(resolve(result))
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
class AresBaseService(AresGrpcServiceBase):
|
|
75
|
+
"""
|
|
76
|
+
Base class for ARES services to handle common management logic like settings and timeouts.
|
|
77
|
+
"""
|
|
78
|
+
def __init__(self,
|
|
79
|
+
service_name: str,
|
|
80
|
+
description: str,
|
|
81
|
+
version: str,
|
|
82
|
+
port: int,
|
|
83
|
+
use_localhost: bool = True,
|
|
84
|
+
max_message_size: int = -1):
|
|
85
|
+
super().__init__(
|
|
86
|
+
service_name=service_name,
|
|
87
|
+
description=description,
|
|
88
|
+
version=version,
|
|
89
|
+
port=port,
|
|
90
|
+
use_localhost=use_localhost,
|
|
91
|
+
max_message_size=max_message_size
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def add_setting(self,
|
|
95
|
+
setting_name: str,
|
|
96
|
+
setting_type: Any,
|
|
97
|
+
default_value: Any = None,
|
|
98
|
+
optional: bool = True,
|
|
99
|
+
constraints: Union[list, None] = None,
|
|
100
|
+
struct_schema: Optional[Dict] = None,
|
|
101
|
+
limits: Optional[Any] = None,
|
|
102
|
+
description: Optional[str] = None):
|
|
103
|
+
"""
|
|
104
|
+
Adds a setting to the service wrapper's settings schema.
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
# Ensure constraints is a list if not provided
|
|
108
|
+
choices = constraints if constraints is not None else []
|
|
109
|
+
|
|
110
|
+
if default_value is not None:
|
|
111
|
+
default_ares_value = ares_value_utils.create_ares_value(default_value)
|
|
112
|
+
self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(
|
|
113
|
+
setting_type=setting_type,
|
|
114
|
+
optional=optional,
|
|
115
|
+
choices=choices,
|
|
116
|
+
struct_schema=struct_schema,
|
|
117
|
+
limits=limits,
|
|
118
|
+
default_value=default_ares_value,
|
|
119
|
+
description=description
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(
|
|
123
|
+
setting_type=setting_type,
|
|
124
|
+
optional=optional,
|
|
125
|
+
choices=choices,
|
|
126
|
+
struct_schema=struct_schema,
|
|
127
|
+
limits=limits,
|
|
128
|
+
description=description
|
|
129
|
+
)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
self._logger.error(f"Encountered an exception while adding setting {setting_name}: {e}")
|
|
132
|
+
|
|
133
|
+
def set_timeout(self, new_timeout: int):
|
|
134
|
+
"""
|
|
135
|
+
Sets the timeout for the service wrapper.
|
|
136
|
+
"""
|
|
137
|
+
self._service_wrapper._timeout = new_timeout
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from importlib import metadata
|
|
2
|
+
|
|
3
|
+
_DATAMODEL_DIST_NAME = "ares_datamodel"
|
|
4
|
+
_HEADER_NAME = "datamodel-version"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_datamodel_version() -> str:
|
|
8
|
+
"""
|
|
9
|
+
Returns the installed version of the ares_datamodel package.
|
|
10
|
+
|
|
11
|
+
Falls back to ares_datamodel.__version__ if the distribution metadata
|
|
12
|
+
is not available, and finally to "unknown" if the package cannot be imported.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
return metadata.version(_DATAMODEL_DIST_NAME)
|
|
16
|
+
except metadata.PackageNotFoundError:
|
|
17
|
+
try:
|
|
18
|
+
import ares_datamodel # type: ignore
|
|
19
|
+
|
|
20
|
+
return getattr(ares_datamodel, "__version__", "unknown")
|
|
21
|
+
except ImportError:
|
|
22
|
+
return "unknown"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_datamodel_metadata_header() -> tuple[str, str]:
|
|
26
|
+
"""
|
|
27
|
+
Returns the (key, value) tuple for the outgoing gRPC metadata header.
|
|
28
|
+
"""
|
|
29
|
+
return _HEADER_NAME, get_datamodel_version()
|