PyAres 0.1.4__tar.gz → 0.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 (46) hide show
  1. {pyares-0.1.4 → pyares-0.2.0}/PKG-INFO +2 -2
  2. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Analyzing/analysis_service.py +22 -7
  3. {pyares-0.1.4/PyAres/Test → pyares-0.2.0/PyAres/Demo}/analyzer_test.py +2 -0
  4. pyares-0.2.0/PyAres/Demo/analyzer_test_tools_demo.py +19 -0
  5. pyares-0.2.0/PyAres/Demo/analyzer_wiki.py +31 -0
  6. pyares-0.2.0/PyAres/Demo/hotplate.py +72 -0
  7. pyares-0.2.0/PyAres/Demo/planner_test_tools_demo.py +21 -0
  8. pyares-0.2.0/PyAres/Demo/planner_wiki.py +29 -0
  9. pyares-0.2.0/PyAres/Demo/rotary_mixer.py +37 -0
  10. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Device/device_service.py +23 -6
  11. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Models/ares_data_models.py +7 -1
  12. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Planning/planner_models.py +28 -10
  13. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Planning/planning_service.py +33 -13
  14. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_data_schema_utils.py +5 -13
  15. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_data_type_utils.py +2 -2
  16. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_outcome_utils.py +1 -1
  17. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_struct_utils.py +32 -16
  18. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_value_utils.py +16 -13
  19. pyares-0.2.0/PyAres/Utils/param_history_info_utils.py +10 -0
  20. pyares-0.2.0/PyAres/Utils/plan_request_utils.py +11 -0
  21. pyares-0.2.0/PyAres/Utils/plan_response_utils.py +16 -0
  22. pyares-0.2.0/PyAres/Utils/planning_param_utils.py +20 -0
  23. pyares-0.2.0/PyAres/test_tools/__init__.py +2 -0
  24. pyares-0.2.0/PyAres/test_tools/analyzer_tester.py +92 -0
  25. pyares-0.2.0/PyAres/test_tools/planner_tester.py +86 -0
  26. {pyares-0.1.4 → pyares-0.2.0}/PyAres.egg-info/PKG-INFO +2 -2
  27. {pyares-0.1.4 → pyares-0.2.0}/PyAres.egg-info/SOURCES.txt +17 -4
  28. {pyares-0.1.4 → pyares-0.2.0}/PyAres.egg-info/requires.txt +1 -1
  29. {pyares-0.1.4 → pyares-0.2.0}/README.md +5 -1
  30. {pyares-0.1.4 → pyares-0.2.0}/pyproject.toml +2 -5
  31. {pyares-0.1.4 → pyares-0.2.0}/LICENSE +0 -0
  32. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Analyzing/__init__.py +0 -0
  33. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Analyzing/analyzer_models.py +0 -0
  34. {pyares-0.1.4/PyAres/Test → pyares-0.2.0/PyAres/Demo}/device_test.py +0 -0
  35. {pyares-0.1.4/PyAres/Test → pyares-0.2.0/PyAres/Demo}/planner_test.py +0 -0
  36. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Device/__init__.py +0 -0
  37. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Device/device_models.py +0 -0
  38. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Device/device_warnings.py +0 -0
  39. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Models/__init__.py +0 -0
  40. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Planning/__init__.py +0 -0
  41. {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_device_command_utils.py +0 -0
  42. {pyares-0.1.4 → pyares-0.2.0}/PyAres/__init__.py +0 -0
  43. {pyares-0.1.4 → pyares-0.2.0}/PyAres.egg-info/dependency_links.txt +0 -0
  44. {pyares-0.1.4 → pyares-0.2.0}/PyAres.egg-info/top_level.txt +0 -0
  45. {pyares-0.1.4 → pyares-0.2.0}/setup.cfg +0 -0
  46. {pyares-0.1.4 → pyares-0.2.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAres
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: The official Python library for ARES.
5
5
  Author-email: AFRL <AFRL.RX.AREScontact@us.af.mil>
6
6
  License-Expression: MIT
@@ -9,5 +9,5 @@ Requires-Python: >=3.10
9
9
  License-File: LICENSE
10
10
  Requires-Dist: grpcio-tools>=1.75.1
11
11
  Requires-Dist: grpcio<2.0.0,>=1.71.0
12
- Requires-Dist: ares-datamodel>=0.13.2b1
12
+ Requires-Dist: ares-datamodel>=0.15.1b1
13
13
  Dynamic: license-file
@@ -177,7 +177,8 @@ class AresAnalyzerService:
177
177
  description: str = "",
178
178
  timeout: int = 30,
179
179
  use_localhost: bool = True,
180
- port: int = 7083):
180
+ port: int = 7083,
181
+ max_message_size: int = -1):
181
182
  """
182
183
  Initializes the AresAnalyzerService.
183
184
 
@@ -190,11 +191,17 @@ class AresAnalyzerService:
190
191
  description (str): A brief description of your analyzer.
191
192
  use_localhost (bool): If true, binds to localhost. Otherwise, binds to [::].
192
193
  port (int): The port that your analyzer service will serve on. Defaults to port 7083.
194
+ max_message_size (int): The max size, in megabytes, of the messages your Analysis service is capable of sending. Increasing this can help transfer data like images, but may result in some loss in performance
193
195
  """
196
+
194
197
  self.info = InfoResponse(name=name, version=version, description=description)
195
198
  self._capabilities = analyzer_capabilities_pb2.AnalyzerCapabilities(settings_schema={})
196
199
  self._port = port
197
- self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
200
+ server_options = []
201
+ if max_message_size != -1:
202
+ print("Setting Custom Max Message Size")
203
+ server_options.append(('grpc.max_receive_message_length', max_message_size * 1024 * 1024))
204
+ self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10), options=server_options)
198
205
  self._service_wrapper = AresAnalyzerServiceWrapper(info=self.info, timeout=timeout, custom_analysis_logic=custom_analysis_logic)
199
206
  analyzer_service_grpc.add_AresRemoteAnalyzerServiceServicer_to_server(self._service_wrapper, self._server)
200
207
 
@@ -238,13 +245,21 @@ class AresAnalyzerService:
238
245
  """
239
246
  self._capabilities.timeout_seconds = new_timeout
240
247
 
241
- def start(self):
242
- """
243
- Starts the service on the specified port, and waits for termination.
248
+ def start(self, wait_for_termination: bool = True):
249
+ """
250
+ Starts the service on the specified port, and waits for termination.
251
+
252
+ Args:
253
+ wait_for_termination (bool): A boolean value that determines whether the start method will use the "wait_for_termination" blocking call.
254
+ If true, the gRPC service will keep the main thread alive but at the cost of blocking any continued execution of your python logic.
255
+ Setting this value to false will allow you to continue execution after starting your service, however this should ONLY be done if you have
256
+ another mechanism for keeping your process alive (such as a GUI, or a loop). Defaults to true.
244
257
  """
