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.
- PyAres/Analyzing/__init__.py +9 -0
- PyAres/Analyzing/analysis_service.py +263 -0
- PyAres/Analyzing/analyzer_models.py +47 -0
- PyAres/Device/__init__.py +3 -0
- PyAres/Device/device_models.py +40 -0
- PyAres/Device/device_service.py +285 -0
- PyAres/Device/device_warnings.py +2 -0
- PyAres/Models/__init__.py +7 -0
- PyAres/Models/ares_data_models.py +27 -0
- PyAres/Planning/__init__.py +9 -0
- PyAres/Planning/planner_models.py +83 -0
- PyAres/Planning/planning_service.py +252 -0
- PyAres/Test/analyzer_test.py +27 -0
- PyAres/Test/device_test.py +48 -0
- PyAres/Test/planner_test.py +85 -0
- PyAres/Utils/ares_data_schema_utils.py +44 -0
- PyAres/Utils/ares_data_type_utils.py +36 -0
- PyAres/Utils/ares_device_command_utils.py +43 -0
- PyAres/Utils/ares_outcome_utils.py +10 -0
- PyAres/Utils/ares_struct_utils.py +208 -0
- PyAres/Utils/ares_value_utils.py +214 -0
- PyAres/__init__.py +14 -0
- pyares-0.1.4.dist-info/METADATA +13 -0
- pyares-0.1.4.dist-info/RECORD +27 -0
- pyares-0.1.4.dist-info/WHEEL +5 -0
- pyares-0.1.4.dist-info/licenses/LICENSE +21 -0
- pyares-0.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from ares_datamodel import request_metadata_pb2
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
class AresDataType(Enum):
|
|
5
|
+
UNKNOWN = 0
|
|
6
|
+
NULL = 1
|
|
7
|
+
BOOLEAN = 2
|
|
8
|
+
STRING = 3
|
|
9
|
+
NUMBER = 4
|
|
10
|
+
STRING_ARRAY = 5
|
|
11
|
+
NUMBER_ARRAY = 6
|
|
12
|
+
BYTE_ARRAY = 7
|
|
13
|
+
BOOL_ARRAY = 8
|
|
14
|
+
|
|
15
|
+
class Outcome(Enum):
|
|
16
|
+
UNSPECIFIED_OUTCOME = 0
|
|
17
|
+
SUCCESS = 1
|
|
18
|
+
FAILURE = 2
|
|
19
|
+
WARNING = 3
|
|
20
|
+
CANCELED = 4
|
|
21
|
+
|
|
22
|
+
class RequestMetadata():
|
|
23
|
+
def __init__(self, proto_metadata: request_metadata_pb2.RequestMetadata):
|
|
24
|
+
self.system_name = proto_metadata.system_name
|
|
25
|
+
self.campaign_name = proto_metadata.campaign_name
|
|
26
|
+
self.campaign_id = proto_metadata.campaign_id
|
|
27
|
+
self.experiment_id = proto_metadata.experiment_id
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import Dict, Any, List
|
|
2
|
+
from ..Models import Outcome, AresDataType, RequestMetadata
|
|
3
|
+
|
|
4
|
+
class ParameterHistoryItem:
|
|
5
|
+
""" Represents a single historical parameter item """
|
|
6
|
+
def __init__(self, planned_value: Any, achieved_value: Any):
|
|
7
|
+
"""
|
|
8
|
+
Initializes a ParameterHistoryItem
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
planned_value: A value that was planned
|
|
12
|
+
achieved_value: Optionally a value that was actually achieved for what was planned
|
|
13
|
+
"""
|
|
14
|
+
self.planned_value = planned_value
|
|
15
|
+
self.achieved_value = achieved_value
|
|
16
|
+
|
|
17
|
+
class PlanningParameter:
|
|
18
|
+
"""
|
|
19
|
+
Represents a single parameter within a planning request.
|
|
20
|
+
|
|
21
|
+
Designed to provide a more user-friendly abstraction for the user to interact
|
|
22
|
+
with planning parameters through.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, name: str, minimum_value: float,
|
|
25
|
+
maximum_value: float, param_history: list[ParameterHistoryItem], data_type: AresDataType,
|
|
26
|
+
is_planned: bool, is_result: bool, planner_name: str, initial_value = None):
|
|
27
|
+
"""
|
|
28
|
+
Initializes a PlanningParameter.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name: The name or key associated with the parameter.
|
|
32
|
+
minimum_value: The minimum value the parameter is capable of being assigned.
|
|
33
|
+
maximum_value: The maximum value the parameter is capable of being assigned.
|
|
34
|
+
param_history: A list of historical planned and achieved values associated with the parameter.
|
|
35
|
+
data_type: The data type associated with the parameter.
|
|
36
|
+
is_planned: A bool representing whether this parameter is designed to be planned for.
|
|
37
|
+
is_result: A bool representing whether this parameter is the intended result of the experiment.
|
|
38
|
+
planner_name: The name of the planner ARES requested be used to plan for this parameter.
|
|
39
|
+
initial_value: An optional initial value for the given parameter
|
|
40
|
+
"""
|
|
41
|
+
self.name : str = name
|
|
42
|
+
self.minimum_value : float = minimum_value
|
|
43
|
+
self.maximum_value : float= maximum_value
|
|
44
|
+
self.param_history : List = param_history
|
|
45
|
+
self.data_type : AresDataType = data_type
|
|
46
|
+
self.is_planned : bool = is_planned
|
|
47
|
+
self.is_result : bool = is_result
|
|
48
|
+
self.planner_name : str = planner_name
|
|
49
|
+
self.initial_value = initial_value
|
|
50
|
+
|
|
51
|
+
class PlanRequest:
|
|
52
|
+
"""
|
|
53
|
+
Represents a PlanRequest message received from ARES.
|
|
54
|
+
|
|
55
|
+
Designed to provide a more user-friendly abstraction for interacting with a plan request message.
|
|
56
|
+
"""
|
|
57
|
+
def __init__(self, parameters: list[PlanningParameter], settings: Dict[str, Any], analysis_results: list[float], metadata: RequestMetadata):
|
|
58
|
+
"""
|
|
59
|
+
Initializes a PlanRequest.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
parameters: A list of PlanningParameter objects.
|
|
63
|
+
"""
|
|
64
|
+
self.parameters = parameters
|
|
65
|
+
self.settings = settings
|
|
66
|
+
self.analysis_results = analysis_results
|
|
67
|
+
self.request_metadata = metadata
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PlanResponse:
|
|
71
|
+
""" Represents a PlanResponse message to be send to ARES. """
|
|
72
|
+
def __init__(self, parameter_names: list[str], parameter_values: list, planning_outcome: Outcome = Outcome.SUCCESS, error_string: str = ""):
|
|
73
|
+
"""
|
|
74
|
+
Initializes a PlanResponse.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
parameter_names: A list of names associated with planned parameters.
|
|
78
|
+
parameter_values: A list of values associated with planned parameters.
|
|
79
|
+
"""
|
|
80
|
+
self.parameter_names = parameter_names
|
|
81
|
+
self.parameter_values = parameter_values
|
|
82
|
+
self.outcome = planning_outcome
|
|
83
|
+
self.error_string = error_string
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import grpc
|
|
2
|
+
from concurrent import futures
|
|
3
|
+
from typing import Callable, Awaitable, Union, Dict
|
|
4
|
+
|
|
5
|
+
from ares_datamodel.planning.remote import ares_remote_planner_service_pb2 as planner_service
|
|
6
|
+
from ares_datamodel.planning.remote import ares_remote_planner_service_pb2_grpc as planner_service_grpc
|
|
7
|
+
from ares_datamodel.planning import planner_pb2
|
|
8
|
+
from ares_datamodel.planning import planner_settings_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
|
+
|
|
17
|
+
# Import Utilities
|
|
18
|
+
from ..Utils import ares_value_utils
|
|
19
|
+
from ..Utils import ares_data_schema_utils
|
|
20
|
+
from ..Utils import ares_data_type_utils
|
|
21
|
+
from ..Utils import ares_struct_utils
|
|
22
|
+
from ..Utils import ares_outcome_utils
|
|
23
|
+
|
|
24
|
+
# Import python models
|
|
25
|
+
from ..Models import ares_data_models
|
|
26
|
+
from .planner_models import *
|
|
27
|
+
|
|
28
|
+
# Type hint for the user's custom planning logic
|
|
29
|
+
PlanLogicFunction = Callable[[PlanRequest], Union[PlanResponse, Awaitable[PlanResponse]]]
|
|
30
|
+
|
|
31
|
+
class AresPlannerServiceWrapper(planner_service_grpc.AresRemotePlannerServiceServicer):
|
|
32
|
+
"""
|
|
33
|
+
A wrapper around the gRPC service to expose native Python objects for planning
|
|
34
|
+
"""
|
|
35
|
+
def __init__(self, service_name: str, description: str, version: str, timeout: int, custom_plan_logic: PlanLogicFunction):
|
|
36
|
+
self._custom_plan_logic: PlanLogicFunction = custom_plan_logic
|
|
37
|
+
self._service_name: str = service_name
|
|
38
|
+
self._description: str = description
|
|
39
|
+
self._version: str = version
|
|
40
|
+
self._settings: Dict[str, ares_data_schema_pb2.SchemaEntry] = {}
|
|
41
|
+
self._planner_options: list[planner_pb2.Planner] = []
|
|
42
|
+
self._supported_types: list[ares_data_type_pb2.AresDataType] = []
|
|
43
|
+
self._timeout: int = timeout
|
|
44
|
+
|
|
45
|
+
def GetPlannerServiceCapabilities(self, request, context) -> planner_service_capabilities_pb2.PlannerServiceCapabilities:
|
|
46
|
+
print("Capabilities Requested!")
|
|
47
|
+
"""
|
|
48
|
+
Implements the gRPC Capabilities request method. Responsible for telling ARES what this planner
|
|
49
|
+
service is capable of.
|
|
50
|
+
"""
|
|
51
|
+
capabilities = planner_service_capabilities_pb2.PlannerServiceCapabilities(timeout_seconds=self._timeout)
|
|
52
|
+
capabilities.service_name = self._service_name
|
|
53
|
+
capabilities.accepted_types.extend(self._supported_types)
|
|
54
|
+
capabilities.available_planners.extend(self._planner_options)
|
|
55
|
+
|
|
56
|
+
for(key, value) in self._settings.items():
|
|
57
|
+
settings_entry: ares_data_schema_pb2.SchemaEntry = capabilities.settings_schema.fields[key]
|
|
58
|
+
settings_entry.type = value.type
|
|
59
|
+
settings_entry.optional - value.optional
|
|
60
|
+
|
|
61
|
+
if len(value.string_choices.strings) != 0:
|
|
62
|
+
settings_entry.string_choices.strings.extend(value.string_choices.strings)
|
|
63
|
+
|
|
64
|
+
elif len(value.number_choices.numbers) != 0:
|
|
65
|
+
settings_entry.number_choices.numbers.extend(value.number_choices.numbers)
|
|
66
|
+
|
|
67
|
+
print("Capabilites Sent!")
|
|
68
|
+
return capabilities
|
|
69
|
+
|
|
70
|
+
def GetInfo(self, request, context) -> connection_info_pb2.InfoResponse:
|
|
71
|
+
try:
|
|
72
|
+
response = connection_info_pb2.InfoResponse(
|
|
73
|
+
name=self._service_name,
|
|
74
|
+
version=self._version,
|
|
75
|
+
description=self._description
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return response
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
response = connection_info_pb2.InfoResponse(
|
|
82
|
+
name="ERROR",
|
|
83
|
+
version="ERROR",
|
|
84
|
+
description="Error fetching information"
|
|
85
|
+
)
|
|
86
|
+
print(f"Exception while trying to respond to ARES with information! {e}")
|
|
87
|
+
return response
|
|
88
|
+
|
|
89
|
+
def GetState(self, request, context) -> connection_state_pb2.StateResponse:
|
|
90
|
+
try:
|
|
91
|
+
return connection_state_pb2.StateResponse(state=connection_state_pb2.State.ACTIVE, state_message=f"{self._service_name} is active!")
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print(f"{e}")
|
|
95
|
+
return connection_state_pb2.StateResponse(state=connection_state_pb2.State.ERROR, state_message=f"Exception while trying to respond to ARES with state! {e}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def GetConnectionStatus(self, request, context):
|
|
100
|
+
try:
|
|
101
|
+
return connection_state_pb2.StateResponse(state=connection_state_pb2.State.ACTIVE, state_message=f"{self._service_name} is active!")
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Exception while trying to respond to ARES with connection status! {e}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def Plan(self, request: plan_pb2.PlanningRequest, context) -> plan_pb2.PlanningResponse:
|
|
108
|
+
"""
|
|
109
|
+
Implements the gRPC Plan method. This method converts protobuf requests to native Python objects
|
|
110
|
+
before executing the users custom planning logic and converting their response back to protobuf.
|
|
111
|
+
"""
|
|
112
|
+
parameters = []
|
|
113
|
+
for proto_param in request.planning_parameters:
|
|
114
|
+
parameters.append(
|
|
115
|
+
PlanningParameter
|
|
116
|
+
(
|
|
117
|
+
name=proto_param.parameter_name,
|
|
118
|
+
maximum_value=proto_param.maximum_value,
|
|
119
|
+
minimum_value=proto_param.minimum_value,
|
|
120
|
+
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],
|
|
121
|
+
data_type=ares_data_type_utils.proto_ares_type_to_python_ares_type(proto_param.data_type),
|
|
122
|
+
is_planned=proto_param.is_planned,
|
|
123
|
+
is_result=proto_param.is_result,
|
|
124
|
+
planner_name=proto_param.planner_name,
|
|
125
|
+
initial_value=ares_value_utils.ares_value_to_py(proto_param.initial_value)
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
python_request = PlanRequest(parameters=parameters,
|
|
129
|
+
settings=ares_struct_utils.ares_struct_to_dict(request.adapter_settings),
|
|
130
|
+
analysis_results=list(request.analysis_results),
|
|
131
|
+
metadata=RequestMetadata(request.metadata))
|
|
132
|
+
|
|
133
|
+
#Handle call using the user's custom planning logic
|
|
134
|
+
response_proto = plan_pb2.PlanningResponse()
|
|
135
|
+
try:
|
|
136
|
+
python_response = self._custom_plan_logic(python_request)
|
|
137
|
+
if isinstance(python_response, Awaitable):
|
|
138
|
+
python_response = python_response.__await__()
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
#Handle errors from user's logic
|
|
142
|
+
context.set_code(grpc.StatusCode.INTERNAL)
|
|
143
|
+
context.set_details(f"Error in custom planning logic: {e}")
|
|
144
|
+
response_proto.error_string = f"{e}"
|
|
145
|
+
response_proto.planning_outcome = ares_outcome_enum_pb2.FAILURE
|
|
146
|
+
return response_proto
|
|
147
|
+
|
|
148
|
+
if not isinstance(python_response, PlanResponse):
|
|
149
|
+
response_proto.error_string = "The returned response from the user planning method was not a plan response, and thus was invalid."
|
|
150
|
+
response_proto.planning_outcome = ares_outcome_enum_pb2.FAILURE
|
|
151
|
+
return response_proto
|
|
152
|
+
|
|
153
|
+
response_proto.planning_outcome = ares_outcome_utils.python_ares_outcome_to_proto_ares_outcome(python_response.outcome)
|
|
154
|
+
response_proto.error_string = python_response.error_string
|
|
155
|
+
|
|
156
|
+
for i in range(len(python_response.parameter_names)):
|
|
157
|
+
planned_parameter = plan_pb2.PlannedParameter(parameter_value=ares_value_utils.create_ares_value(python_response.parameter_values[i]))
|
|
158
|
+
planned_parameter.parameter_name = python_response.parameter_names[i]
|
|
159
|
+
new_planned_parameter = response_proto.planned_parameters.add()
|
|
160
|
+
new_planned_parameter.CopyFrom(planned_parameter)
|
|
161
|
+
|
|
162
|
+
print("Sending Plan Response.....")
|
|
163
|
+
return response_proto
|
|
164
|
+
|
|
165
|
+
class AresPlannerService:
|
|
166
|
+
"""
|
|
167
|
+
Manages the gRPC server for the AresPlannerService
|
|
168
|
+
"""
|
|
169
|
+
def __init__(self, custom_plan_logic: PlanLogicFunction, service_name: str, service_description: str, service_version: str, timeout: int = 30, use_localhost: bool = True, port: int = 7082):
|
|
170
|
+
"""
|
|
171
|
+
Initializes the AresPlannerService
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
custom_plan_logic: A callable function that will be executed when a PlanRequest is received.
|
|
175
|
+
This function should accept a 'PyARES.AresPlanning.PlanRequest' object and return a
|
|
176
|
+
'PyARES.AresPlanning.PlanResponse' object (or an awaitable that resolves to one).
|
|
177
|
+
service_name: The name descriptor that is associated with your planner service.
|
|
178
|
+
service_description: A brief description describing your implementation of the planner service.
|
|
179
|
+
service_version: The version of your planner service.
|
|
180
|
+
use_localhost: An optional value that allows the user to specify whether to host the service on the local network. Defaults to True.
|
|
181
|
+
port: The port that your planner service will serve on. Defaults to port 7082.
|
|
182
|
+
"""
|
|
183
|
+
#Public Values, designed to be accessible to the user
|
|
184
|
+
self.service_name = service_name
|
|
185
|
+
self.service_description = service_description
|
|
186
|
+
self.service_version = service_version
|
|
187
|
+
|
|
188
|
+
#Private values, mostly related to the service
|
|
189
|
+
self._port = port
|
|
190
|
+
self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
|
191
|
+
self._service_wrapper = AresPlannerServiceWrapper(service_name, service_description, service_version, timeout, custom_plan_logic)
|
|
192
|
+
planner_service_grpc.add_AresRemotePlannerServiceServicer_to_server(self._service_wrapper, self._server)
|
|
193
|
+
if(use_localhost):
|
|
194
|
+
self._server.add_insecure_port(f'localhost:{self._port}')
|
|
195
|
+
else:
|
|
196
|
+
self._server.add_insecure_port(f'[::]:{self._port}')
|
|
197
|
+
|
|
198
|
+
def add_planner_option(self, planner_name: str, planner_description: str, planner_version: str):
|
|
199
|
+
"""
|
|
200
|
+
Adds a planner option that is reported to ARES when your services capabilities are requested.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
planner_name (str): The dedicated name of your planner.
|
|
204
|
+
planner_description (str): A brief description of your planner that is displayed in ARES.
|
|
205
|
+
planner_version (str): The version of your planner.
|
|
206
|
+
"""
|
|
207
|
+
self._service_wrapper._planner_options.append(planner_pb2.Planner(planner_name=planner_name, description=planner_description, version=planner_version))
|
|
208
|
+
|
|
209
|
+
def add_setting(self, setting_name: str, setting_type: ares_data_models.AresDataType, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = []):
|
|
210
|
+
"""
|
|
211
|
+
Adds a planner setting to be reported to ARES when your services capabilities are requested.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
setting_name (str): The name of the setting.
|
|
215
|
+
setting_type (AresDataType): The type of this settings value.
|
|
216
|
+
optional (bool): Whether the setting is optional.
|
|
217
|
+
constraints: An optional list of values to constrain the available setting choices. Can be integers, strings, or floats.
|
|
218
|
+
"""
|
|
219
|
+
self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints)
|
|
220
|
+
|
|
221
|
+
def add_supported_type(self, type: ares_data_models.AresDataType):
|
|
222
|
+
"""
|
|
223
|
+
Adds the specified type to the list of value types your planenr service accepts.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
type (AresDataType): The type being added to the list of allowed types.
|
|
227
|
+
"""
|
|
228
|
+
self._service_wrapper._supported_types.append(ares_data_type_utils.python_ares_type_to_proto_ares_type(type))
|
|
229
|
+
|
|
230
|
+
def set_timeout(self, new_timeout: int):
|
|
231
|
+
"""
|
|
232
|
+
Sets the time, in seconds, that ARES will wait to receive a response from this service.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
new_timeout: The time to be assigned as the new timeout value
|
|
236
|
+
"""
|
|
237
|
+
self._service_wrapper._timeout = new_timeout
|
|
238
|
+
|
|
239
|
+
def start(self):
|
|
240
|
+
"""
|
|
241
|
+
Starts the service on the specified port, and waits for termination.
|
|
242
|
+
"""
|
|
243
|
+
print(f"Starting Ares Planner Service on port {self._port}...")
|
|
244
|
+
self._server.start()
|
|
245
|
+
self._server.wait_for_termination()
|
|
246
|
+
|
|
247
|
+
def stop(self):
|
|
248
|
+
"""
|
|
249
|
+
Stops the service, terminating the connection.
|
|
250
|
+
"""
|
|
251
|
+
print("Stopping Ares Planning Service...")
|
|
252
|
+
self._server.stop(0).wait()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome
|
|
2
|
+
|
|
3
|
+
def analyze(request: AnalysisRequest) -> Analysis:
|
|
4
|
+
#Custom Analysis Logic
|
|
5
|
+
temperature = request.inputs.get("Temperature")
|
|
6
|
+
|
|
7
|
+
if not isinstance(temperature, float):
|
|
8
|
+
print("Temperature was not a float")
|
|
9
|
+
temperature = 0.0
|
|
10
|
+
|
|
11
|
+
print(f"Temperature: {temperature}")
|
|
12
|
+
|
|
13
|
+
analysis = Analysis(result=temperature)
|
|
14
|
+
return analysis
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
#Basic details about your analyzer
|
|
19
|
+
name = "Python Test Analyzer"
|
|
20
|
+
version = "0.0.1"
|
|
21
|
+
description = "This is a test analyzer to demonstrate working with PyAres to create analyzers!"
|
|
22
|
+
pythonDemoAnalyzer = AresAnalyzerService(analyze, name, version, description)
|
|
23
|
+
|
|
24
|
+
#Add Analysis Parameters
|
|
25
|
+
pythonDemoAnalyzer.add_analysis_parameter("Temperature", AresDataType.NUMBER)
|
|
26
|
+
|
|
27
|
+
pythonDemoAnalyzer.start()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from PyAres import AresDeviceService, DeviceCommandDescriptor, DeviceSchemaEntry, AresDataType
|
|
2
|
+
from typing import Dict
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
class DemoDevice:
|
|
6
|
+
# A simulated device. In reality, these communications would be happening with external hardware over serial, usb, etc.
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.temperature = 0.0
|
|
9
|
+
|
|
10
|
+
def set_temperature(self, temperature: float):
|
|
11
|
+
self.temperature = temperature
|
|
12
|
+
time.sleep(5)
|
|
13
|
+
return {}
|
|
14
|
+
|
|
15
|
+
def get_temperature(self):
|
|
16
|
+
return { "temperature": self.temperature }
|
|
17
|
+
|
|
18
|
+
def get_device_state(self) -> Dict:
|
|
19
|
+
state_dict = { "temperature": self.temperature }
|
|
20
|
+
return state_dict
|
|
21
|
+
|
|
22
|
+
def enter_safe_mode(self):
|
|
23
|
+
self.temperature = 0
|
|
24
|
+
|
|
25
|
+
device = DemoDevice()
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
# Basic information about my device
|
|
29
|
+
device_name = "Demo Device"
|
|
30
|
+
description = "A device to demonstrate the PyAres device capabilities"
|
|
31
|
+
version = "1.0.0"
|
|
32
|
+
device_service = AresDeviceService(device.enter_safe_mode, device.get_device_state, device_name, description, version)
|
|
33
|
+
|
|
34
|
+
# Create the "Set Temperature" Command
|
|
35
|
+
parameter_schema = DeviceSchemaEntry(AresDataType.NUMBER, "A numeric temperature value", "Degree's Celsius")
|
|
36
|
+
input_schema = { "temperature": parameter_schema }
|
|
37
|
+
set_temp_descriptor = DeviceCommandDescriptor("Set Temperature", "Set's the temperature of the demo device to the provided value.", input_schema, {})
|
|
38
|
+
device_service.add_new_command(set_temp_descriptor, device.set_temperature)
|
|
39
|
+
|
|
40
|
+
# Create the "Get Temperature" Command
|
|
41
|
+
output_schema = {"temperature": DeviceSchemaEntry(AresDataType.NUMBER, "The current temperature of the device", "Degree's Celsius")}
|
|
42
|
+
get_temp_desc = DeviceCommandDescriptor("Get Temperature", "Get's the current temperature of the demo device.", {}, output_schema)
|
|
43
|
+
device_service.add_new_command(get_temp_desc, device.get_temperature)
|
|
44
|
+
|
|
45
|
+
#Add Settings
|
|
46
|
+
device_service.add_setting("Allow Negative Values", True)
|
|
47
|
+
|
|
48
|
+
device_service.start()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from PyAres import *
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
def plan(request: PlanRequest) -> PlanResponse:
|
|
6
|
+
print("Planning Requested!")
|
|
7
|
+
new_values = []
|
|
8
|
+
gpdoods = []
|
|
9
|
+
names = []
|
|
10
|
+
|
|
11
|
+
for param in request.parameters:
|
|
12
|
+
if param.planner_name == "GPRDood":
|
|
13
|
+
gpdoods.append(param)
|
|
14
|
+
|
|
15
|
+
if param.planner_name == "Random Planner":
|
|
16
|
+
new_value = random_planner(param)
|
|
17
|
+
new_values.append(new_value)
|
|
18
|
+
names.append(param.name)
|
|
19
|
+
|
|
20
|
+
elif param.planner_name == "Gradual Planner":
|
|
21
|
+
new_value = gradual_planner(param)
|
|
22
|
+
new_values.append(new_value)
|
|
23
|
+
names.append(param.name)
|
|
24
|
+
|
|
25
|
+
else:
|
|
26
|
+
print("Invalid planner name detected... defaulting to random")
|
|
27
|
+
new_value = random_planner(param)
|
|
28
|
+
new_values.append(new_value)
|
|
29
|
+
names.append(param.name)
|
|
30
|
+
|
|
31
|
+
return PlanResponse(parameter_names=names, parameter_values=new_values)
|
|
32
|
+
|
|
33
|
+
def random_planner(param: PlanningParameter) -> float:
|
|
34
|
+
if param.data_type == AresDataType.NUMBER:
|
|
35
|
+
return random.uniform(param.minimum_value, param.maximum_value)
|
|
36
|
+
|
|
37
|
+
else:
|
|
38
|
+
print("Found a non-number....")
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
def gradual_planner(param: PlanningParameter) -> float:
|
|
42
|
+
if(param.data_type == AresDataType.NUMBER):
|
|
43
|
+
if len(param.param_history) == 0:
|
|
44
|
+
return param.minimum_value
|
|
45
|
+
|
|
46
|
+
previous_value = param.param_history[-1].planned_value
|
|
47
|
+
previous_value += 5
|
|
48
|
+
|
|
49
|
+
if previous_value > param.maximum_value:
|
|
50
|
+
return param.minimum_value
|
|
51
|
+
|
|
52
|
+
else:
|
|
53
|
+
return previous_value
|
|
54
|
+
|
|
55
|
+
else:
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
#Basic details about your planner
|
|
60
|
+
name = "Python Test Planner"
|
|
61
|
+
version = "1.0.0"
|
|
62
|
+
description = "This is a test planner to demonstrate working with PyAres to create planners!"
|
|
63
|
+
pythonDemoPlanner = AresPlannerService(plan, name, description, version)
|
|
64
|
+
|
|
65
|
+
#Add Supported Types
|
|
66
|
+
pythonDemoPlanner.add_supported_type(AresDataType.NUMBER)
|
|
67
|
+
|
|
68
|
+
#Add Planner Options
|
|
69
|
+
pythonDemoPlanner.add_planner_option("Random Planner", "A planner that returns random values", "1.0.0")
|
|
70
|
+
pythonDemoPlanner.add_planner_option("Gradual Planner", "A planner that gradually increases a value based on the values history", "1.0.0")
|
|
71
|
+
|
|
72
|
+
#Add Planner Settings
|
|
73
|
+
pythonDemoPlanner.add_setting("String Setting", AresDataType.STRING)
|
|
74
|
+
pythonDemoPlanner.add_setting("Number Setting", AresDataType.NUMBER)
|
|
75
|
+
pythonDemoPlanner.add_setting("Boolean Setting", AresDataType.BOOLEAN)
|
|
76
|
+
pythonDemoPlanner.add_setting("String Array Setting", AresDataType.STRING_ARRAY)
|
|
77
|
+
pythonDemoPlanner.add_setting("Constrained Strings", AresDataType.STRING_ARRAY, True, ["One", "Two", "Three"])
|
|
78
|
+
pythonDemoPlanner.add_setting("Number Array Setting", AresDataType.NUMBER_ARRAY)
|
|
79
|
+
pythonDemoPlanner.add_setting("Constrained Numbers", AresDataType.NUMBER_ARRAY, True, [1, 2, 3])
|
|
80
|
+
|
|
81
|
+
#Set Planner Timeout
|
|
82
|
+
pythonDemoPlanner.set_timeout(60)
|
|
83
|
+
|
|
84
|
+
#Start Your Planner Service
|
|
85
|
+
pythonDemoPlanner.start()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Union, Dict
|
|
2
|
+
|
|
3
|
+
#Datamodel Imports
|
|
4
|
+
from ares_datamodel import ares_data_schema_pb2
|
|
5
|
+
|
|
6
|
+
from ..Models import ares_data_models
|
|
7
|
+
# def ares_schema_to_dict(schema: ares_data_schema_pb2.AresDataSchema) -> dict:
|
|
8
|
+
# """Converts an AresDataSchemaSimplified to a dictionary for user logic."""
|
|
9
|
+
# result = {}
|
|
10
|
+
# for key, entry in schema.fields.items():
|
|
11
|
+
# result[key] = {
|
|
12
|
+
# "type": ares_data_schema_pb2.AresDataType.Name(entry.type),
|
|
13
|
+
# "is_array": entry.is_array
|
|
14
|
+
# }
|
|
15
|
+
# return result
|
|
16
|
+
|
|
17
|
+
def create_settings_schema_entry(
|
|
18
|
+
setting_type: ares_data_models.AresDataType,
|
|
19
|
+
optional: bool,
|
|
20
|
+
choices: Union[list[str], list[int], list[float]]) -> ares_data_schema_pb2.SchemaEntry:
|
|
21
|
+
"""
|
|
22
|
+
Takes in an AresSetting object and converts it into the protobuf SchemaEntry message.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
new_setting: The AresSetting object that provides all the details around the setting implementation.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
(SchemaEntry): A new SchemaEntry message.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
if(isinstance(choices, list)):
|
|
32
|
+
if(len(choices) == 0):
|
|
33
|
+
schema_entry = ares_data_schema_pb2.SchemaEntry(type=setting_type.value, optional=optional)
|
|
34
|
+
|
|
35
|
+
elif(all(isinstance(item, str) for item in choices)):
|
|
36
|
+
schema_entry = ares_data_schema_pb2.SchemaEntry(type=setting_type.value, optional=optional)
|
|
37
|
+
schema_entry.string_choices.strings.extend(choices)
|
|
38
|
+
|
|
39
|
+
elif(all(isinstance(item, (int, float)) for item in choices)):
|
|
40
|
+
schema_entry = ares_data_schema_pb2.SchemaEntry(type=setting_type.value, optional=optional)
|
|
41
|
+
schema_entry.number_choices.numbers.extend(choices)
|
|
42
|
+
|
|
43
|
+
return schema_entry
|
|
44
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
from ares_datamodel import ares_data_type_pb2
|
|
3
|
+
from ..Models import AresDataType
|
|
4
|
+
|
|
5
|
+
def python_ares_type_to_proto_ares_type(py_value: AresDataType) -> ares_data_type_pb2.AresDataType:
|
|
6
|
+
""" A method to convert from the python AresDataType class to the protobuf version """
|
|
7
|
+
return py_value.value
|
|
8
|
+
|
|
9
|
+
def proto_ares_type_to_python_ares_type(proto_value: ares_data_type_pb2.AresDataType) -> AresDataType:
|
|
10
|
+
""" A method to convert from the protobuf AresDataType class to the python version """
|
|
11
|
+
return AresDataType(proto_value)
|
|
12
|
+
|
|
13
|
+
def determine_python_ares_data_type(value: Union[int, float, str, bool, list]):
|
|
14
|
+
""" A method that takes in a value and returns the corresponding `PyAres.Models.AresDataType`"""
|
|
15
|
+
match value:
|
|
16
|
+
case str():
|
|
17
|
+
return AresDataType.STRING
|
|
18
|
+
case bool():
|
|
19
|
+
# Boolean is a subtype of int, meaning we have to check if it's a bool
|
|
20
|
+
# before we check if it's an int, otherwise every bool would return an int instead.
|
|
21
|
+
return AresDataType.BOOLEAN
|
|
22
|
+
case int():
|
|
23
|
+
return AresDataType.NUMBER
|
|
24
|
+
case float():
|
|
25
|
+
return AresDataType.NUMBER
|
|
26
|
+
case list():
|
|
27
|
+
if(all(isinstance(x, str) for x in value)):
|
|
28
|
+
return AresDataType.STRING_ARRAY
|
|
29
|
+
elif(all(isinstance(x, (int, float)) for x in value)):
|
|
30
|
+
return AresDataType.NUMBER_ARRAY
|
|
31
|
+
elif(all(isinstance(x, bool)) for x in value):
|
|
32
|
+
return AresDataType.BOOL_ARRAY
|
|
33
|
+
else:
|
|
34
|
+
return AresDataType.UNKNOWN
|
|
35
|
+
case _:
|
|
36
|
+
return AresDataType.UNKNOWN
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from ..Device import DeviceCommandDescriptor
|
|
4
|
+
from ..Device import DeviceSchemaEntry
|
|
5
|
+
from ares_datamodel.device import device_command_descriptor_pb2
|
|
6
|
+
from ares_datamodel import ares_data_schema_pb2
|
|
7
|
+
|
|
8
|
+
from . import ares_data_type_utils
|
|
9
|
+
|
|
10
|
+
def python_command_description_to_proto(python_description: DeviceCommandDescriptor) -> device_command_descriptor_pb2.DeviceCommandDescriptor:
|
|
11
|
+
proto_description = device_command_descriptor_pb2.DeviceCommandDescriptor()
|
|
12
|
+
|
|
13
|
+
#First, we need to transform our Python DeviceSchemaEntry classes into the protobuf equivalent. Then add them to our new proto message
|
|
14
|
+
transformed_input_schema = {key: python_device_schema_entry_to_proto(value) for key, value in python_description.input_schema.items()}
|
|
15
|
+
for key, value in transformed_input_schema.items():
|
|
16
|
+
new_entry: ares_data_schema_pb2.SchemaEntry = proto_description.input_schema.fields[key]
|
|
17
|
+
new_entry.CopyFrom(value)
|
|
18
|
+
|
|
19
|
+
transformed_output_schema = {key: python_device_schema_entry_to_proto(value) for key, value in python_description.output_schema.items()}
|
|
20
|
+
for key, value in transformed_output_schema.items():
|
|
21
|
+
new_entry: ares_data_schema_pb2.SchemaEntry = proto_description.output_schema.fields[key]
|
|
22
|
+
new_entry.CopyFrom(value)
|
|
23
|
+
|
|
24
|
+
proto_description.name = python_description.name
|
|
25
|
+
proto_description.description = python_description.description
|
|
26
|
+
|
|
27
|
+
return proto_description
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def python_device_schema_entry_to_proto(entry: DeviceSchemaEntry) -> ares_data_schema_pb2.SchemaEntry:
|
|
31
|
+
proto_schema = ares_data_schema_pb2.SchemaEntry()
|
|
32
|
+
proto_schema.type = ares_data_type_utils.python_ares_type_to_proto_ares_type(entry.type)
|
|
33
|
+
proto_schema.optional = entry.optional
|
|
34
|
+
proto_schema.description = entry.description
|
|
35
|
+
proto_schema.unit = entry.unit
|
|
36
|
+
|
|
37
|
+
if all(isinstance(x, (int, float)) for x in entry.contraints):
|
|
38
|
+
proto_schema.number_choices.numbers.extend(entry.contraints)
|
|
39
|
+
|
|
40
|
+
elif all(isinstance(x, str) for x in entry.contraints):
|
|
41
|
+
proto_schema.string_choices.strings.extend(entry.contraints)
|
|
42
|
+
|
|
43
|
+
return proto_schema
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from ..Models import Outcome
|
|
2
|
+
from ares_datamodel import ares_outcome_enum_pb2
|
|
3
|
+
|
|
4
|
+
def python_ares_outcome_to_proto_ares_outcome(py_value: Outcome) -> ares_outcome_enum_pb2.Outcome:
|
|
5
|
+
""" A method to convert from the python AresDataType class to the protobuf version """
|
|
6
|
+
return py_value.value
|
|
7
|
+
|
|
8
|
+
def proto_ares_outcome_to_python_ares_outcome(proto_value: ares_outcome_enum_pb2.Outcome) -> Outcome:
|
|
9
|
+
""" A method to convert from the protobuf AresDataType class to the python version """
|
|
10
|
+
return Outcome(proto_value)
|