PyAres 0.1.4__py3-none-any.whl

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.
@@ -0,0 +1,9 @@
1
+ from .analysis_service import AresAnalyzerService
2
+ from .analyzer_models import Analysis, AnalysisRequest, InfoResponse
3
+
4
+ __all__ = [
5
+ "Analysis",
6
+ "AnalysisRequest",
7
+ "InfoResponse",
8
+ "AresAnalyzerService",
9
+ ]
@@ -0,0 +1,263 @@
1
+ # Standard Imports
2
+ import grpc
3
+ from concurrent import futures
4
+ from typing import Callable, Awaitable, Union, Mapping, Dict
5
+
6
+ # Import generated protobuf and gRPC stubs
7
+ from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service
8
+ from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2_grpc as analyzer_service_grpc
9
+ from ares_datamodel.analyzing import analysis_pb2
10
+ from ares_datamodel.analyzing import analyzer_capabilities_pb2
11
+ from ares_datamodel.connection import connection_state_pb2
12
+ from ares_datamodel.connection import connection_status_pb2
13
+ from ares_datamodel.connection import connection_info_pb2
14
+ from ares_datamodel import ares_data_type_pb2
15
+ from ares_datamodel import ares_data_schema_pb2
16
+ from ares_datamodel import ares_outcome_enum_pb2
17
+
18
+ # Import Utilities
19
+ from ..Utils import ares_struct_utils
20
+ from ..Utils import ares_data_schema_utils
21
+ from ..Utils import ares_outcome_utils
22
+
23
+ # Import python models
24
+ from ..Models import ares_data_models, RequestMetadata
25
+ from .analyzer_models import AnalysisRequest, Analysis, InfoResponse
26
+
27
+ # Type hints for the user's custom logic
28
+ AnalyzeLogicFunction = Callable[[AnalysisRequest], Union[Analysis, Awaitable[Analysis]]]
29
+
30
+ class AresAnalyzerServiceWrapper(analyzer_service_grpc.AresRemoteAnalyzerServiceServicer):
31
+ """
32
+ A wrapper around the gRPC service to expose native Python objects for analysis.
33
+ """
34
+ def __init__(self, info: InfoResponse, timeout: int, custom_analysis_logic: AnalyzeLogicFunction):
35
+ self._info = info
36
+ self._timeout = timeout
37
+ self._custom_analysis_logic = custom_analysis_logic
38
+ self._settings: Dict[str, ares_data_schema_pb2.SchemaEntry] = {}
39
+ self._analysis_parameters: Dict[str, ares_data_schema_pb2.SchemaEntry] = {}
40
+
41
+ def GetInfo(self, request, context) -> connection_info_pb2.InfoResponse:
42
+ print("Info Requested!")
43
+ try:
44
+ response = connection_info_pb2.InfoResponse(
45
+ name=self._info.name,
46
+ version=self._info.version,
47
+ description=self._info.description)
48
+
49
+ return response
50
+
51
+ except Exception as e:
52
+ response = connection_info_pb2.InfoResponse(
53
+ name="ERROR",
54
+ version="ERROR",
55
+ description="Error fetching information"
56
+ )
57
+ print(f"Exception while trying to respond to ARES with information! {e}")
58
+ return response
59
+
60
+
61
+ def Analyze(self, request: analyzer_service.AnalysisRequest, context) -> analysis_pb2.Analysis:
62
+ print("Received an analysis request!")
63
+ try:
64
+ python_request = AnalysisRequest(
65
+ inputs=ares_struct_utils.ares_struct_to_dict(request.inputs),
66
+ settings=ares_struct_utils.ares_struct_to_dict(request.settings),
67
+ metadata=RequestMetadata(request.metadata)
68
+ )
69
+
70
+ proto_analysis = analysis_pb2.Analysis()
71
+ python_response = self._custom_analysis_logic(python_request)
72
+ if isinstance(python_response, Awaitable):
73
+ python_response = python_response.__await__()
74
+
75
+ if not isinstance(python_response, Analysis):
76
+ print("Analysis response was an invalid type, ")
77
+ proto_analysis.analysis_outcome = ares_outcome_enum_pb2.FAILURE
78
+ proto_analysis.error_string = "The user's custom analysis logic returned an invalid type, analysis cannot be processed"
79
+ return proto_analysis
80
+
81
+ print("Sending Analysis Response.....")
82
+ return analysis_pb2.Analysis(
83
+ result=python_response.result,
84
+ analysis_outcome=ares_outcome_utils.python_ares_outcome_to_proto_ares_outcome(python_response.outcome),
85
+ error_string=python_response.error_string
86
+ )
87
+
88
+ except Exception as e:
89
+ context.set_code(grpc.StatusCode.INTERNAL)
90
+ context.set_details(f"Error in custom analysis logic: {e}")
91
+ return analysis_pb2.Analysis(analysis_outcome=ares_outcome_enum_pb2.FAILURE, error_string=str(e))
92
+
93
+ def GetState(self, request, context) -> connection_state_pb2.StateResponse:
94
+ try:
95
+ analyzer_state = connection_state_pb2.StateResponse(state=connection_state_pb2.State.ACTIVE)
96
+ return analyzer_state
97
+
98
+ except Exception as e:
99
+ print(f"{e}")
100
+ return connection_state_pb2.StateResponse(state=connection_state_pb2.State.ERROR, state_message=f"Exception while trying to respond to ARES with state! {e}")
101
+
102
+
103
+ def GetAnalysisParameters(self, request, context):
104
+ print("Analysis Parameters Requested")
105
+ try:
106
+ analysisParamResponse = analyzer_service.AnalysisParametersResponse()
107
+
108
+ for key, value in self._analysis_parameters.items():
109
+ map_entry = analysisParamResponse.parameter_schema.fields[key]
110
+ map_entry.CopyFrom(value)
111
+
112
+ return analysisParamResponse
113
+
114
+ except Exception as e:
115
+ print(f"Exception while trying to respond to ARES with analysis parameters! {e}")
116
+
117
+ def GetAnalyzerCapabilities(self, request, context) -> analyzer_capabilities_pb2.AnalyzerCapabilities:
118
+ print("Capabilities Requested!")
119
+ capabilities = analyzer_capabilities_pb2.AnalyzerCapabilities(timeout_seconds=self._timeout)
120
+ try:
121
+ for(key, value) in self._settings.items():
122
+ settings_entry = capabilities.settings_schema.fields[key]
123
+ settings_entry.type = value.type
124
+ settings_entry.optional = value.optional
125
+
126
+ if len(value.string_choices.strings) != 0:
127
+ settings_entry.string_choices.strings.extend(value.string_choices.strings)
128
+
129
+ elif len(value.number_choices.numbers) != 0:
130
+ settings_entry.number_choices.numbers.extend(value.number_choices.numbers)
131
+
132
+ return capabilities
133
+
134
+ except Exception as e:
135
+ print(f"Exception while trying to respond to ARES capabilities request! {e}")
136
+ return capabilities
137
+
138
+ def GetConnectionStatus(self, request, context):
139
+ try:
140
+ return connection_status_pb2.ConnectionStatus(status=connection_status_pb2.AresStatus.CONNECTED)
141
+
142
+ except Exception as e:
143
+ print(f"Exception while trying to respond to ARES with connection status! {e}")
144
+
145
+ def ValidateInputs(self, request: analyzer_service.ParameterValidationRequest, context):
146
+ print("Validating Inputs")
147
+ response = analyzer_service.ParameterValidationResult(success=True)
148
+ provided_params: Mapping[str, ares_data_type_pb2.AresDataType] = request.input_schema.fields
149
+
150
+ for stored_key, stored_schema in self._analysis_parameters.items():
151
+ if stored_key in provided_params:
152
+ matching_schema = provided_params.get(stored_key)
153
+
154
+ if stored_schema.type != matching_schema:
155
+ message = f"Schema Mismatch! {stored_key} was provided with the value type {stored_schema.type}, but the value type {matching_schema} was expected!"
156
+ response.messages.append(message)
157
+ print(message)
158
+ else:
159
+ if not stored_schema.optional:
160
+ 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!"
161
+ response.messages.append(message)
162
+ print(message)
163
+
164
+ if response.messages.count != 0:
165
+ response.success = False
166
+
167
+ return response
168
+
169
+ class AresAnalyzerService:
170
+ """
171
+ Manages the gRPC server for the AresAnalyzerService.
172
+ """
173
+ def __init__(self,
174
+ custom_analysis_logic: AnalyzeLogicFunction,
175
+ name: str,
176
+ version: str,
177
+ description: str = "",
178
+ timeout: int = 30,
179
+ use_localhost: bool = True,
180
+ port: int = 7083):
181
+ """
182
+ Initializes the AresAnalyzerService.
183
+
184
+ Args:
185
+ custom_analysis_logic (`AnalyzeLogicFunction`): A callable function that will be executed when an Analysis request is received.
186
+ This function should accept a `PyAres.Analyzing.AnalysisRequest` object and return a
187
+ `PyAres.Analyzing.Analysis` object (or an awaitable that resolves to one).
188
+ name (str): The name of your analyzer.
189
+ version (str): The version of your analyzer.
190
+ description (str): A brief description of your analyzer.
191
+ use_localhost (bool): If true, binds to localhost. Otherwise, binds to [::].
192
+ port (int): The port that your analyzer service will serve on. Defaults to port 7083.
193
+ """
194
+ self.info = InfoResponse(name=name, version=version, description=description)
195
+ self._capabilities = analyzer_capabilities_pb2.AnalyzerCapabilities(settings_schema={})
196
+ self._port = port
197
+ self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
198
+ self._service_wrapper = AresAnalyzerServiceWrapper(info=self.info, timeout=timeout, custom_analysis_logic=custom_analysis_logic)
199
+ analyzer_service_grpc.add_AresRemoteAnalyzerServiceServicer_to_server(self._service_wrapper, self._server)
200
+
201
+ if use_localhost:
202
+ self._server.add_insecure_port(f'localhost:{self._port}')
203
+ else:
204
+ self._server.add_insecure_port(f'[::]:{self._port}')
205
+
206
+ def add_setting(self, setting_name: str, setting_type: ares_data_models.AresDataType, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = []):
207
+ """
208
+ Adds an analyzer setting to be reported to ARES when capabilities are requested.
209
+ While most `PyAres.Models.AresDataType` options are supported, bool arrays and byte arrays
210
+ cannot be used as the type for your setting values.
211
+
212
+ Args:
213
+ setting_name (str): The name of the setting.
214
+ setting_type (AresDataType): The type of this settings value.
215
+ optional (bool): Whether the setting is optional.
216
+ constraints: An optional list of values to constrain the available setting choices. Can be integers, strings, or floats.
217
+ """
218
+ self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints)
219
+
220
+ def add_analysis_parameter(self, parameter_name: str, parameter_type: ares_data_models.AresDataType, optional: bool = False):
221
+ """
222
+ Adds an analysis parameter that will be reported to ARES. Analysis parameters are inputs your analyzer accepts from ARES, and will be mapped to command outputs
223
+ in experiment scripts.
224
+
225
+ Args:
226
+ parameter_name (str): The name of the parameter being created.
227
+ parameter_type (AresDataType): The type associated with the new parameter.
228
+ optional (bool): Defaults to false. Determines whether your analyzer requires this information.
229
+ """
230
+ self._service_wrapper._analysis_parameters[parameter_name] = ares_data_schema_utils.create_settings_schema_entry(parameter_type, optional, [])
231
+
232
+ def set_timeout(self, new_timeout: int):
233
+ """
234
+ Sets the time, in seconds, that ARES will wait to receive a response from this service.
235
+
236
+ Args:
237
+ new_timeout: The new timeout value in seconds.
238
+ """
239
+ self._capabilities.timeout_seconds = new_timeout
240
+
241
+ def start(self):
242
+ """
243
+ Starts the service on the specified port, and waits for termination.
244
+ """
245
+ print(f"Starting Ares Analyzer Service on port {self._port}...")
246
+ self._server.start()
247
+ self._server.wait_for_termination()
248
+
249
+ def stop(self):
250
+ """
251
+ Stops the service, terminating the connection.
252
+ """
253
+ print("Stopping Ares Analyzer Service...")
254
+ self._server.stop(0).wait()
255
+
256
+
257
+
258
+
259
+
260
+
261
+
262
+
263
+
@@ -0,0 +1,47 @@
1
+ from typing import Dict, Any
2
+ from ..Models import Outcome, RequestMetadata
3
+
4
+ class AnalysisRequest:
5
+ """ Represents an analysis request received from ARES. """
6
+
7
+ def __init__(self, inputs: Dict[str, Any], settings: Dict[str, Any], metadata: RequestMetadata):
8
+ self.inputs = inputs
9
+ self.settings = settings
10
+ self.request_metadata = metadata
11
+
12
+ class Analysis:
13
+ """ Represents the result of an analysis process. """
14
+
15
+ def __init__(self, result: float, outcome: Outcome = Outcome.SUCCESS, error_string: str = ""):
16
+ """
17
+ Initializes an Analysis message
18
+
19
+ Args:
20
+ result: The value your analyzer returns as the result of the experiment being analyzed. Represented as a float.
21
+ success: A boolean value that represents whether analysis was done successfully.
22
+ error_string: An optional string argument for passing why analysis failed to ARES. Will default to an empty string if no value is provided.
23
+ """
24
+ self.result = result
25
+ self.outcome = outcome
26
+ self.error_string = error_string
27
+
28
+
29
+ class InfoResponse:
30
+ """ A response message that provides basic information about your analyzer. """
31
+
32
+ def __init__(self, name: str, version: str, description: str = ""):
33
+ """
34
+ Initializes a new InfoResponse message.
35
+
36
+ Args:
37
+ name: The name of your analyzer, to be displayed in ARES.
38
+ version: The specific version of your analyzer. This information is saved in experiment results that use your analyzer.
39
+ description: An optional (but recommended) string that gives a basic description of your analyzer.
40
+ """
41
+
42
+ self.name = name
43
+ self.version = version
44
+ self.description = description
45
+
46
+
47
+
@@ -0,0 +1,3 @@
1
+ from .device_models import DeviceCommandDescriptor
2
+ from .device_models import DeviceSchemaEntry
3
+ from .device_service import AresDeviceService
@@ -0,0 +1,40 @@
1
+ from typing import Dict, Union
2
+ from ..Models import ares_data_models
3
+
4
+ class DeviceSchemaEntry:
5
+ """ A class that describes an input or output parameter for a device command """
6
+
7
+ def __init__(self, type: ares_data_models.AresDataType, description: str = "", unit: str = "", optional: bool = False, constraints: Union[list[int], list[float], list[str]] = []):
8
+ """
9
+ Initializes a new DeviceSchemaEntry
10
+
11
+ Args:
12
+ type ('ares_data_models.AresDataType'): An AresDataType that describes the type associated with this schema entry
13
+ description (str): A description of the given schema entry
14
+ unit (str): The unit associated with this schema entry
15
+ optional (bool): A boolean value that determines whether or not this schema entry's inclusion is optional
16
+ contraints (Union[list[int], list[float], list[str]]): An optional list of contraints to limit the number of choices available for this schema entry
17
+ """
18
+
19
+ self.type = type
20
+ self.optional = optional
21
+ self.description = description
22
+ self.unit = unit
23
+ self.contraints = constraints
24
+
25
+ class DeviceCommandDescriptor:
26
+ """ A class that contains all the necessary information to describe a device command """
27
+ def __init__(self, name: str, description: str, input_schema: Dict[str, DeviceSchemaEntry], output_schema: Dict[str, DeviceSchemaEntry]):
28
+ """
29
+ Initializes a new instance of the device command descriptor class.
30
+
31
+ Args:
32
+ name (str): The name of this device command.
33
+ description (str): The description of this device command.
34
+ input_schema (list[ares_data_models.AresDataType]): A dictionary that defines the input parameters to the device command.
35
+ output_schema (list[ares_data_models.AresDataType]): A dictionary that defines the output parameters to the device command.
36
+ """
37
+ self.name = name
38
+ self.description = description
39
+ self.input_schema = input_schema
40
+ self.output_schema = output_schema
@@ -0,0 +1,285 @@
1
+ import grpc
2
+ import inspect
3
+ import time
4
+ import warnings
5
+ from concurrent import futures
6
+ from typing import Dict, Callable, Awaitable, Union, Any
7
+
8
+ from ares_datamodel.device.remote import ares_remote_device_service_pb2 as device_service
9
+ from ares_datamodel.device.remote import ares_remote_device_service_pb2_grpc as device_service_grpc
10
+ from ares_datamodel.device import device_status_pb2
11
+ from ares_datamodel.device import device_execution_result_pb2
12
+ from ares_datamodel.device import device_polling_settings_pb2
13
+ from ares_datamodel import ares_data_schema_pb2
14
+ from ares_datamodel import ares_struct_pb2
15
+ from google.protobuf import empty_pb2
16
+
17
+ from .device_models import DeviceCommandDescriptor
18
+ from ..Utils import ares_device_command_utils
19
+ from ..Utils import ares_data_schema_utils
20
+ from ..Utils import ares_struct_utils
21
+ from ..Utils import ares_value_utils
22
+ from ..Utils import ares_data_type_utils
23
+
24
+ # Type hint for the user's custom methods
25
+ EnterSafeModeMethod = Callable[[], None]
26
+ DeviceCommandMethod = Callable[..., Dict[str, any]]
27
+ DeviceStateMethod = Callable[[], Dict[str, any]]
28
+
29
+ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServicer):
30
+ """
31
+ A wrapper around the gRPC service to expose native Python objects for devices
32
+ """
33
+
34
+ def __init__(self, device_name: str, description: str, version: str, enter_safe_mode: EnterSafeModeMethod, update_device_state: DeviceStateMethod):
35
+ self.device_name = device_name
36
+ self.description = description
37
+ self.version = version
38
+ self._enter_safe_mode = enter_safe_mode
39
+ self._update_device_state = update_device_state
40
+ self._setting_schema: Dict[str, ares_data_schema_pb2.SchemaEntry] = {}
41
+ self._current_settings: Dict[str, ares_struct_pb2.AresValue] = {}
42
+ self._state_schema: Dict[str, ares_data_schema_pb2.SchemaEntry] = {}
43
+ self._commands: list[DeviceCommandDescriptor] = []
44
+ self._command_methods: Dict[str, Callable] = {}
45
+
46
+ def GetOperationalStatus(self, request, context) -> device_status_pb2.DeviceOperationalStatus:
47
+ return device_status_pb2.DeviceOperationalStatus(operational_state=device_status_pb2.OperationalState.ACTIVE, message=f"{self.device_name} is active!")
48
+
49
+ def GetInfo(self, request, context) -> device_service.DeviceInfoResponse:
50
+ info = device_service.DeviceInfoResponse()
51
+ info.name = self.device_name
52
+ info.description = self.description
53
+ info.version = self.version
54
+ return info
55
+
56
+ def GetCommands(self, request, context) -> device_service.CommandsResponse:
57
+ response = device_service.CommandsResponse()
58
+
59
+ for command_descriptor in self._commands:
60
+ proto_desc = ares_device_command_utils.python_command_description_to_proto(command_descriptor)
61
+ response.commands.append(proto_desc)
62
+
63
+ return response
64
+
65
+ def ExecuteCommand(self, request: device_service.ExecuteCommandRequest, context) -> device_execution_result_pb2.DeviceExecutionResult:
66
+ response = device_execution_result_pb2.DeviceExecutionResult()
67
+
68
+ if request.command_name in self._command_methods:
69
+ method = self._command_methods.get(request.command_name)
70
+
71
+ if not isinstance(method, Callable):
72
+ return device_execution_result_pb2.DeviceExecutionResult(success=False, error="Failed to find a valid remote method that corresponds to the requested action.")
73
+
74
+ method_signature = inspect.signature(method)
75
+
76
+ num_parameters: int = len(method_signature.parameters.items())
77
+ num_provided_parameters: int = len(request.arguments.fields)
78
+
79
+ if num_parameters != num_provided_parameters:
80
+ response.success = False
81
+ response.error = "Could not execute command as provided parameter count did not match the parameter count of the matching method signature!"
82
+ return response
83
+
84
+ #Convert the protobuf map to a Python dictionary
85
+ provided_param_dict = ares_struct_utils.ares_struct_to_dict(request.arguments)
86
+ result : Dict[str, Any] = method(**provided_param_dict)
87
+
88
+ for key, value in result.items():
89
+ ares_struct_utils.add_value_to_struct(response.result, key, ares_value_utils.create_ares_value(value))
90
+
91
+ response.success = True
92
+ return response
93
+
94
+ else:
95
+ response.success = False
96
+ response.error = "Unable to find requested command, cannot process device command request!"
97
+ return response
98
+
99
+ def EnterSafeMode(self, request, context) -> None:
100
+ #Handle call using the user's custom safe mode logic
101
+ try:
102
+ python_response = self._enter_safe_mode()
103
+ if isinstance(python_response, Awaitable):
104
+ python_response = python_response.__await__()
105
+
106
+ except Exception as e:
107
+ #Handle errors from user's logic
108
+ context.set_code(grpc.StatusCode.INTERNAL)
109
+ context.set_details(f"Error in safe mode logic: {e}")
110
+
111
+ def GetSettingsSchema(self, request, context) -> device_service.SettingsSchemaResponse:
112
+ response = device_service.SettingsSchemaResponse()
113
+ for key, value in self._setting_schema.items():
114
+ settings_entry = response.schema.fields[key]
115
+ settings_entry.type = value.type
116
+ settings_entry.optional = value.optional
117
+
118
+ if len(value.string_choices.strings) != 0:
119
+ settings_entry.string_choices.strings.extend(value.string_choices.strings)
120
+
121
+ elif len(value.number_choices.numbers) != 0:
122
+ settings_entry.number_choices.numbers.extend(value.number_choices.numbers)
123
+
124
+ return response
125
+
126
+ def GetCurrentSettings(self, request, context) -> device_service.CurrentSettingsResponse:
127
+ response = device_service.CurrentSettingsResponse()
128
+ try:
129
+ for key, value in self._current_settings.items():
130
+ new_entry = response.settings.fields[key]
131
+ new_ares_value = ares_value_utils.create_ares_value(value)
132
+ new_entry.CopyFrom(new_ares_value)
133
+
134
+ return response
135
+
136
+ except Exception as e:
137
+ print(f"EXCEPTION CAUGHT: {e}")
138
+ return response
139
+
140
+ def SetSettings(self, request: device_service.SetSettingsRequest, context) -> empty_pb2.Empty:
141
+ self._current_settings = ares_struct_utils.ares_struct_to_dict(request.settings)
142
+ return empty_pb2.Empty()
143
+
144
+ def GetStateSchema(self, request, context) -> device_service.StateSchemaResponse:
145
+ response = device_service.StateSchemaResponse()
146
+ for key, value in self._state_schema.items():
147
+ settings_entry = response.schema.fields[key]
148
+ settings_entry.type = value.type
149
+ settings_entry.optional = value.optional
150
+
151
+ if len(value.string_choices.strings) != 0:
152
+ settings_entry.string_choices.strings.extend(value.string_choices.strings)
153
+
154
+ elif len(value.number_choices.numbers) != 0:
155
+ settings_entry.number_choices.numbers.extend(value.number_choices.numbers)
156
+
157
+ return response
158
+
159
+ def GetState(self, request, context) -> device_service.DeviceStateResponse:
160
+ response = self._update_device_state()
161
+ if isinstance(response, Awaitable):
162
+ response = response.__await__()
163
+
164
+ proto_response = device_service.DeviceStateResponse()
165
+ proto_response.state = ares_struct_pb2.AresStruct()
166
+
167
+ if not isinstance(response, Dict):
168
+ print("State Response was invalid. All state responses should be returned in the form of a dictionary.")
169
+ return proto_response
170
+
171
+ for key, value in response.items():
172
+ ares_struct_utils.add_value_to_struct(proto_response.state, key, ares_value_utils.create_ares_value(value))
173
+
174
+ return proto_response
175
+
176
+ def GetStateStream(self, request: device_service.DeviceStateStreamRequest, context):
177
+ """ A server-side streaming RPC method that yields device states. """
178
+ polling_info: device_polling_settings_pb2.DevicePollingSettings = request.polling_settings
179
+
180
+ try:
181
+ if polling_info.polling_type == device_polling_settings_pb2.PollingType.INTERVAL:
182
+ delay = polling_info.interval_ms/1000
183
+ while True:
184
+ response = self._update_device_state()
185
+ if isinstance(response, Awaitable):
186
+ response = response.__await__()
187
+
188
+ proto_response = device_service.DeviceStateResponse()
189
+
190
+ if not isinstance(response, Dict):
191
+ print("State Response was invalid. All state responses should be returned in the form of a dictionary.")
192
+ return proto_response
193
+
194
+ for key, value in response.items():
195
+ ares_struct_utils.add_value_to_struct(proto_response.state, key, ares_value_utils.create_ares_value(value))
196
+
197
+ yield proto_response
198
+
199
+ if context.is_active() == False:
200
+ print("Client for ARES device has disconnected.")
201
+ break
202
+
203
+ time.sleep(delay)
204
+
205
+ else:
206
+ print("Probably do something here...")
207
+
208
+
209
+ except grpc.RpcError as e:
210
+ print(f"gRPC error occured in device state stream")
211
+
212
+ class AresDeviceService:
213
+ """ Manages the gRPC service for the AresDeviceSerivce """
214
+ def __init__(self, enter_safe_mode_logic: EnterSafeModeMethod, get_device_state_logic: DeviceStateMethod, device_name: str, description: str, version: str, use_localhost: bool = True, port: int = 7100):
215
+ """
216
+ Initializes the AresDeviceService
217
+
218
+ Args:
219
+ enter_safe_mode_logic: A callable function that will be executed when the device is instructed to enter safe mode.
220
+ This logic should put your device in a stable state, for instance telling a furnace to return to ambient temperature.
221
+ get_device_state_logic: A callable function that handles gathering device state information for logging purposes. This function
222
+ should return a dictionary containing the keys and values that define your devices state.
223
+ device_name (str): The name description of your device.
224
+ description (str): A brief description of your device.
225
+ version (str): The version associated with your device implementation.
226
+ use_localhost (bool): An optional value that allows the user to specify whether to host the service on the local network. Defaults to True.
227
+ port (int): The port that your device service will serve on. Defaults to port 7100.
228
+ """
229
+
230
+ self.device_name = device_name
231
+ self.description = description
232
+ self.version = version
233
+
234
+ self._port = port
235
+ self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
236
+ self._service_wrapper = AresDeviceServiceWrapper(device_name, description, version, enter_safe_mode_logic, get_device_state_logic)
237
+ device_service_grpc.add_AresRemoteDeviceServiceServicer_to_server(self._service_wrapper, self._server)
238
+ if(use_localhost):
239
+ self._server.add_insecure_port(f'localhost:{self._port}')
240
+ else:
241
+ self._server.add_insecure_port(f'[::]:{self._port}')
242
+
243
+ def add_new_command(self, cmd_descriptor: DeviceCommandDescriptor, method):
244
+ """
245
+ Adds a new command for use with this device that is reported to ARES.
246
+
247
+ Args:
248
+ cmd_descriptor (`PyAres.Device.DeviceCommandDescriptor`): A DeviceCommandDescriptor object that contains all the necessary information that ARES needs about your command.
249
+ method (Callable[..., Dict[str, any]]): Your command method. This method can take in any number of arguments, but should always return a dictionary of it's results.
250
+ """
251
+ method_signature = inspect.signature(method)
252
+ num_method_parameters: int = len(method_signature.parameters.items())
253
+ num_desc_parameters: int = len(cmd_descriptor.input_schema.items())
254
+
255
+ if num_method_parameters != num_desc_parameters:
256
+ warnings.warn(f"A mismatch in the number of input parameters your command method {method.__name__} and command descriptor expects was detected! This may result in unexpected behavior!")
257
+
258
+ self._service_wrapper._command_methods[cmd_descriptor.name] = method
259
+ self._service_wrapper._commands.append(cmd_descriptor)
260
+
261
+ def add_setting(self, setting_name: str, setting_value: Any, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = []):
262
+ """
263
+ Adds a new device setting to be reported to ARES when your devices capabilities are requested.
264
+
265
+ Args:
266
+ setting_name (str): The name of the setting.
267
+ setting_value (Any): The default value of the setting
268
+ optional (bool): Whether the setting is optional
269
+ constraints: An optional list of values to constrain the available setting choices. Can be integers, floats, or strings.
270
+ """
271
+ setting_type = ares_data_type_utils.determine_python_ares_data_type(setting_value)
272
+ self._service_wrapper._setting_schema[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints)
273
+ new_ares_value = ares_value_utils.create_ares_value(setting_value)
274
+ self._service_wrapper._current_settings[setting_name] = new_ares_value
275
+
276
+ def start(self):
277
+ """ Starts the service on the specified port, and waits for termination. """
278
+ print(f"Starting Ares Device Service on port {self._port}...")
279
+ self._server.start()
280
+ self._server.wait_for_termination()
281
+
282
+ def stop(self):
283
+ """ Stops the service, terminating the connection. """
284
+ print("Stopping Ares Device Service...")
285
+ self._server.stop(0).wait()
@@ -0,0 +1,2 @@
1
+ class ParameterMismatchWarning(UserWarning):
2
+ pass
@@ -0,0 +1,7 @@
1
+ from .ares_data_models import AresDataType, Outcome, RequestMetadata
2
+
3
+ __all__ = [
4
+ "AresDataType",
5
+ "Outcome",
6
+ "RequestMetadata"
7
+ ]