245
- print(f"Starting Ares Analyzer Service on port {self._port}...")
258
+ print(f"Starting Ares Device Service on port {self._port}...")
246
259
  self._server.start()
247
- self._server.wait_for_termination()
260
+
261
+ if wait_for_termination:
262
+ self._server.wait_for_termination()
248
263
 
249
264
  def stop(self):
250
265
  """
@@ -23,5 +23,7 @@ if __name__ == "__main__":
23
23
 
24
24
  #Add Analysis Parameters
25
25
  pythonDemoAnalyzer.add_analysis_parameter("Temperature", AresDataType.NUMBER)
26
+ pythonDemoAnalyzer.add_setting(setting_name="", setting_type=AresDataType.NULL, optional=True, constraints=[])
27
+ pythonDemoAnalyzer.start(wait_for_termination=True)
26
28
 
27
29
  pythonDemoAnalyzer.start()
@@ -0,0 +1,19 @@
1
+ from PyAres.test_tools import AnalyzerTestClient
2
+
3
+
4
+ if __name__ == "__main__":
5
+ client = AnalyzerTestClient()
6
+
7
+ # 1. Health Checks
8
+ client.check_status()
9
+ client.get_info()
10
+
11
+ # 2. Run a Mock Analysis
12
+ # Replace these keys with actual data your analyzer expects
13
+ sample_inputs = {
14
+ "Temperature": 130.5
15
+ }
16
+
17
+ client.run_analysis(inputs=sample_inputs)
18
+
19
+
@@ -0,0 +1,31 @@
1
+ from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome
2
+
3
+ def analyze_sample(request: AnalysisRequest) -> Analysis:
4
+ # 1. Extract inputs
5
+ # 'Growth_Metric' would come from a sensor or previous step
6
+ raw_value = request.inputs.get("Growth_Metric")
7
+
8
+ if raw_value is None:
9
+ return Analysis(result=0.0, outcome=Outcome.FAILURE)
10
+
11
+ # 2. Perform Logic
12
+ print(f"Analyzing sample with value: {raw_value}")
13
+
14
+ calculated_score = raw_value * 1.5 # Placeholder logic
15
+ is_success = calculated_score > 10.0 # Define success criteria
16
+
17
+ # 3. Return Result
18
+ return Analysis(result=calculated_score, outcome=Outcome.SUCCESS)
19
+
20
+ if __name__ == "__main__":
21
+ service = AresAnalyzerService(
22
+ analyze_sample,
23
+ "Growth Analyzer",
24
+ "0.1.0",
25
+ "Calculates growth viability"
26
+ )
27
+
28
+ # Define what data we need from ARES
29
+ service.add_analysis_parameter("Growth_Metric", AresDataType.NUMBER)
30
+
31
+ service.start()
@@ -0,0 +1,72 @@
1
+ from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor
2
+
3
+ # --- PART 1: The Simulated Hardware ---
4
+ class VirtualHotplate:
5
+ def __init__(self):
6
+ self.target_temp = 25.0 # Start at room temp
7
+
8
+ def set_temperature(self, temp: float):
9
+ """Simulates setting the heater."""
10
+ print(f"[Hardware] Heating to {temp}°C...")
11
+ self.target_temp = temp
12
+ return {} # Return empty dict if no data needs to be sent back
13
+
14
+ def get_temperature(self):
15
+ """Simulates reading the sensor."""
16
+ # In a real device, you'd read a serial port here.
17
+ print("[Hardware] Retrieving the current temperature...")
18
+ return { "current_temp": self.target_temp }
19
+
20
+ def get_state(self):
21
+ """Required: Tells ARES the current status for logging."""
22
+ return { "current_temp": self.target_temp }
23
+
24
+ def safe_mode(self):
25
+ """Required: A safety fallback (e.g., turn off heat)."""
26
+ print("[Hardware] SAFE MODE TRIGGERED: Heater off.")
27
+ self.target_temp = 0.0
28
+
29
+ # --- PART 2: The Ares Service Wrapper ---
30
+ if __name__ == "__main__":
31
+ # 1. Initialize the hardware
32
+ my_hotplate = VirtualHotplate()
33
+
34
+ # 2. Define the Service Info
35
+ service = AresDeviceService(
36
+ my_hotplate.safe_mode,
37
+ my_hotplate.get_state,
38
+ "My Virtual Hotplate", # Device Name
39
+ "A simulated lab hotplate", # Description
40
+ "1.0.0" # Version
41
+ )
42
+
43
+ # 3. Define Command: Set Temperature
44
+ # This schema tells ARES to draw a Number Input box in the UI
45
+ input_schema = {
46
+ "temp": DeviceSchemaEntry(AresDataType.NUMBER, "Target Temperature", "Celsius")
47
+ }
48
+ set_cmd = DeviceCommandDescriptor(
49
+ "Set Temp",
50
+ "Sets the hotplate target temperature",
51
+ input_schema,
52
+ {} # No output expected
53
+ )
54
+ service.add_new_command(set_cmd, my_hotplate.set_temperature)
55
+
56
+ # 4. Define Command: Get Temperature
57
+ # This schema tells ARES to expect a number back
58
+ output_schema = {
59
+ "current_temp": DeviceSchemaEntry(AresDataType.NUMBER, "Current Temperature", "Celsius")
60
+ }
61
+ get_cmd = DeviceCommandDescriptor(
62
+ "Get Temp",
63
+ "Reads the current temperature",
64
+ {}, # No input needed
65
+ output_schema
66
+ )
67
+ service.add_new_command(get_cmd, my_hotplate.get_temperature)
68
+
69
+ # 5. Start the Service
70
+ # This will block and listen for ARES connections
71
+ print("Virtual Hotplate Service Running...")
72
+ service.start()
@@ -0,0 +1,21 @@
1
+ from PyAres import PlanRequest, PlanningParameter, AresDataType, ParameterHistoryItem
2
+ from PyAres.test_tools import PlannerTestClient
3
+
4
+ if __name__ == "__main__":
5
+ client = PlannerTestClient()
6
+
7
+ # 1. Health Checks
8
+ client.check_status()
9
+ client.get_info()
10
+
11
+ # 2. Mock Data
12
+ planning_parameters = []
13
+ planning_parameters.append(PlanningParameter("Param One", 0, 100, [ParameterHistoryItem(123, 128)], AresDataType.NUMBER, False, False, "Random Planner"))
14
+ planning_parameters.append(PlanningParameter("Param Two", 0, 500, [ParameterHistoryItem(132, 122)], AresDataType.NUMBER, False, False, "Random Planner"))
15
+ planning_parameters.append(PlanningParameter("Param Three", 0, 500, [ParameterHistoryItem(141, 121)], AresDataType.NUMBER, False, False, "Random Planner"))
16
+ setting_dict = {"Random Setting" : 4000}
17
+ analysis_results = [10, 20, 30]
18
+
19
+ # 3. Create and Send the Request
20
+ request = PlanRequest(planning_parameters, setting_dict, analysis_results)
21
+ response = client.run_planning(request)
@@ -0,0 +1,29 @@
1
+ from PyAres import AresPlannerService, PlanRequest, PlanResponse, AresDataType
2
+ import random
3
+
4
+ def generate_plan(request: PlanRequest) -> PlanResponse:
5
+ planned_values = []
6
+ names = []
7
+
8
+ # Iterate through every parameter configured in the ARES Experiment
9
+ for param in request.parameters:
10
+ # Simple Logic: Pick a random value within the allowed range
11
+ val = random.uniform(param.minimum_value, param.maximum_value)
12
+
13
+ names.append(param.name)
14
+ planned_values.append(val)
15
+
16
+ return PlanResponse(parameter_names=names, parameter_values=planned_values)
17
+
18
+ if __name__ == "__main__":
19
+ service = AresPlannerService(
20
+ generate_plan,
21
+ "Random Search Planner",
22
+ "This planner picks random values within bounds.",
23
+ "1.0.0"
24
+ )
25
+
26
+ # Tell ARES we can plan for Numeric values
27
+ service.add_supported_type(AresDataType.NUMBER)
28
+
29
+ service.start()
@@ -0,0 +1,37 @@
1
+ from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor
2
+
3
+ # 1. Define your hardware logic
4
+ def set_speed(rpm: float):
5
+ print(f"Setting motor speed to {rpm}")
6
+ # Hardware communication goes here...
7
+ return {} # Return empty dict if no data needs to be sent back
8
+
9
+ def get_status():
10
+ # Return a dictionary matching your state schema
11
+ return { "rpm": 1200 }
12
+
13
+ def safe_mode():
14
+ print("Stopping motor immediately!")
15
+
16
+ # 2. Initialize Service
17
+ service = AresDeviceService(
18
+ safe_mode,
19
+ get_status,
20
+ "Rotary Mixer",
21
+ "High-speed mixer control",
22
+ "1.0.0",
23
+ port=7101
24
+ )
25
+
26
+ # 3. Define the 'Set Speed' Command
27
+ # Input: One number (Speed)
28
+ input_schema = {
29
+ "rpm": DeviceSchemaEntry(AresDataType.NUMBER, "Speed in RPM", "RPM")
30
+ }
31
+ cmd_descriptor = DeviceCommandDescriptor("Set Speed", "Sets mixer speed", input_schema, {})
32
+
33
+ # 4. Register the command
34
+ service.add_new_command(cmd_descriptor, set_speed)
35
+
36
+ # 5. Start
37
+ service.start()
@@ -23,8 +23,8 @@ from ..Utils import ares_data_type_utils
23
23
 
24
24
  # Type hint for the user's custom methods
25
25
  EnterSafeModeMethod = Callable[[], None]
26
- DeviceCommandMethod = Callable[..., Dict[str, any]]
27
- DeviceStateMethod = Callable[[], Dict[str, any]]
26
+ DeviceCommandMethod = Callable[..., Dict[str, Any]]
27
+ DeviceStateMethod = Callable[[], Dict[str, Any]]
28
28
 
29
29
  class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServicer):
30
30
  """
