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.
Files changed (70) hide show
  1. {pyares-1.1.0 → pyares-1.2.0}/PKG-INFO +1 -1
  2. pyares-1.2.0/PyAres/Analyzing/analysis_service.py +147 -0
  3. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Devices/device_test.py +2 -2
  4. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Planners/planner_test.py +8 -0
  5. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Device/device_service.py +37 -21
  6. pyares-1.2.0/PyAres/Planning/planning_service.py +166 -0
  7. pyares-1.2.0/PyAres/Utils/ares_service_base.py +137 -0
  8. pyares-1.2.0/PyAres/Utils/datamodel_version_utils.py +29 -0
  9. pyares-1.2.0/PyAres/Utils/grpc_base.py +59 -0
  10. pyares-1.2.0/PyAres/Utils/logging_utils.py +17 -0
  11. pyares-1.2.0/PyAres/Utils/metadata_interceptors.py +69 -0
  12. {pyares-1.1.0 → pyares-1.2.0}/tests/test_analyzer.py +55 -10
  13. pyares-1.2.0/tests/test_analyzer_integration.py +86 -0
  14. {pyares-1.1.0 → pyares-1.2.0}/tests/test_planner.py +4 -3
  15. pyares-1.1.0/PyAres/Analyzing/analysis_service.py +0 -290
  16. pyares-1.1.0/PyAres/Planning/planning_service.py +0 -293
  17. {pyares-1.1.0 → pyares-1.2.0}/.github/dependabot.yml +0 -0
  18. {pyares-1.1.0 → pyares-1.2.0}/.github/workflows/pypi.yaml +0 -0
  19. {pyares-1.1.0 → pyares-1.2.0}/.github/workflows/python-tests.yaml +0 -0
  20. {pyares-1.1.0 → pyares-1.2.0}/.gitignore +0 -0
  21. {pyares-1.1.0 → pyares-1.2.0}/LICENSE +0 -0
  22. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Analyzing/__init__.py +0 -0
  23. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Analyzing/analyzer_models.py +0 -0
  24. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Analyzers/airship_analyzer.py +0 -0
  25. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_test.py +0 -0
  26. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_test_tools_demo.py +0 -0
  27. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_wiki.py +0 -0
  28. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Devices/failure_test_device.py +0 -0
  29. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Devices/hotplate.py +0 -0
  30. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Devices/random_number_device.py +0 -0
  31. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Devices/rotary_mixer.py +0 -0
  32. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Planners/airship_planner.py +0 -0
  33. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Planners/planner_test_tools_demo.py +0 -0
  34. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Demo/Planners/planner_wiki.py +0 -0
  35. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Device/__init__.py +0 -0
  36. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Device/device_models.py +0 -0
  37. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Device/device_warnings.py +0 -0
  38. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Models/__init__.py +0 -0
  39. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Models/ares_data_models.py +0 -0
  40. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Planning/__init__.py +0 -0
  41. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Planning/planner_models.py +0 -0
  42. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_data_schema_utils.py +0 -0
  43. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_data_type_utils.py +0 -0
  44. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_device_command_utils.py +0 -0
  45. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_objective_status_utils.py +0 -0
  46. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_outcome_utils.py +0 -0
  47. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_plan_status_code_utils.py +0 -0
  48. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_struct_utils.py +0 -0
  49. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/ares_value_utils.py +0 -0
  50. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/device_status_code_utils.py +0 -0
  51. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/param_history_info_utils.py +0 -0
  52. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/plan_request_utils.py +0 -0
  53. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/plan_response_utils.py +0 -0
  54. {pyares-1.1.0 → pyares-1.2.0}/PyAres/Utils/planning_param_utils.py +0 -0
  55. {pyares-1.1.0 → pyares-1.2.0}/PyAres/__init__.py +0 -0
  56. {pyares-1.1.0 → pyares-1.2.0}/PyAres/test_tools/__init__.py +0 -0
  57. {pyares-1.1.0 → pyares-1.2.0}/PyAres/test_tools/analyzer_tester.py +0 -0
  58. {pyares-1.1.0 → pyares-1.2.0}/PyAres/test_tools/planner_tester.py +0 -0
  59. {pyares-1.1.0 → pyares-1.2.0}/README.md +0 -0
  60. {pyares-1.1.0 → pyares-1.2.0}/pyproject.toml +0 -0
  61. {pyares-1.1.0 → pyares-1.2.0}/setup.cfg +0 -0
  62. {pyares-1.1.0 → pyares-1.2.0}/setup.py +0 -0
  63. {pyares-1.1.0 → pyares-1.2.0}/tests/__init__.py +0 -0
  64. {pyares-1.1.0 → pyares-1.2.0}/tests/mock_grpc_context.py +0 -0
  65. {pyares-1.1.0 → pyares-1.2.0}/tests/test_ares_data_schema_utils.py +0 -0
  66. {pyares-1.1.0 → pyares-1.2.0}/tests/test_ares_data_type_utils.py +0 -0
  67. {pyares-1.1.0 → pyares-1.2.0}/tests/test_ares_device.py +0 -0
  68. {pyares-1.1.0 → pyares-1.2.0}/tests/test_ares_struct_utils.py +0 -0
  69. {pyares-1.1.0 → pyares-1.2.0}/tests/test_ares_value_type_conversions.py +0 -0
  70. {pyares-1.1.0 → pyares-1.2.0}/tests/test_batch_planning_regressions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAres
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: The official Python library for ARES.
5
5
  Project-URL: Homepage, https://github.com/AFRL-ARES/PyAres
6
6
  Author-email: AFRL <AFRL.RX.AREScontact@us.af.mil>
@@ -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.QUANTITY, "The current temperature of the device", quantity_schema=quantity_schema)}
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, StatusCode
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(operational_state=device_status_pb2.OperationalState.ACTIVE, message=f"{self.device_name} is active!")
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.device_name
56
- info.description = self.description
57
- info.version = self.version
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._port = port
282
- self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
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._server.start()
363
+ server = self.get_server()
364
+ server.start()
349
365
 
350
366
  if wait_for_termination:
351
- self._server.wait_for_termination()
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._server.stop(0).wait()
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()