@@ -211,7 +211,14 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
211
211
 
212
212
  class AresDeviceService:
213
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):
214
+ def __init__(self,
215
+ enter_safe_mode_logic: EnterSafeModeMethod,
216
+ get_device_state_logic: DeviceStateMethod,
217
+ device_name: str,
218
+ description: str,
219
+ version: str,
220
+ use_localhost: bool = True,
221
+ port: int = 7100):
215
222
  """
216
223
  Initializes the AresDeviceService
217
224
 
@@ -273,11 +280,21 @@ class AresDeviceService:
273
280
  new_ares_value = ares_value_utils.create_ares_value(setting_value)
274
281
  self._service_wrapper._current_settings[setting_name] = new_ares_value
275
282
 
276
- def start(self):
277
- """ Starts the service on the specified port, and waits for termination. """
283
+ def start(self, wait_for_termination: bool = True):
284
+ """
285
+ Starts the service on the specified port, and waits for termination.
286
+
287
+ Args:
288
+ wait_for_termination (bool): A boolean value that determines whether the start method will use the "wait_for_termination" blocking call.
289
+ If true, the gRPC service will keep the main thread alive but at the cost of blocking any continued execution of your python logic.
290
+ Setting this value to false will allow you to continue execution after starting your service, however this should ONLY be done if you have
291
+ another mechanism for keeping your process alive (such as a GUI, or a loop). Defaults to true.
292
+ """
278
293
  print(f"Starting Ares Device Service on port {self._port}...")
279
294
  self._server.start()
280
- self._server.wait_for_termination()
295
+
296
+ if wait_for_termination:
297
+ self._server.wait_for_termination()
281
298
 
282
299
  def stop(self):
283
300
  """ Stops the service, terminating the connection. """
@@ -24,4 +24,10 @@ class RequestMetadata():
24
24
  self.system_name = proto_metadata.system_name
25
25
  self.campaign_name = proto_metadata.campaign_name
26
26
  self.campaign_id = proto_metadata.campaign_id
27
- self.experiment_id = proto_metadata.experiment_id
27
+ self.experiment_id = proto_metadata.experiment_id
28
+
29
+ @classmethod
30
+ def from_default_values(cls):
31
+ """ Alternative constructor for creating fake metadata """
32
+ default = request_metadata_pb2.RequestMetadata(system_name="TEST SYSTEM", campaign_name="TEST CAMPAIGN", campaign_id="TEST ID", experiment_id="TEST EXPERIMENT ID")
33
+ return cls(default)
@@ -1,4 +1,4 @@
1
- from typing import Dict, Any, List
1
+ from typing import Dict, Any, List, Sequence
2
2
  from ..Models import Outcome, AresDataType, RequestMetadata
3
3
 
4
4
  class ParameterHistoryItem:
@@ -38,23 +38,41 @@ class PlanningParameter:
38
38
  planner_name: The name of the planner ARES requested be used to plan for this parameter.
39
39
  initial_value: An optional initial value for the given parameter
40
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
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
49
  self.initial_value = initial_value
50
50
 
51
+
52
+ class ParamHistoryInfo:
53
+ """
54
+ Represents the history of a given parameter.
55
+
56
+ Designed to provide a more user-friendly abstraction for interacting with a param history object.
57
+ """
58
+ def __init__(self, planned_value: Any, achieved_value: Any):
59
+ """
60
+ Initializes a ParamHistoryInfo.
61
+
62
+ Args:
63
+ planned_value (Any): The value given directly from the planner.
64
+ achieved_value (Any): An optional value that represents the real world achieved value, which may differ from the planners target value.
65
+ """
66
+ self.planned_value = planned_value
67
+ self.achieved_value = achieved_value
68
+
51
69
  class PlanRequest:
52
70
  """
53
71
  Represents a PlanRequest message received from ARES.
54
72
 
55
73
  Designed to provide a more user-friendly abstraction for interacting with a plan request message.
56
74
  """
57
- def __init__(self, parameters: list[PlanningParameter], settings: Dict[str, Any], analysis_results: list[float], metadata: RequestMetadata):
75
+ def __init__(self, parameters: list[PlanningParameter], settings: Dict[str, Any], analysis_results: Sequence[float], metadata: RequestMetadata = RequestMetadata.from_default_values()):
58
76
  """
59
77
  Initializes a PlanRequest.
60
78
 
@@ -166,19 +166,27 @@ class AresPlannerService:
166
166
  """
167
167
  Manages the gRPC server for the AresPlannerService
168
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):
169
+ def __init__(self, custom_plan_logic: PlanLogicFunction,
170
+ service_name: str,
171
+ service_description: str,
172
+ service_version: str,
173
+ timeout: int = 30,
174
+ use_localhost: bool = True,
175
+ port: int = 7082,
176
+ max_message_size: int = -1):
170
177
  """
171
178
  Initializes the AresPlannerService
172
179
 
173
180
  Args:
174
- custom_plan_logic: A callable function that will be executed when a PlanRequest is received.
181
+ custom_plan_logic (PlanLogicFunction): A callable function that will be executed when a PlanRequest is received.
175
182
  This function should accept a 'PyARES.AresPlanning.PlanRequest' object and return a
176
183
  '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.
184
+ service_name (str): The name descriptor that is associated with your planner service.
185
+ service_description (str): A brief description describing your implementation of the planner service.
186
+ service_version (str): The version of your planner service.
187
+ use_localhost (bool): An optional value that allows the user to specify whether to host the service on the local network. Defaults to True.
188
+ port (int): The port that your planner service will serve on. Defaults to port 7082.
189
+ max_message_size (int): The max size, in megabytes, of the messages your Planning service is capable of sending. Increasing this can help transfer heavy data like images, but may result in some loss in performance
182
190
  """
183
191
  #Public Values, designed to be accessible to the user
184
192
  self.service_name = service_name
@@ -187,7 +195,11 @@ class AresPlannerService:
187
195
 
188
196
  #Private values, mostly related to the service
189
197
  self._port = port
190
- self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
198
+ server_options = []
199
+ if max_message_size != -1:
200
+ print("Setting Custom Max Message Size")
201
+ server_options.append(('grpc.max_receive_message_length', max_message_size * 1024 * 1024))
202
+ self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10), options=server_options)
191
203
  self._service_wrapper = AresPlannerServiceWrapper(service_name, service_description, service_version, timeout, custom_plan_logic)
192
204
  planner_service_grpc.add_AresRemotePlannerServiceServicer_to_server(self._service_wrapper, self._server)
193
205
  if(use_localhost):
@@ -236,13 +248,21 @@ class AresPlannerService:
236
248
  """
237
249
  self._service_wrapper._timeout = new_timeout
238
250
 
239
- def start(self):
240
- """
241
- Starts the service on the specified port, and waits for termination.
251
+ def start(self, wait_for_termination: bool = True):
252
+ """
253
+ Starts the service on the specified port, and waits for termination.
254
+
255
+ Args:
256
+ wait_for_termination (bool): A boolean value that determines whether the start method will use the "wait_for_termination" blocking call.
257
+ If true, the gRPC service will keep the main thread alive but at the cost of blocking any continued execution of your python logic.
258
+ Setting this value to false will allow you to continue execution after starting your service, however this should ONLY be done if you have
259
+ another mechanism for keeping your process alive (such as a GUI, or a loop). Defaults to true.
242
260
  """
243
- print(f"Starting Ares Planner Service on port {self._port}...")
261
+ print(f"Starting Ares Device Service on port {self._port}...")
244
262
  self._server.start()
245
- self._server.wait_for_termination()
263
+
264
+ if wait_for_termination:
265
+ self._server.wait_for_termination()
246
266
 
247
267
  def stop(self):
248
268
  """
@@ -1,18 +1,6 @@
1
- from typing import Union, Dict
2
-
3
- #Datamodel Imports
1
+ from typing import Union
4
2
  from ares_datamodel import ares_data_schema_pb2
5
-
6
3
  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
4
 
17
5
  def create_settings_schema_entry(
18
6
  setting_type: ares_data_models.AresDataType,
@@ -40,5 +28,9 @@ def create_settings_schema_entry(
40
28
  schema_entry = ares_data_schema_pb2.SchemaEntry(type=setting_type.value, optional=optional)
41
29
  schema_entry.number_choices.numbers.extend(choices)
42
30
 
31
+ else:
32
+ schema_entry = ares_data_schema_pb2.SchemaEntry(type=setting_type.value, optional=optional)
33
+
34
+
43
35
  return schema_entry
44
36
 
@@ -4,7 +4,7 @@ from ..Models import AresDataType
4
4
 
5
5
  def python_ares_type_to_proto_ares_type(py_value: AresDataType) -> ares_data_type_pb2.AresDataType:
6
6
  """ A method to convert from the python AresDataType class to the protobuf version """
7
- return py_value.value
7
+ return ares_data_type_pb2.AresDataType(py_value.value)
8
8
 
9
9
  def proto_ares_type_to_python_ares_type(proto_value: ares_data_type_pb2.AresDataType) -> AresDataType:
10
10
  """ A method to convert from the protobuf AresDataType class to the python version """
@@ -28,7 +28,7 @@ def determine_python_ares_data_type(value: Union[int, float, str, bool, list]):
28
28
  return AresDataType.STRING_ARRAY
29
29
  elif(all(isinstance(x, (int, float)) for x in value)):
30
30
  return AresDataType.NUMBER_ARRAY
31
- elif(all(isinstance(x, bool)) for x in value):
31
+ elif(all(isinstance(x, bool) for x in value)):
32
32
  return AresDataType.BOOL_ARRAY
33
33
  else:
34
34
  return AresDataType.UNKNOWN
@@ -3,7 +3,7 @@ from ares_datamodel import ares_outcome_enum_pb2
3
3
 
4
4
  def python_ares_outcome_to_proto_ares_outcome(py_value: Outcome) -> ares_outcome_enum_pb2.Outcome:
5
5
  """ A method to convert from the python AresDataType class to the protobuf version """
6
- return py_value.value
6
+ return ares_outcome_enum_pb2.Outcome(py_value.value)
7
7
 
8
8
  def proto_ares_outcome_to_python_ares_outcome(proto_value: ares_outcome_enum_pb2.Outcome) -> Outcome:
9
9
  """ A method to convert from the protobuf AresDataType class to the python version """
@@ -1,10 +1,10 @@
1
1
  from ares_datamodel import ares_struct_pb2
2
- from typing import Union, Dict
2
+ from typing import Union, Dict, Any
3
3
 
4
4
  from . import ares_value_utils
5
5
  import copy
6
6
 
7
- def ares_struct_to_dict(ares_struct: ares_struct_pb2.AresStruct) -> Dict[str, any]:
7
+ def ares_struct_to_dict(ares_struct: ares_struct_pb2.AresStruct) -> Dict[str, Any]:
8
8
  """Converts an AresStruct protobuf message to a Python dictionary."""
9
9
  return {k: ares_value_utils.ares_value_to_py(v) for k, v in ares_struct.fields.items()}
10
10
 
@@ -62,6 +62,9 @@ def create_bool_struct(key: str, value: bool) -> ares_struct_pb2.AresStruct:
62
62
  Returns:
63
63
  (AresStruct): A new AresStruct containing the provided key and value.
64
64
  """
65
+ new_struct = ares_struct_pb2.AresStruct()
66
+ new_struct.fields[key] = ares_value_utils.create_bool(value)
67
+ return new_struct
65
68
 
66
69
  def create_null_struct(key: str) -> ares_struct_pb2.AresStruct:
67
70
  """
@@ -168,7 +171,16 @@ def copy_struct(existing_struct: ares_struct_pb2.AresStruct) -> ares_struct_pb2.
168
171
  """
169
172
  return copy.deepcopy(existing_struct)
170
173
 
171
- def create_ares_struct(key: str, value: any):
174
+ def create_empty_struct() -> ares_struct_pb2.AresStruct:
175
+ """
176
+ Creates a new empty AresStruct
177
+
178
+ Returns:
179
+ (AresStruct): The newly created empty AresStruct.
180
+ """
181
+ return ares_struct_pb2.AresStruct()
182
+
183
+ def create_ares_struct(key: str, value: Any):
172
184
  """
173
185
  Creates a new AresStruct using the provided key and value.
174
186
 
@@ -181,28 +193,32 @@ def create_ares_struct(key: str, value: any):
181
193
  """
182
194
 
183
195
  if(isinstance(value, str)):
184
- return create_string_struct(value)
196
+ return create_string_struct(key, value)
185
197
 
186
198
  elif(isinstance(value, (int, float))):
187
- return create_number_struct(value)
199
+ return create_number_struct(key, value)
188
200
 
189
201
  elif(isinstance(value, bool)):
190
- return create_bool_struct(value)
191
-
192
- elif(isinstance(value, (list[int], list[float]))):
193
- return create_number_array_struct(value)
202
+ return create_bool_struct(key, value)
194
203
 
195
204
  elif(isinstance(value, bytes)):
196
- return create_bytes_array_struct(value)
197
-
198
- elif(isinstance(value, list[str])):
199
- return create_string_array_struct(value)
205
+ return create_bytes_array_struct(key, value)
200
206
 
201
- elif(isinstance(value, list[bool])):
202
- return create_bool_array_struct(value)
207
+ elif(isinstance(value, list)):
208
+ if(len(value) == 0):
209
+ return create_null_struct(key)
210
+
211
+ elif(all(isinstance(item, str) for item in value)):
212
+ return create_string_array_struct(key, value)
213
+
214
+ elif(all(isinstance(item, (float, int)) for item in value)):
215
+ return create_number_array_struct(key, value)
216
+
217
+ elif(all(isinstance(item, bool) for item in value)):
218
+ return create_bool_array_struct(key, value)
203
219
 
204
220
  else:
205
- return create_null_struct()
221
+ return create_null_struct(key)
206
222
 
207
223
 
208
224
 
@@ -1,8 +1,7 @@
1
1
  from ares_datamodel import ares_struct_pb2
2
2
  from ares_datamodel import ares_data_type_pb2
3
- from typing import Union
3
+ from typing import Union, Any
4
4
 
5
- from . import ares_struct_utils
6
5
  from . import ares_data_type_utils
7
6
  from ..Models import AresDataType
8
7
 
@@ -33,14 +32,13 @@ def py_to_ares_value(py_value, ares_value: ares_struct_pb2.AresValue):
33
32
  ares_value.number_value = py_value
34
33
  elif isinstance(py_value, bytes):
35
34
  ares_value.bytes_value = py_value
36
- elif isinstance(py_value, dict):
37
- ares_struct_utils.dict_to_ares_struct(py_value, ares_value.struct_value)
38
35
  elif isinstance(py_value, list):
39
- for item in py_value:
40
- new_val = ares_value.list_value.values.add()
41
- py_to_ares_value(item, new_val)
42
- elif py_value is None:
43
- ares_value.null_value = 0 # google.protobuf.NullValue.NULL_VALUE
36
+ if(all(isinstance(x, str) for x in py_value)):
37
+ ares_value.string_array_value.strings.extend(py_value)
38
+ elif(all(isinstance(x, (int, float)) for x in py_value)):
39
+ ares_value.number_array_value.numbers.extend(py_value)
40
+ elif(all(isinstance(x, bool) for x in py_value)):
41
+ ares_value.bool_array_value.bools.extend(py_value)
44
42
  else:
45
43
  raise TypeError(f"Unsupported type for AresValue: {type(py_value)}")
46
44
 
@@ -137,7 +135,9 @@ def create_bool_array(value: list[bool]) -> ares_struct_pb2.AresValue:
137
135
  Returns:
138
136
  (AresValue): A new AresValue containing the provided list of booleans.
139
137
  """
140
- return ares_struct_pb2.AresValue(bool_array_value=value)
138
+ ares_value = ares_struct_pb2.AresValue()
139
+ ares_value.bool_array_value.bools.extend(value)
140
+ return ares_value
141
141
 
142
142
  def create_default(python_datatype: AresDataType) -> ares_struct_pb2.AresValue:
143
143
  """
@@ -177,7 +177,7 @@ def create_default(python_datatype: AresDataType) -> ares_struct_pb2.AresValue:
177
177
  else:
178
178
  return create_null()
179
179
 
180
- def create_ares_value(value: any) -> ares_struct_pb2.AresValue:
180
+ def create_ares_value(value: Any) -> ares_struct_pb2.AresValue:
181
181
  """
182
182
  Creates a new AresValue using the provided value.
183
183
  If the provided value is not valid for use in an AresValue, a null AresValue is returned.
@@ -198,7 +198,7 @@ def create_ares_value(value: any) -> ares_struct_pb2.AresValue:
198
198
  return create_bool(value)
199
199
 
200
200
  elif(isinstance(value, bytes)):
201
- return create_bool_array(value)
201
+ return create_bytes(value)
202
202
 
203
203
  elif(isinstance(value, list)):
204
204
  if all(isinstance(x, (int, float)) for x in value):
@@ -209,6 +209,9 @@ def create_ares_value(value: any) -> ares_struct_pb2.AresValue:
209
209
 
210
210
  elif all(isinstance(x, bool) for x in value):
211
211
  return create_bool_array(value)
212
-
212
+
213
+ else:
214
+ return create_null()
215
+
213
216
  else:
214
217
  return create_null()
@@ -0,0 +1,10 @@
1
+ from ..Planning import ParameterHistoryItem
2
+ from ares_datamodel.planning import plan_pb2
3
+ from . import ares_value_utils
4
+
5
+ def convert_proto_param_history_to_python(proto: plan_pb2.ParameterHistoryInfo) -> ParameterHistoryItem:
6
+ return ParameterHistoryItem(proto.planned_value, proto.achieved_value)
7
+
8
+ def convert_python_param_history_to_proto(python: ParameterHistoryItem) -> plan_pb2.ParameterHistoryInfo:
9
+ info = plan_pb2.ParameterHistoryInfo(planned_value=ares_value_utils.create_ares_value(python.planned_value), achieved_value=ares_value_utils.create_ares_value(python.achieved_value))
10
+ return info
@@ -0,0 +1,11 @@
1
+ from ares_datamodel.planning import plan_pb2
2
+ from ..Planning import PlanRequest
3
+ from . import planning_param_utils
4
+ from . import ares_struct_utils
5
+
6
+ def python_plan_request_to_proto(request: PlanRequest) -> plan_pb2.PlanningRequest:
7
+ proto_request = plan_pb2.PlanningRequest()
8
+ proto_request.analysis_results.extend(request.analysis_results)
9
+ ares_struct_utils.dict_to_ares_struct(request.settings, proto_request.adapter_settings)
10
+ proto_request.planning_parameters.extend([planning_param_utils.convert_python_plan_param_to_proto(p) for p in request.parameters])
11
+ return proto_request
@@ -0,0 +1,16 @@
1
+ from ares_datamodel.planning import plan_pb2
2
+ from ..Planning import PlanResponse
3
+ from . import ares_outcome_utils
4
+
5
+ def proto_plan_response_to_python(proto_response: plan_pb2.PlanningResponse) -> PlanResponse:
6
+ param_names = []
7
+ param_values = []
8
+
9
+ for param in proto_response.planned_parameters:
10
+ param_names.append(param.parameter_name)
11
+ param_values.append(param.parameter_value)
12
+
13
+ python_response = PlanResponse(param_names, param_values)
14
+ python_response.outcome = ares_outcome_utils.proto_ares_outcome_to_python_ares_outcome(proto_response.planning_outcome)
15
+ python_response.error_string = proto_response.error_string
16
+ return python_response
@@ -0,0 +1,20 @@
1
+ from ..Planning import PlanningParameter, PlanResponse
2
+ from ares_datamodel.planning import plan_pb2
3
+ from typing import List
4
+ from . import param_history_info_utils
5
+ from . import ares_data_type_utils
6
+ from . import ares_value_utils
7
+
8
+ def convert_python_plan_param_to_proto(param: PlanningParameter) -> plan_pb2.PlanningParameter:
9
+ new_proto_param = plan_pb2.PlanningParameter()
10
+ new_proto_param.parameter_name = param.name
11
+ new_proto_param.minimum_value = param.minimum_value
12
+ new_proto_param.maximum_value = param.maximum_value
13
+ new_proto_param.parameter_history.extend([param_history_info_utils.convert_python_param_history_to_proto(p) for p in param.param_history])
14
+ new_proto_param.data_type = ares_data_type_utils.python_ares_type_to_proto_ares_type(param.data_type)
15
+ new_proto_param.is_planned = param.is_planned
16
+ new_proto_param.is_result = param.is_result
17
+ new_proto_param.planner_name = param.planner_name
18
+ ares_value_utils.py_to_ares_value(param.initial_value, new_proto_param.initial_value)
19
+
20
+ return new_proto_param
@@ -0,0 +1,2 @@
1
+ from .analyzer_tester import AnalyzerTestClient
2
+ from .planner_tester import PlannerTestClient
@@ -0,0 +1,92 @@
1
+ import grpc
2
+ import time
3
+ from typing import Dict
4
+
5
+ from ..Utils import ares_struct_utils
6
+ from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2_grpc as analyzer_service_grpc
7
+ from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service
8
+ from ares_datamodel import ares_analyzer_management_service_pb2
9
+ from ares_datamodel.connection import connection_status_pb2
10
+ from ares_datamodel.analyzing import analysis_pb2
11
+ from ares_datamodel import ares_outcome_enum_pb2
12
+
13
+ class AnalyzerTestClient:
14
+ def __init__(self, port=7083, host='localhost'):
15
+ """
16
+ Initializes an AnalyzerTestClient, useful for testing your PyAres Analyzers.
17
+
18
+ Args:
19
+ port (int): The port that your analyzer test client will use to try and reach your service. Defaults to 7083.
20
+ host (str): The host name that the client will use to try and reach your service. Defaults to localhost.
21
+ """
22
+ self.target = f"{host}:{port}"
23
+ self.channel = grpc.insecure_channel(self.target)
24
+ self.stub = analyzer_service_grpc.AresRemoteAnalyzerServiceStub(self.channel)
25
+ print(f"Client connected to {self.target}")
26
+
27
+ def get_info(self):
28
+ """Used to test that your Analyzer Service is providing info properly. If successful outputs the name, version and description of your service."""
29
+ print("\n--- Testing GetInfo ---")
30
+ try:
31
+ request = ares_analyzer_management_service_pb2.AnalyzerInfoRequest()
32
+ response = self.stub.GetInfo(request)
33
+ print(f"Success! Connected to: {response.name} (version {response.version})")
34
+ print(f"Description: {response.description}")
35
+
36
+ except grpc.RpcError as e:
37
+ print(f"RPC Failed: {e.code()} - {e.details()}")
38
+
39
+ def check_status(self):
40
+ """Checks the status of your Analyzer Service"""
41
+ print("\n--- Testing Connection Status ---")
42
+ try:
43
+ request = connection_status_pb2.ConnectionStatusRequest()
44
+ response: connection_status_pb2.ConnectionStatus = self.stub.GetConnectionStatus(request)
45
+ print(f"Status: {response.status}")
46
+
47
+ except grpc.RpcError as e:
48
+ print(f"RPC Failed: {e.code()} - {e.details()}")
49
+
50
+ def run_analysis(self, inputs: Dict, settings: Dict = { }):
51
+ """
52
+ This method is responsible for testing your analysis loop. Simply provide whatever values your analyzer needs to test your logic.
53
+
54
+ Args:
55
+ inputs (Dict): The inputs required by your analyzer. Ensure the names (keys) match exactly as your analyzer expects, otherwise analysis will fail
56
+ settings (Dict): A dictionary that contains any specific settings you want to pass to your analyzer. Defaults to an empty dictionary.
57
+ """
58
+ print(f"\n--- Testing Analysis ---")
59
+ if settings is None:
60
+ settings = {}
61
+
62
+ try:
63
+ input_struct = ares_struct_utils.create_empty_struct()
64
+ ares_struct_utils.dict_to_ares_struct(inputs, input_struct)
65
+
66
+ settings_struct = ares_struct_utils.create_empty_struct()
67
+ ares_struct_utils.dict_to_ares_struct(settings, input_struct)
68
+
69
+ request = analyzer_service.AnalysisRequest(inputs=input_struct, settings=settings_struct)
70
+
71
+ start_time = time.perf_counter()
72
+ response: analysis_pb2.Analysis = self.stub.Analyze(request)
73
+ end_time = time.perf_counter()
74
+
75
+ if response.analysis_outcome == ares_outcome_enum_pb2.SUCCESS:
76
+ print(f"Analysis Successful!")
77
+ print(f"Result: {response.result}")
78
+
79
+ elif response.analysis_outcome == ares_outcome_enum_pb2.WARNING:
80
+ print("Analysis was successful, but analyzer emitted a warning.")
81
+ print(f"Result: {response.result}")
82
+ print(f"#### WARNING: {response.error_string} #####")
83
+
84
+ else:
85
+ print(f"Analysis Failed (Outcome: {response.analysis_outcome})")
86
+ print(f"Error: {response.error_string}")
87
+
88
+ elapsed_time = end_time - start_time
89
+ print(f"Analysis request was completed in: {elapsed_time:.4f} seconds")
90
+
91
+ except grpc.RpcError as e:
92
+ print(f"RPC Failed: {e.code()} - {e.details()}")
@@ -0,0 +1,86 @@
1
+ import grpc
2
+ import time
3
+ from ..Utils import plan_request_utils, plan_response_utils
4
+ from ..Planning import PlanRequest, PlanResponse
5
+ from ..Models import Outcome
6
+ from ares_datamodel.planning.remote import ares_remote_planner_service_pb2_grpc as planner_service_grpc
7
+ from ares_datamodel import ares_planner_management_service_pb2
8
+ from ares_datamodel.connection import connection_status_pb2
9
+ from ares_datamodel.planning import plan_pb2
10
+ from ares_datamodel import ares_outcome_enum_pb2
11
+ from ares_datamodel.connection import connection_info_pb2
12
+
13
+ class PlannerTestClient:
14
+ def __init__(self, port=7082, host='localhost'):
15
+ """
16
+ Initializes an PlannerTestClient, useful for testing your PyAres Planners.
17
+
18
+ Args:
19
+ port (int): The port that your planner test client will use to try and reach your service. Defaults to 7083.
20
+ host (str): The host name that the client will use to try and reach your service. Defaults to localhost.
21
+ """
22
+ self.target = f"{host}:{port}"
23
+ self.channel = grpc.insecure_channel(self.target)
24
+ self.stub = planner_service_grpc.AresRemotePlannerServiceStub(self.channel)
25
+ print(f"Client connected to {self.target}")
26
+
27
+ def get_info(self):
28
+ """Used to test that your Planner Service is providing info properly. If successful outputs the name, version and description of your service."""
29
+ print("\n--- Testing GetInfo ---")
30
+ try:
31
+ request = ares_planner_management_service_pb2.PlannerInfoRequest()
32
+ response: connection_info_pb2.InfoResponse = self.stub.GetInfo(request)
33
+ print(f"Success! Connected to: {response.name} (version {response.version})")
34
+ print(f"Description: {response.description}")
35
+
36
+ except grpc.RpcError as e:
37
+ print(f"RPC Failed: {e.code()} - {e.details()}")
38
+
39
+ def check_status(self):
40
+ """Checks the status of your Planner Service"""
41
+ print("\n--- Testing Connection Status ---")
42
+ try:
43
+ request = connection_status_pb2.ConnectionStatusRequest()
44
+ response: connection_status_pb2.ConnectionStatus = self.stub.GetConnectionStatus(request)
45
+ print(f"Status: {response.status}")
46
+
47
+ except grpc.RpcError as e:
48
+ print(f"RPC Failed: {e.code()} - {e.details()}")
49
+
50
+ def run_planning(self, request: PlanRequest) -> PlanResponse:
51
+ """
52
+ This method is responsible for testing your planning loop. Simply provide whatever values your planner needs to test your logic.
53
+
54
+ Args:
55
+ request (PlanRequest): The self crafted plan request. This should include all relevant data for your planner.
56
+ """
57
+ print("\n--- Testing Planning Logic ---")
58
+
59
+ try:
60
+ proto_request = plan_request_utils.python_plan_request_to_proto(request)
61
+
62
+ start_time = time.perf_counter()
63
+ response: plan_pb2.PlanningResponse = self.stub.Plan(proto_request)
64
+ end_time = time.perf_counter()
65
+
66
+ if response.planning_outcome == ares_outcome_enum_pb2.SUCCESS:
67
+ print(f"Planning Successful!")
68
+
69
+ elif response.planning_outcome == ares_outcome_enum_pb2.WARNING:
70
+ print("Planning was successful, but planner emitted a warning.")
71
+ print(f"#### WARNING: {response.error_string} #####")
72
+
73
+ else:
74
+ print(f"Planning Failed (Outcome: {response.planning_outcome})")
75
+ print(f"Error: {response.error_string}")
76
+
77
+ elapsed_time = end_time - start_time
78
+ print(f"Planning request was completed in: {elapsed_time:.4f} seconds")
79
+
80
+ return plan_response_utils.proto_plan_response_to_python(response)
81
+
82
+ except grpc.RpcError as e:
83
+ print(f"RPC Failed: {e.code()} - {e.details()}")
84
+ return PlanResponse([], [], error_string="GRPC FAILURE", planning_outcome=Outcome.FAILURE)
85
+
86
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAres
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: The official Python library for ARES.
5
5
  Author-email: AFRL <AFRL.RX.AREScontact@us.af.mil>
6
6
  License-Expression: MIT
@@ -9,5 +9,5 @@ Requires-Python: >=3.10
9
9
  License-File: LICENSE
10
10
  Requires-Dist: grpcio-tools>=1.75.1
11
11
  Requires-Dist: grpcio<2.0.0,>=1.71.0
12
- Requires-Dist: ares-datamodel>=0.13.2b1
12
+ Requires-Dist: ares-datamodel>=0.15.1b1
13
13
  Dynamic: license-file
@@ -12,6 +12,15 @@ PyAres.egg-info/top_level.txt
12
12
  PyAres/Analyzing/__init__.py
13
13
  PyAres/Analyzing/analysis_service.py
14
14
  PyAres/Analyzing/analyzer_models.py
15
+ PyAres/Demo/analyzer_test.py
16
+ PyAres/Demo/analyzer_test_tools_demo.py
17
+ PyAres/Demo/analyzer_wiki.py
18
+ PyAres/Demo/device_test.py
19
+ PyAres/Demo/hotplate.py
20
+ PyAres/Demo/planner_test.py
21
+ PyAres/Demo/planner_test_tools_demo.py
22
+ PyAres/Demo/planner_wiki.py
23
+ PyAres/Demo/rotary_mixer.py
15
24
  PyAres/Device/__init__.py
16
25
  PyAres/Device/device_models.py
17
26
  PyAres/Device/device_service.py
@@ -21,12 +30,16 @@ PyAres/Models/ares_data_models.py
21
30
  PyAres/Planning/__init__.py
22
31
  PyAres/Planning/planner_models.py
23
32
  PyAres/Planning/planning_service.py
24
- PyAres/Test/analyzer_test.py
25
- PyAres/Test/device_test.py
26
- PyAres/Test/planner_test.py
27
33
  PyAres/Utils/ares_data_schema_utils.py
28
34
  PyAres/Utils/ares_data_type_utils.py
29
35
  PyAres/Utils/ares_device_command_utils.py
30
36
  PyAres/Utils/ares_outcome_utils.py
31
37
  PyAres/Utils/ares_struct_utils.py
32
- PyAres/Utils/ares_value_utils.py
38
+ PyAres/Utils/ares_value_utils.py
39
+ PyAres/Utils/param_history_info_utils.py
40
+ PyAres/Utils/plan_request_utils.py
41
+ PyAres/Utils/plan_response_utils.py
42
+ PyAres/Utils/planning_param_utils.py
43
+ PyAres/test_tools/__init__.py
44
+ PyAres/test_tools/analyzer_tester.py
45
+ PyAres/test_tools/planner_tester.py
@@ -1,3 +1,3 @@
1
1
  grpcio-tools>=1.75.1
2
2
  grpcio<2.0.0,>=1.71.0
3
- ares-datamodel>=0.13.2b1
3
+ ares-datamodel>=0.15.1b1
@@ -145,4 +145,8 @@ The central component to your PyAres device is your AresDeviceService. This clas
145
145
 
146
146
  ### 📄 License
147
147
 
148
- The PyAres project is licensed under the MIT License - see details in [LICENSE.txt]([https://github.com/AFRL-ARES/PyAres/blob/Develop/LICENSE.txt](https://github.com/AFRL-ARES/PyAres/blob/Develop/LICENSE))
148
+ The PyAres project is licensed under the MIT License - see details in [LICENSE.txt]([https://github.com/AFRL-ARES/PyAres/blob/Develop/LICENSE.txt](https://github.com/AFRL-ARES/PyAres/blob/Develop/LICENSE)) <br></br>
149
+
150
+ ### CLEARANCE
151
+ Distribution A. Approved for public release: distribution unlimited. AFRL-2025-5332.
152
+
@@ -2,9 +2,6 @@
2
2
  requires = [
3
3
  "setuptools>=61.0.0",
4
4
  "wheel",
5
- "grpcio",
6
- "grpcio-tools>=1.74.0",
7
- "ares-datamodel",
8
5
  "googleapis-common-protos>=1.70.0"
9
6
  ]
10
7
  build-backend = "setuptools.build_meta"
@@ -14,7 +11,7 @@ include = ["PyAres*"]
14
11
 
15
12
  [project]
16
13
  name = "PyAres"
17
- version = "0.1.4"
14
+ version = "0.2.0"
18
15
  authors = [{ name = "AFRL", email = "AFRL.RX.AREScontact@us.af.mil" }]
19
16
  description = "The official Python library for ARES."
20
17
  license = "MIT"
@@ -23,7 +20,7 @@ requires-python = ">= 3.10"
23
20
  dependencies = [
24
21
  "grpcio-tools (>=1.75.1)",
25
22
  "grpcio (>=1.71.0,<2.0.0)",
26
- "ares-datamodel (>=0.13.2b1)",
23
+ "ares-datamodel (>=0.15.1b1)",
27
24
  ]
28
25
 
29
26
  [project.urls]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes