ivoryos 1.2.1__py3-none-any.whl → 1.2.2__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.
Potentially problematic release.
This version of ivoryos might be problematic. Click here for more details.
- ivoryos/optimizer/ax_optimizer.py +164 -0
- ivoryos/optimizer/base_optimizer.py +65 -0
- ivoryos/optimizer/baybe_optimizer.py +183 -0
- ivoryos/optimizer/registry.py +9 -0
- ivoryos/routes/api/api.py +56 -0
- ivoryos/utils/serilize.py +4 -6
- ivoryos/version.py +1 -1
- {ivoryos-1.2.1.dist-info → ivoryos-1.2.2.dist-info}/METADATA +9 -5
- {ivoryos-1.2.1.dist-info → ivoryos-1.2.2.dist-info}/RECORD +12 -16
- {ivoryos-1.2.1.dist-info → ivoryos-1.2.2.dist-info}/WHEEL +1 -1
- {ivoryos-1.2.1.dist-info → ivoryos-1.2.2.dist-info}/top_level.txt +0 -1
- tests/__init__.py +0 -0
- tests/conftest.py +0 -133
- tests/integration/__init__.py +0 -0
- tests/integration/test_route_auth.py +0 -80
- tests/integration/test_route_control.py +0 -94
- tests/integration/test_route_database.py +0 -61
- tests/integration/test_route_design.py +0 -36
- tests/integration/test_route_main.py +0 -35
- tests/integration/test_sockets.py +0 -26
- {ivoryos-1.2.1.dist-info → ivoryos-1.2.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# optimizers/ax_optimizer.py
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from ivoryos.optimizer.base_optimizer import OptimizerBase
|
|
7
|
+
from ivoryos.utils.utils import install_and_import
|
|
8
|
+
|
|
9
|
+
class AxOptimizer(OptimizerBase):
|
|
10
|
+
def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config=None):
|
|
11
|
+
try:
|
|
12
|
+
from ax.api.client import Client
|
|
13
|
+
except ImportError as e:
|
|
14
|
+
install_and_import("ax", "ax-platform")
|
|
15
|
+
raise ImportError("Please install Ax with pip install ax-platform to use AxOptimizer. Attempting to install Ax...")
|
|
16
|
+
super().__init__(experiment_name, parameter_space, objective_config, optimizer_config)
|
|
17
|
+
|
|
18
|
+
self.client = Client()
|
|
19
|
+
# 2. Configure where Ax will search.
|
|
20
|
+
self.client.configure_experiment(
|
|
21
|
+
name=experiment_name,
|
|
22
|
+
parameters=self._convert_parameter_to_ax_format(parameter_space)
|
|
23
|
+
)
|
|
24
|
+
# 3. Configure the objective function.
|
|
25
|
+
self.client.configure_optimization(objective=self._convert_objective_to_ax_format(objective_config))
|
|
26
|
+
if optimizer_config:
|
|
27
|
+
self.client.set_generation_strategy(self._convert_generator_to_ax_format(optimizer_config))
|
|
28
|
+
self.generators = self._create_generator_mapping()
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def _create_generator_mapping():
|
|
32
|
+
"""Create a mapping from string values to Generator enum members."""
|
|
33
|
+
from ax.modelbridge import Generators
|
|
34
|
+
return {member.value: member for member in Generators}
|
|
35
|
+
|
|
36
|
+
def _convert_parameter_to_ax_format(self, parameter_space):
|
|
37
|
+
"""
|
|
38
|
+
Converts the parameter space configuration to Baybe format.
|
|
39
|
+
:param parameter_space: The parameter space configuration.
|
|
40
|
+
[
|
|
41
|
+
{"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
|
|
42
|
+
{"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
|
|
43
|
+
{"name": "param_3", "type": "range", "bounds": [0 10], "value_type": "int"},
|
|
44
|
+
]
|
|
45
|
+
:return: A list of Baybe parameters.
|
|
46
|
+
"""
|
|
47
|
+
from ax import RangeParameterConfig, ChoiceParameterConfig
|
|
48
|
+
ax_params = []
|
|
49
|
+
for p in parameter_space:
|
|
50
|
+
if p["type"] == "range":
|
|
51
|
+
ax_params.append(
|
|
52
|
+
RangeParameterConfig(
|
|
53
|
+
name=p["name"],
|
|
54
|
+
bounds=tuple(p["bounds"]),
|
|
55
|
+
parameter_type=p["value_type"]
|
|
56
|
+
))
|
|
57
|
+
elif p["type"] == "choice":
|
|
58
|
+
ax_params.append(
|
|
59
|
+
ChoiceParameterConfig(
|
|
60
|
+
name=p["name"],
|
|
61
|
+
values=p["bounds"],
|
|
62
|
+
parameter_type=p["value_type"],
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
return ax_params
|
|
66
|
+
|
|
67
|
+
def _convert_objective_to_ax_format(self, objective_config: list):
|
|
68
|
+
"""
|
|
69
|
+
Converts the objective configuration to Baybe format.
|
|
70
|
+
:param parameter_space: The parameter space configuration.
|
|
71
|
+
[
|
|
72
|
+
{"name": "obj_1", "minimize": True, "weight": 1},
|
|
73
|
+
{"name": "obj_2", "minimize": False, "weight": 2}
|
|
74
|
+
]
|
|
75
|
+
:return: Ax objective configuration. "-cost, utility"
|
|
76
|
+
"""
|
|
77
|
+
objectives = []
|
|
78
|
+
for obj in objective_config:
|
|
79
|
+
obj_name = obj.get("name")
|
|
80
|
+
minimize = obj.get("minimize", True)
|
|
81
|
+
weight = obj.get("weight", 1)
|
|
82
|
+
sign = "-" if minimize else ""
|
|
83
|
+
objectives.append(f"{sign}{weight} * {obj_name}")
|
|
84
|
+
return ", ".join(objectives)
|
|
85
|
+
|
|
86
|
+
def _convert_generator_to_ax_format(self, optimizer_config):
|
|
87
|
+
"""
|
|
88
|
+
Converts the optimizer configuration to Ax format.
|
|
89
|
+
:param optimizer_config: The optimizer configuration.
|
|
90
|
+
:return: Ax generator configuration.
|
|
91
|
+
"""
|
|
92
|
+
from ax.generation_strategy.generation_node import GenerationStep
|
|
93
|
+
from ax.generation_strategy.generation_strategy import GenerationStrategy
|
|
94
|
+
generators = self._create_generator_mapping()
|
|
95
|
+
step_1 = optimizer_config.get("step_1", {})
|
|
96
|
+
step_2 = optimizer_config.get("step_2", {})
|
|
97
|
+
step_1_generator = step_1.get("model", "Sobol")
|
|
98
|
+
step_2_generator = step_2.get("model", "BOTorch")
|
|
99
|
+
generator_1 = GenerationStep(model=generators.get(step_1_generator), num_trials=step_1.get("num_samples", 5))
|
|
100
|
+
generator_2 = GenerationStep(model=generators.get(step_2_generator), num_trials=step_2.get("num_samples", -1))
|
|
101
|
+
return GenerationStrategy(steps=[generator_1, generator_2])
|
|
102
|
+
|
|
103
|
+
def suggest(self, n=1):
|
|
104
|
+
trial_index, params = self.client.get_next_trials(1).popitem()
|
|
105
|
+
self.trial_index = trial_index
|
|
106
|
+
return params
|
|
107
|
+
|
|
108
|
+
def observe(self, results):
|
|
109
|
+
self.client.complete_trial(
|
|
110
|
+
trial_index=self.trial_index,
|
|
111
|
+
raw_data=results
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def get_schema():
|
|
116
|
+
return {
|
|
117
|
+
"parameter_types": ["range", "choice"],
|
|
118
|
+
"multiple_objectives": True,
|
|
119
|
+
# "objective_weights": True,
|
|
120
|
+
"optimizer_config": {
|
|
121
|
+
"step_1": {"model": ["Sobol", "Uniform", "Factorial", "Thompson"], "num_samples": 5},
|
|
122
|
+
"step_2": {"model": ["BoTorch", "SAASBO", "SAAS_MTGP", "Legacy_GPEI", "EB", "EB_Ashr", "ST_MTGP", "BO_MIXED", "Contextual_SACBO"]}
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def append_existing_data(self, existing_data):
|
|
127
|
+
"""
|
|
128
|
+
Append existing data to the Ax experiment.
|
|
129
|
+
:param existing_data: A dictionary containing existing data.
|
|
130
|
+
"""
|
|
131
|
+
from pandas import DataFrame
|
|
132
|
+
if not existing_data:
|
|
133
|
+
return
|
|
134
|
+
if isinstance(existing_data, DataFrame):
|
|
135
|
+
existing_data = existing_data.to_dict(orient="records")
|
|
136
|
+
parameter_names = [i.get("name") for i in self.parameter_space]
|
|
137
|
+
objective_names = [i.get("name") for i in self.objective_config]
|
|
138
|
+
for name, value in existing_data.items():
|
|
139
|
+
# First attach the trial and note the trial index
|
|
140
|
+
parameters = {name: value for name in existing_data if name in parameter_names}
|
|
141
|
+
trial_index = self.client.attach_trial(parameters=parameters)
|
|
142
|
+
raw_data = {name: value for name in existing_data if name in objective_names}
|
|
143
|
+
# Then complete the trial with the existing data
|
|
144
|
+
self.client.complete_trial(trial_index=trial_index, raw_data=raw_data)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
# Example usage
|
|
149
|
+
optimizer = AxOptimizer(
|
|
150
|
+
experiment_name="example_experiment",
|
|
151
|
+
parameter_space=[
|
|
152
|
+
{"name": "param_1", "type": "range", "bounds": [0.0, 1.0], "value_type": "float"},
|
|
153
|
+
{"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"}
|
|
154
|
+
],
|
|
155
|
+
objective_config=[
|
|
156
|
+
{"name": "objective_1", "minimize": True},
|
|
157
|
+
{"name": "objective_2", "minimize": False}
|
|
158
|
+
],
|
|
159
|
+
optimizer_config={
|
|
160
|
+
"step_1": {"model": "Sobol", "num_samples": 5},
|
|
161
|
+
"step_2": {"model": "BoTorch"}
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
print(optimizer._create_generator_mapping())
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
### ivoryos/optimizers/base.py
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OptimizerBase(ABC):
|
|
8
|
+
def __init__(self, experiment_name:str, parameter_space: list, objective_config: dict, optimizer_config: dict):
|
|
9
|
+
"""
|
|
10
|
+
:param experiment_name: arbitrary name
|
|
11
|
+
:param parameter_space: list of parameter names
|
|
12
|
+
[
|
|
13
|
+
{"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
|
|
14
|
+
{"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
|
|
15
|
+
{"name": "param_3", "type": "range", "bounds": [0 10], "value_type": "int"},
|
|
16
|
+
]
|
|
17
|
+
:param objective_config: objective configuration
|
|
18
|
+
[
|
|
19
|
+
{"name": "obj_1", "minimize": True, "weight": 1},
|
|
20
|
+
{"name": "obj_2", "minimize": False, "weight": 1}
|
|
21
|
+
]
|
|
22
|
+
:param optimizer_config: optimizer configuration
|
|
23
|
+
optimizer_config={
|
|
24
|
+
"step_1": {"model": "Random", "num_samples": 10},
|
|
25
|
+
"step_2": {"model": "BOTorch"}
|
|
26
|
+
}
|
|
27
|
+
"""
|
|
28
|
+
self.experiment_name = experiment_name
|
|
29
|
+
self.parameter_space = parameter_space
|
|
30
|
+
self.objective_config = objective_config
|
|
31
|
+
self.optimizer_config = optimizer_config
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def suggest(self, n=1):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def observe(self, results: dict):
|
|
39
|
+
"""
|
|
40
|
+
observe
|
|
41
|
+
:param results: {"objective_name": "value"}
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def append_existing_data(self, existing_data):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def get_schema():
|
|
51
|
+
"""
|
|
52
|
+
Returns a template for the optimizer configuration.
|
|
53
|
+
"""
|
|
54
|
+
return {
|
|
55
|
+
"parameter_types": ["range", "choice"],
|
|
56
|
+
"multiple_objectives": True,
|
|
57
|
+
# "objective_weights": True,
|
|
58
|
+
"optimizer_config": {
|
|
59
|
+
"step_1": {"model": [], "num_samples": 10},
|
|
60
|
+
"step_2": {"model": []}
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
### Directory: ivoryos/optimizers/baybe_optimizer.py
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from ivoryos.utils.utils import install_and_import
|
|
6
|
+
from ivoryos.optimizer.base_optimizer import OptimizerBase
|
|
7
|
+
|
|
8
|
+
class BaybeOptimizer(OptimizerBase):
|
|
9
|
+
def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config):
|
|
10
|
+
try:
|
|
11
|
+
from baybe import Campaign
|
|
12
|
+
except ImportError:
|
|
13
|
+
install_and_import("baybe")
|
|
14
|
+
print("Please install Baybe with pip install baybe to before register BaybeOptimizer.")
|
|
15
|
+
|
|
16
|
+
super().__init__(experiment_name, parameter_space, objective_config, optimizer_config)
|
|
17
|
+
self._trial_id = 0
|
|
18
|
+
self._trials = {}
|
|
19
|
+
|
|
20
|
+
self.experiment = Campaign(
|
|
21
|
+
searchspace=self._convert_parameter_to_searchspace(parameter_space),
|
|
22
|
+
objective=self._convert_objective_to_baybe_format(objective_config),
|
|
23
|
+
recommender=self._convert_recommender_to_baybe_format(optimizer_config),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def suggest(self, n=1):
|
|
28
|
+
self.df = self.experiment.recommend(batch_size=n)
|
|
29
|
+
return self.df.to_dict(orient="records")[0]
|
|
30
|
+
|
|
31
|
+
def observe(self, results, index=None):
|
|
32
|
+
"""
|
|
33
|
+
Observes the results of a trial and updates the experiment.
|
|
34
|
+
:param results: A dictionary containing the results of the trial.
|
|
35
|
+
:param index: The index of the trial in the DataFrame, if applicable.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
for name, value in results.items():
|
|
39
|
+
self.df[name] = [value]
|
|
40
|
+
self.experiment.add_measurements(self.df)
|
|
41
|
+
|
|
42
|
+
def append_existing_data(self, existing_data: Dict):
|
|
43
|
+
"""
|
|
44
|
+
Append existing data to the Ax experiment.
|
|
45
|
+
:param existing_data: A dictionary containing existing data.
|
|
46
|
+
"""
|
|
47
|
+
import pandas as pd
|
|
48
|
+
if not existing_data:
|
|
49
|
+
return
|
|
50
|
+
# parameter_names = [i.get("name") for i in self.parameter_space]
|
|
51
|
+
# objective_names = [i.get("name") for i in self.objective_config]
|
|
52
|
+
self.experiment.add_measurements(pd.DataFrame(existing_data))
|
|
53
|
+
# for name, value in existing_data.items():
|
|
54
|
+
# # First attach the trial and note the trial index
|
|
55
|
+
# parameters = {name: value for name in existing_data if name in parameter_names}
|
|
56
|
+
# trial_index = self.client.attach_trial(parameters=parameters)
|
|
57
|
+
# raw_data = {name: value for name in existing_data if name in objective_names}
|
|
58
|
+
# # Then complete the trial with the existing data
|
|
59
|
+
# self.client.complete_trial(trial_index=trial_index, raw_data=raw_data)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _convert_parameter_to_searchspace(self, parameter_space):
|
|
63
|
+
"""
|
|
64
|
+
Converts the parameter space configuration to Baybe format.
|
|
65
|
+
:param parameter_space: The parameter space configuration.
|
|
66
|
+
[
|
|
67
|
+
{"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
|
|
68
|
+
{"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
|
|
69
|
+
{"name": "param_3", "type": "range", "bounds": [0 10], "value_type": "int"},
|
|
70
|
+
{"name": "param_4", "type": "substance", "bounds": ["methanol", "water", "toluene"], "value_type": "str"} #TODO
|
|
71
|
+
]
|
|
72
|
+
:return: A list of Baybe parameters.
|
|
73
|
+
"""
|
|
74
|
+
from baybe.parameters.categorical import CategoricalParameter
|
|
75
|
+
from baybe.parameters.numerical import NumericalContinuousParameter, NumericalDiscreteParameter
|
|
76
|
+
from baybe.searchspace import SearchSpace
|
|
77
|
+
parameters = []
|
|
78
|
+
for p in parameter_space:
|
|
79
|
+
if p["type"] == "range":
|
|
80
|
+
if p["value_type"] == "float":
|
|
81
|
+
parameters.append(NumericalContinuousParameter(name=p["name"], bounds=p["bounds"]))
|
|
82
|
+
elif p["value_type"] == "int":
|
|
83
|
+
values = tuple([int(v) for v in range(p["bounds"][0], p["bounds"][1] + 1)])
|
|
84
|
+
parameters.append(NumericalDiscreteParameter(name=p["name"], values=values))
|
|
85
|
+
|
|
86
|
+
elif p["type"] == "choice":
|
|
87
|
+
if p["value_type"] == "str":
|
|
88
|
+
parameters.append(CategoricalParameter(name=p["name"], values=p["bounds"]))
|
|
89
|
+
elif p["value_type"] in ["int", "float"]:
|
|
90
|
+
parameters.append(NumericalDiscreteParameter(name=p["name"], values=p["bounds"]))
|
|
91
|
+
return SearchSpace.from_product(parameters)
|
|
92
|
+
|
|
93
|
+
def _convert_objective_to_baybe_format(self, objective_config):
|
|
94
|
+
"""
|
|
95
|
+
Converts the objective configuration to Baybe format.
|
|
96
|
+
:param parameter_space: The parameter space configuration.
|
|
97
|
+
[
|
|
98
|
+
{"name": "obj_1", "minimize": True},
|
|
99
|
+
{"name": "obj_2", "minimize": False}
|
|
100
|
+
]
|
|
101
|
+
:return: A Baybe objective configuration.
|
|
102
|
+
"""
|
|
103
|
+
from baybe.targets import NumericalTarget
|
|
104
|
+
from baybe.objectives import SingleTargetObjective, DesirabilityObjective, ParetoObjective
|
|
105
|
+
targets = []
|
|
106
|
+
weights = []
|
|
107
|
+
for obj in objective_config:
|
|
108
|
+
obj_name = obj.get("name")
|
|
109
|
+
minimize = obj.get("minimize", False)
|
|
110
|
+
weight = obj.get("weight", 1)
|
|
111
|
+
weights.append(weight)
|
|
112
|
+
targets.append(NumericalTarget(name=obj_name, mode="MAX" if minimize else "MIN"))
|
|
113
|
+
|
|
114
|
+
if len(targets) == 1:
|
|
115
|
+
return SingleTargetObjective(target=targets[0])
|
|
116
|
+
else:
|
|
117
|
+
# Handle multiple objectives
|
|
118
|
+
return ParetoObjective(targets=targets)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _convert_recommender_to_baybe_format(self, recommender_config):
|
|
122
|
+
"""
|
|
123
|
+
Converts the recommender configuration to Baybe format.
|
|
124
|
+
:param recommender_config: The recommender configuration.
|
|
125
|
+
:return: A Baybe recommender configuration.
|
|
126
|
+
"""
|
|
127
|
+
from baybe.recommenders import (
|
|
128
|
+
BotorchRecommender,
|
|
129
|
+
FPSRecommender,
|
|
130
|
+
TwoPhaseMetaRecommender,
|
|
131
|
+
RandomRecommender,
|
|
132
|
+
NaiveHybridSpaceRecommender
|
|
133
|
+
)
|
|
134
|
+
step_1 = recommender_config.get("step_1", {})
|
|
135
|
+
step_2 = recommender_config.get("step_2", {})
|
|
136
|
+
step_1_recommender = step_1.get("model", "Random")
|
|
137
|
+
step_2_recommender = step_2.get("model", "BOTorch")
|
|
138
|
+
if step_1.get("model") == "Random":
|
|
139
|
+
step_1_recommender = RandomRecommender()
|
|
140
|
+
elif step_1.get("model") == "FPS":
|
|
141
|
+
step_1_recommender = FPSRecommender()
|
|
142
|
+
if step_2.get("model") == "Naive Hybrid Space":
|
|
143
|
+
step_2_recommender = NaiveHybridSpaceRecommender()
|
|
144
|
+
elif step_2.get("model") == "BOTorch":
|
|
145
|
+
step_2_recommender = BotorchRecommender()
|
|
146
|
+
return TwoPhaseMetaRecommender(
|
|
147
|
+
initial_recommender=step_1_recommender,
|
|
148
|
+
recommender=step_2_recommender
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def get_schema():
|
|
153
|
+
"""
|
|
154
|
+
Returns a template for the optimizer configuration.
|
|
155
|
+
"""
|
|
156
|
+
return {
|
|
157
|
+
"parameter_types": ["range", "choice", "substance"],
|
|
158
|
+
"multiple_objectives": True,
|
|
159
|
+
"optimizer_config": {
|
|
160
|
+
"step_1": {"model": ["Random", "FPS"], "num_samples": 10},
|
|
161
|
+
"step_2": {"model": ["BOTorch", "Naive Hybrid Space"]}
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
# Example usage
|
|
167
|
+
baybe_optimizer = BaybeOptimizer(
|
|
168
|
+
experiment_name="example_experiment",
|
|
169
|
+
parameter_space=[
|
|
170
|
+
{"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
|
|
171
|
+
{"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
|
|
172
|
+
{"name": "param_3", "type": "range", "bounds": [0, 10], "value_type": "int"}
|
|
173
|
+
],
|
|
174
|
+
objective_config=[
|
|
175
|
+
{"name": "obj_1", "minimize": True},
|
|
176
|
+
{"name": "obj_2", "minimize": False}
|
|
177
|
+
],
|
|
178
|
+
optimizer_config={
|
|
179
|
+
"step_1": {"model": "Random", "num_samples": 10},
|
|
180
|
+
"step_2": {"model": "BOTorch"}
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
print(baybe_optimizer.suggest(5))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from flask import Blueprint, jsonify, request, current_app
|
|
3
|
+
|
|
4
|
+
from ivoryos.routes.control.control import find_instrument_by_name
|
|
5
|
+
from ivoryos.utils.form import create_form_from_module
|
|
6
|
+
from ivoryos.utils.global_config import GlobalConfig
|
|
7
|
+
from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
|
|
8
|
+
|
|
9
|
+
from ivoryos.socket_handlers import abort_pending, abort_current, pause, retry, runner
|
|
10
|
+
from ivoryos.utils.task_runner import TaskRunner
|
|
11
|
+
|
|
12
|
+
api = Blueprint('api', __name__)
|
|
13
|
+
global_config = GlobalConfig()
|
|
14
|
+
task_runner = TaskRunner()
|
|
15
|
+
|
|
16
|
+
#TODO: add authentication and authorization to the API endpoints
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@api.route("/control/", strict_slashes=False, methods=['GET'])
|
|
20
|
+
@api.route("/control/<string:instrument>", methods=['POST'])
|
|
21
|
+
def backend_control(instrument: str=None):
|
|
22
|
+
"""
|
|
23
|
+
.. :quickref: Backend Control; backend control
|
|
24
|
+
|
|
25
|
+
backend control through http requests
|
|
26
|
+
|
|
27
|
+
.. http:get:: /api/control/
|
|
28
|
+
|
|
29
|
+
:param instrument: instrument name
|
|
30
|
+
:type instrument: str
|
|
31
|
+
|
|
32
|
+
.. http:post:: /api/control/
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
if instrument:
|
|
36
|
+
inst_object = find_instrument_by_name(instrument)
|
|
37
|
+
forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
|
|
38
|
+
|
|
39
|
+
if request.method == 'POST':
|
|
40
|
+
method_name = request.form.get("hidden_name", None)
|
|
41
|
+
form = forms.get(method_name, None)
|
|
42
|
+
if form:
|
|
43
|
+
kwargs = {field.name: field.data for field in form if field.name not in ['csrf_token', 'hidden_name']}
|
|
44
|
+
wait = request.form.get("hidden_wait", "true") == "true"
|
|
45
|
+
output = task_runner.run_single_step(component=instrument, method=method_name, kwargs=kwargs, wait=wait,
|
|
46
|
+
current_app=current_app._get_current_object())
|
|
47
|
+
return jsonify(output), 200
|
|
48
|
+
|
|
49
|
+
snapshot = global_config.deck_snapshot.copy()
|
|
50
|
+
# Iterate through each instrument in the snapshot
|
|
51
|
+
for instrument_key, instrument_data in snapshot.items():
|
|
52
|
+
# Iterate through each function associated with the current instrument
|
|
53
|
+
for function_key, function_data in instrument_data.items():
|
|
54
|
+
# Convert the function signature to a string representation
|
|
55
|
+
function_data['signature'] = str(function_data['signature'])
|
|
56
|
+
return jsonify(snapshot), 200
|
ivoryos/utils/serilize.py
CHANGED
|
@@ -7,8 +7,8 @@ import sys
|
|
|
7
7
|
|
|
8
8
|
import flask
|
|
9
9
|
|
|
10
|
-
from example.abstract_sdl_example
|
|
11
|
-
|
|
10
|
+
from example.abstract_sdl_example import abstract_sdl as deck
|
|
11
|
+
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class ScriptAnalyzer:
|
|
@@ -179,14 +179,12 @@ class ScriptAnalyzer:
|
|
|
179
179
|
print(f" {name}: {error}")
|
|
180
180
|
|
|
181
181
|
if __name__ == "__main__":
|
|
182
|
-
balance = AbstractBalance("re")
|
|
183
|
-
pump = AbstractPump("re")
|
|
184
182
|
|
|
185
183
|
_analyzer = ScriptAnalyzer()
|
|
186
|
-
module = sys.modules[
|
|
184
|
+
# module = sys.modules[deck]
|
|
187
185
|
try:
|
|
188
186
|
|
|
189
|
-
result, with_warnings, failed, _ = _analyzer.analyze_module(
|
|
187
|
+
result, with_warnings, failed, _ = _analyzer.analyze_module(deck)
|
|
190
188
|
|
|
191
189
|
output_path = f"analysis.json"
|
|
192
190
|
_analyzer.save_to_json(result, output_path)
|
ivoryos/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.2.
|
|
1
|
+
__version__ = "1.2.2"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ivoryos
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
|
|
5
|
-
|
|
6
|
-
Author: Ivory Zhang
|
|
7
|
-
Author-email: ivoryzhang@chem.ubc.ca
|
|
5
|
+
Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
|
|
8
6
|
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/heingroup/ivoryos
|
|
8
|
+
Requires-Python: >=3.7
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: bcrypt
|
|
@@ -17,6 +17,10 @@ Requires-Dist: Flask-WTF
|
|
|
17
17
|
Requires-Dist: SQLAlchemy-Utils
|
|
18
18
|
Requires-Dist: python-dotenv
|
|
19
19
|
Requires-Dist: astor; python_version < "3.9"
|
|
20
|
+
Provides-Extra: optimizer
|
|
21
|
+
Requires-Dist: ax-platform; extra == "optimizer"
|
|
22
|
+
Requires-Dist: baybe; extra == "optimizer"
|
|
23
|
+
Dynamic: license-file
|
|
20
24
|
|
|
21
25
|
[](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
|
|
22
26
|
[](https://pypi.org/project/ivoryos/)
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
ivoryos/__init__.py,sha256=x4PnDTbhx1ZTugfuclGQ03GO3yidAfiUFU7vuGyKD8M,9830
|
|
2
2
|
ivoryos/config.py,sha256=y3RxNjiIola9tK7jg-mHM8EzLMwiLwOzoisXkDvj0gA,2174
|
|
3
3
|
ivoryos/socket_handlers.py,sha256=VWVWiIdm4jYAutwGu6R0t1nK5MuMyOCL0xAnFn06jWQ,1302
|
|
4
|
-
ivoryos/version.py,sha256=
|
|
4
|
+
ivoryos/version.py,sha256=uuf4VNtTNA93fMhoAur9YafzaKJFnczY-H1SSCSuRVQ,22
|
|
5
|
+
ivoryos/optimizer/ax_optimizer.py,sha256=PoSu8hrDFFpqyhRBnaSMswIUsDfEX6sPWt8NEZ_sobs,7112
|
|
6
|
+
ivoryos/optimizer/base_optimizer.py,sha256=JTbUharZKn0t8_BDbAFuwZIbT1VOnX1Xuog1pJuU8hY,1992
|
|
7
|
+
ivoryos/optimizer/baybe_optimizer.py,sha256=EdrrRiYO-IOx610cPXiQhH4qG8knUP0uiZ0YoyaGIU8,7954
|
|
8
|
+
ivoryos/optimizer/registry.py,sha256=lr0cqdI2iEjw227ZPRpVkvsdYdddjeJJRzawDv77cEc,219
|
|
5
9
|
ivoryos/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
ivoryos/routes/api/api.py,sha256=X_aZMB_nCxW41pqZpJOiEEwGmlqLqJUArruevuy41v0,2284
|
|
6
11
|
ivoryos/routes/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
12
|
ivoryos/routes/auth/auth.py,sha256=CqoP9cM8BuXVGHGujX7-0sNAOdWILU9amyBrObOD6Ss,3283
|
|
8
13
|
ivoryos/routes/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -32,20 +37,11 @@ ivoryos/utils/global_config.py,sha256=zNO9GYhGn7El3msWoxJIm3S4Mzb3VMh2i5ZEsVtvb2
|
|
|
32
37
|
ivoryos/utils/llm_agent.py,sha256=-lVCkjPlpLues9sNTmaT7bT4sdhWvV2DiojNwzB2Lcw,6422
|
|
33
38
|
ivoryos/utils/py_to_json.py,sha256=fyqjaxDHPh-sahgT6IHSn34ktwf6y51_x1qvhbNlH-U,7314
|
|
34
39
|
ivoryos/utils/script_runner.py,sha256=g3_pLYcu6gF9sPjhW9WRlwMH7ScDpz_MqMJzxNayfyg,16725
|
|
35
|
-
ivoryos/utils/serilize.py,sha256=
|
|
40
|
+
ivoryos/utils/serilize.py,sha256=lkBhkz8r2bLmz2_xOb0c4ptSSOqjIu6krj5YYK4Nvj8,6784
|
|
36
41
|
ivoryos/utils/task_runner.py,sha256=cDIcmDaqYh0vXoYaL_kO877pluAo2tyfsHl9OgZqJJE,3029
|
|
37
42
|
ivoryos/utils/utils.py,sha256=-WiU0_brszB9yDsiQepf_7SzNgPTSpul2RSKDOY3pqo,13921
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
tests/integration/test_route_database.py,sha256=mS026W_hEuCTMpSkdRWvM-f4MYykK_6nRDJ4K5a7QA0,2342
|
|
44
|
-
tests/integration/test_route_design.py,sha256=PJAvGRiCY6B53Pu1v5vPAVHHsuaqRmRKk2eesSNshLU,1157
|
|
45
|
-
tests/integration/test_route_main.py,sha256=bmuf8Y_9CRWhiLLf4up11ltEd5YCdsLx6I-o26VGDEw,1228
|
|
46
|
-
tests/integration/test_sockets.py,sha256=4ZyFyExm7a-DYzVqpzEONpWeb1a0IT68wyFaQu0rY_Y,925
|
|
47
|
-
ivoryos-1.2.1.dist-info/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
|
|
48
|
-
ivoryos-1.2.1.dist-info/METADATA,sha256=AiQqfIiig_KOYF5ths0jGNMbax1YV44deM6wKwHwnAY,9247
|
|
49
|
-
ivoryos-1.2.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
50
|
-
ivoryos-1.2.1.dist-info/top_level.txt,sha256=mIOiZkdpSwxFJt1R5fsyOff8mNprXHq1nMGNKNULIyE,14
|
|
51
|
-
ivoryos-1.2.1.dist-info/RECORD,,
|
|
43
|
+
ivoryos-1.2.2.dist-info/licenses/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
|
|
44
|
+
ivoryos-1.2.2.dist-info/METADATA,sha256=vvueQx-kWo57d7AvBCDtUp28Ot_DBmgeTaZ6HVNS3ec,9416
|
|
45
|
+
ivoryos-1.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
46
|
+
ivoryos-1.2.2.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
|
|
47
|
+
ivoryos-1.2.2.dist-info/RECORD,,
|
tests/__init__.py
DELETED
|
File without changes
|
tests/conftest.py
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
|
|
3
|
-
import bcrypt
|
|
4
|
-
import pytest
|
|
5
|
-
from ivoryos.config import get_config
|
|
6
|
-
|
|
7
|
-
from ivoryos import create_app, socketio, db as _db, utils, global_config
|
|
8
|
-
from ivoryos.utils.db_models import User
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@pytest.fixture(scope='session')
|
|
12
|
-
def app():
|
|
13
|
-
"""Create a new app instance for the test session."""
|
|
14
|
-
_app = create_app(get_config('testing'))
|
|
15
|
-
return _app
|
|
16
|
-
|
|
17
|
-
@pytest.fixture
|
|
18
|
-
def client(app):
|
|
19
|
-
"""A test client for the app."""
|
|
20
|
-
with app.test_client() as client:
|
|
21
|
-
with app.app_context():
|
|
22
|
-
_db.create_all()
|
|
23
|
-
yield client
|
|
24
|
-
with app.app_context():
|
|
25
|
-
_db.drop_all()
|
|
26
|
-
|
|
27
|
-
# @pytest.fixture(scope='session')
|
|
28
|
-
# def db(app):
|
|
29
|
-
# """Session-wide test database."""
|
|
30
|
-
# with app.app_context():
|
|
31
|
-
# _db.create_all()
|
|
32
|
-
# yield _db
|
|
33
|
-
# _db.drop_all()
|
|
34
|
-
|
|
35
|
-
@pytest.fixture(scope='module')
|
|
36
|
-
def init_database(app):
|
|
37
|
-
"""
|
|
38
|
-
Creates the database tables and seeds it with a default test user.
|
|
39
|
-
This runs once per test module.
|
|
40
|
-
"""
|
|
41
|
-
with app.app_context():
|
|
42
|
-
# Drop everything first to ensure a clean slate
|
|
43
|
-
_db.drop_all()
|
|
44
|
-
# Create the database tables
|
|
45
|
-
_db.create_all()
|
|
46
|
-
|
|
47
|
-
# Insert a default user for authentication tests
|
|
48
|
-
# Note: In a real app with password hashing, you'd call a hash function here.
|
|
49
|
-
password = 'password'
|
|
50
|
-
bcrypt_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
|
51
|
-
default_user = User(username='testuser', password=bcrypt_password)
|
|
52
|
-
_db.session.add(default_user)
|
|
53
|
-
_db.session.commit()
|
|
54
|
-
|
|
55
|
-
yield _db # this is where the testing happens!
|
|
56
|
-
|
|
57
|
-
# Teardown: drop all tables after the tests in the module are done
|
|
58
|
-
_db.drop_all()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# ---------------------
|
|
62
|
-
# Authentication Fixture
|
|
63
|
-
# ---------------------
|
|
64
|
-
|
|
65
|
-
@pytest.fixture(scope='function')
|
|
66
|
-
def auth(client, init_database):
|
|
67
|
-
"""
|
|
68
|
-
Logs in the default user for a single test function.
|
|
69
|
-
Depends on `init_database` to ensure the user exists.
|
|
70
|
-
Handles logout as part of teardown.
|
|
71
|
-
"""
|
|
72
|
-
# Log in the default user
|
|
73
|
-
client.post('/ivoryos/auth/login', data={
|
|
74
|
-
'username': 'testuser',
|
|
75
|
-
'password': 'password'
|
|
76
|
-
}, follow_redirects=True)
|
|
77
|
-
|
|
78
|
-
yield client # this is where the testing happens!
|
|
79
|
-
|
|
80
|
-
# Log out the user after the test is done
|
|
81
|
-
client.get('/ivoryos/auth/logout', follow_redirects=True)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@pytest.fixture
|
|
85
|
-
def socketio_client(app):
|
|
86
|
-
"""A test client for Socket.IO."""
|
|
87
|
-
return socketio.test_client(app)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class TestEnum(Enum):
|
|
91
|
-
"""An example Enum for testing type conversion."""
|
|
92
|
-
OPTION_A = 'A'
|
|
93
|
-
OPTION_B = 'B'
|
|
94
|
-
|
|
95
|
-
class DummyModule:
|
|
96
|
-
"""A more comprehensive dummy instrument for testing."""
|
|
97
|
-
def int_method(self, arg: int = 1):
|
|
98
|
-
return arg
|
|
99
|
-
|
|
100
|
-
def float_method(self, arg: float = 1.0):
|
|
101
|
-
return arg
|
|
102
|
-
|
|
103
|
-
def bool_method(self, arg: bool = False):
|
|
104
|
-
return arg
|
|
105
|
-
|
|
106
|
-
def list_method(self, arg: list = None):
|
|
107
|
-
return arg or []
|
|
108
|
-
|
|
109
|
-
def enum_method(self, arg: TestEnum = TestEnum.OPTION_A):
|
|
110
|
-
return arg
|
|
111
|
-
|
|
112
|
-
def str_method(self) -> dict:
|
|
113
|
-
return {'status': 'OK'}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@pytest.fixture
|
|
117
|
-
def test_deck(app):
|
|
118
|
-
"""
|
|
119
|
-
A fixture that creates and loads a predictable 'deck' of dummy instruments
|
|
120
|
-
for testing the dynamic control routes.
|
|
121
|
-
"""
|
|
122
|
-
dummy_module = DummyModule()
|
|
123
|
-
snapshot = utils.create_deck_snapshot(dummy_module)
|
|
124
|
-
|
|
125
|
-
with app.app_context():
|
|
126
|
-
global_config.deck_snapshot = snapshot
|
|
127
|
-
global_config.deck = dummy_module # instantiate the class
|
|
128
|
-
|
|
129
|
-
yield DummyModule
|
|
130
|
-
|
|
131
|
-
with app.app_context():
|
|
132
|
-
global_config.deck_snapshot = {}
|
|
133
|
-
global_config.deck = {}
|
tests/integration/__init__.py
DELETED
|
File without changes
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
from ivoryos.utils.db_models import User, db
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def test_get_signup(client):
|
|
5
|
-
"""
|
|
6
|
-
GIVEN a client
|
|
7
|
-
WHEN a GET request is made to /ivoryos/auth/signup
|
|
8
|
-
THEN check that signup page loads with 200 status and contains "Signup" text
|
|
9
|
-
"""
|
|
10
|
-
response = client.get("/ivoryos/auth/signup", follow_redirects=True)
|
|
11
|
-
assert response.status_code == 200
|
|
12
|
-
assert b"Signup" in response.data
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_route_auth_signup(client):
|
|
16
|
-
"""
|
|
17
|
-
GIVEN a client
|
|
18
|
-
WHEN a POST request is made to /ivoryos/auth/signup with valid credentials
|
|
19
|
-
THEN check that signup succeeds with 200 status and the user is created in database
|
|
20
|
-
"""
|
|
21
|
-
response = client.post("/ivoryos/auth/signup",
|
|
22
|
-
data={
|
|
23
|
-
"username": "second_testuser",
|
|
24
|
-
"password": "password"
|
|
25
|
-
},
|
|
26
|
-
follow_redirects=True
|
|
27
|
-
)
|
|
28
|
-
assert response.status_code == 200
|
|
29
|
-
assert b"Login" in response.data
|
|
30
|
-
|
|
31
|
-
# Verify user was created
|
|
32
|
-
with client.application.app_context():
|
|
33
|
-
user = db.session.query(User).filter(User.username == 'second_testuser').first()
|
|
34
|
-
assert user is not None
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_duplicate_user_signup(client, init_database):
|
|
38
|
-
"""
|
|
39
|
-
GIVEN a client and init_database fixture
|
|
40
|
-
WHEN a POST request is made to signup with an existing username
|
|
41
|
-
THEN check that signup fails with 409 status and appropriate error message
|
|
42
|
-
"""
|
|
43
|
-
client.post('/ivoryos/auth/signup', data={
|
|
44
|
-
'username': 'existinguser',
|
|
45
|
-
'password': 'anotherpass'
|
|
46
|
-
})
|
|
47
|
-
# Try to create duplicate
|
|
48
|
-
response = client.post('/ivoryos/auth/signup', data={
|
|
49
|
-
'username': 'existinguser',
|
|
50
|
-
'password': 'anotherpass'
|
|
51
|
-
})
|
|
52
|
-
assert response.status_code == 409
|
|
53
|
-
assert b"Signup" in response.data
|
|
54
|
-
assert b"User already exists" in response.data
|
|
55
|
-
|
|
56
|
-
# Verify user was created
|
|
57
|
-
users = db.session.query(User).filter(User.username == 'existinguser').all()
|
|
58
|
-
assert len(users) == 1
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_failed_login(client):
|
|
62
|
-
"""
|
|
63
|
-
GIVEN a client and invalid login credentials
|
|
64
|
-
WHEN a POST request is made to /ivoryos/auth/login
|
|
65
|
-
THEN check that login fails with 401 status and the appropriate error message
|
|
66
|
-
"""
|
|
67
|
-
response = client.post('/ivoryos/auth/login', data={
|
|
68
|
-
'username': 'nonexistent',
|
|
69
|
-
'password': 'wrongpass'
|
|
70
|
-
})
|
|
71
|
-
assert response.status_code == 401
|
|
72
|
-
|
|
73
|
-
def test_logout(auth):
|
|
74
|
-
"""
|
|
75
|
-
GIVEN an authenticated client
|
|
76
|
-
WHEN a GET request is made to /ivoryos/auth/logout
|
|
77
|
-
THEN check that logout succeeds with 302 status and redirects to login
|
|
78
|
-
"""
|
|
79
|
-
response = auth.get('/ivoryos/auth/logout')
|
|
80
|
-
assert response.status_code == 302 # Redirect to login
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
from unittest.mock import patch, Mock
|
|
2
|
-
|
|
3
|
-
from ivoryos.utils.db_models import Script
|
|
4
|
-
from ivoryos import db
|
|
5
|
-
|
|
6
|
-
def test_control_panel_redirects_anonymous(client):
|
|
7
|
-
"""
|
|
8
|
-
GIVEN an anonymous user
|
|
9
|
-
WHEN the control panel is accessed
|
|
10
|
-
THEN they should be redirected to the login page
|
|
11
|
-
"""
|
|
12
|
-
response = client.get('/ivoryos/control/home/deck', follow_redirects=True)
|
|
13
|
-
assert response.status_code == 200
|
|
14
|
-
assert b'Login' in response.data
|
|
15
|
-
|
|
16
|
-
def test_deck_control_for_auth_user(auth):
|
|
17
|
-
"""
|
|
18
|
-
GIVEN an authenticated user
|
|
19
|
-
WHEN the control panel is accessed
|
|
20
|
-
THEN the page should load successfully
|
|
21
|
-
"""
|
|
22
|
-
response = auth.get('/ivoryos/control/home/deck', follow_redirects=True)
|
|
23
|
-
assert response.status_code == 200
|
|
24
|
-
assert b'<title>IvoryOS | Devices</title>' in response.data # Assuming this text exists on the page
|
|
25
|
-
|
|
26
|
-
def test_temp_control_for_auth_user(auth):
|
|
27
|
-
"""
|
|
28
|
-
GIVEN an authenticated user
|
|
29
|
-
WHEN the control panel is accessed
|
|
30
|
-
THEN the page should load successfully
|
|
31
|
-
"""
|
|
32
|
-
response = auth.get('/ivoryos/control/home/temp', follow_redirects=True)
|
|
33
|
-
assert response.status_code == 200
|
|
34
|
-
# assert b'<title>IvoryOS | Devices</title>' in response.data # Assuming this text exists on the page
|
|
35
|
-
|
|
36
|
-
def test_new_controller_page(auth):
|
|
37
|
-
"""Test new controller page loads"""
|
|
38
|
-
response = auth.get('/ivoryos/control/new/')
|
|
39
|
-
assert response.status_code == 200
|
|
40
|
-
|
|
41
|
-
def test_download_proxy(self, auth_headers):
|
|
42
|
-
"""Test proxy download functionality"""
|
|
43
|
-
with patch('ivoryos.routes.control.control.global_config') as mock_config:
|
|
44
|
-
mock_config.deck_snapshot = {'test_instrument': {'test_method': {'signature': 'test()'}}}
|
|
45
|
-
response = auth_headers.get('/ivoryos/control/download')
|
|
46
|
-
assert response.status_code == 200
|
|
47
|
-
assert response.headers['Content-Disposition'].startswith('attachment')
|
|
48
|
-
|
|
49
|
-
def test_backend_control_get(self, auth_headers):
|
|
50
|
-
"""Test backend control GET endpoint"""
|
|
51
|
-
with patch('ivoryos.routes.control.control.global_config') as mock_config:
|
|
52
|
-
mock_config.deck_snapshot = {'test_instrument': {'test_method': {'signature': 'test()'}}}
|
|
53
|
-
response = auth_headers.get('/ivoryos/api/control/')
|
|
54
|
-
assert response.status_code == 200
|
|
55
|
-
assert response.is_json
|
|
56
|
-
|
|
57
|
-
@patch('ivoryos.routes.control.control.runner')
|
|
58
|
-
@patch('ivoryos.routes.control.control.find_instrument_by_name')
|
|
59
|
-
@patch('ivoryos.routes.control.control.create_form_from_module')
|
|
60
|
-
def test_backend_control_post(self, mock_form, mock_find, mock_runner, auth_headers):
|
|
61
|
-
"""Test backend control POST endpoint"""
|
|
62
|
-
# Setup mocks
|
|
63
|
-
mock_instrument = Mock()
|
|
64
|
-
mock_find.return_value = mock_instrument
|
|
65
|
-
mock_field = Mock()
|
|
66
|
-
mock_field.name = 'test_param'
|
|
67
|
-
mock_field.data = 'test_value'
|
|
68
|
-
mock_form_instance = Mock()
|
|
69
|
-
mock_form_instance.__iter__ = Mock(return_value=iter([mock_field]))
|
|
70
|
-
mock_form.return_value = {'test_method': mock_form_instance}
|
|
71
|
-
mock_runner.run_single_step.return_value = 'success'
|
|
72
|
-
response = auth_headers.post('/ivoryos/api/control/test_instrument', data={
|
|
73
|
-
'hidden_name': 'test_method',
|
|
74
|
-
'hidden_wait': 'true'
|
|
75
|
-
})
|
|
76
|
-
assert response.status_code == 200
|
|
77
|
-
|
|
78
|
-
# def test_control(auth, app):
|
|
79
|
-
# """
|
|
80
|
-
# GIVEN an authenticated user and an existing script
|
|
81
|
-
# WHEN a POST request is made to run the script
|
|
82
|
-
# THEN the user should be redirected and a success message shown
|
|
83
|
-
# """
|
|
84
|
-
# # We need to create a script in the database first
|
|
85
|
-
# with app.app_context():
|
|
86
|
-
# script = Script(name='My Test Script', author='testuser', content='print("hello")')
|
|
87
|
-
# db.session.add(script)
|
|
88
|
-
# db.session.commit()
|
|
89
|
-
# script_id = script.id
|
|
90
|
-
#
|
|
91
|
-
# # Simulate running the script
|
|
92
|
-
# response = auth.post(f'/ivoryos/control/run/{script_id}', follow_redirects=True)
|
|
93
|
-
# assert response.status_code == 200
|
|
94
|
-
# assert b'has been initiated' in response.data # Check for a flash message
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
|
|
3
|
-
from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep
|
|
4
|
-
from ivoryos import db
|
|
5
|
-
|
|
6
|
-
def test_database_scripts_page(auth):
|
|
7
|
-
"""
|
|
8
|
-
GIVEN an authenticated user
|
|
9
|
-
WHEN they access the script database page
|
|
10
|
-
THEN the page should load and show their scripts
|
|
11
|
-
"""
|
|
12
|
-
# First, create a script so the page has something to render
|
|
13
|
-
with auth.application.app_context():
|
|
14
|
-
script = Script(name='test_script', author='testuser')
|
|
15
|
-
db.session.add(script)
|
|
16
|
-
db.session.commit()
|
|
17
|
-
|
|
18
|
-
response = auth.get('/ivoryos/database/scripts/', follow_redirects=True)
|
|
19
|
-
assert response.status_code == 200
|
|
20
|
-
# assert b'Scripts Database' in response.data
|
|
21
|
-
assert b'<title>IvoryOS | Design Database</title>' in response.data
|
|
22
|
-
|
|
23
|
-
def test_database_workflows_page(auth):
|
|
24
|
-
"""
|
|
25
|
-
GIVEN an authenticated user
|
|
26
|
-
WHEN they access the workflow database page
|
|
27
|
-
THEN the page should load and show past workflow runs
|
|
28
|
-
"""
|
|
29
|
-
# Create a workflow run to display
|
|
30
|
-
with auth.application.app_context():
|
|
31
|
-
run = WorkflowRun(name="untitled", platform="deck",start_time=datetime.now())
|
|
32
|
-
db.session.add(run)
|
|
33
|
-
db.session.commit()
|
|
34
|
-
run_id = run.id
|
|
35
|
-
|
|
36
|
-
response = auth.get('/ivoryos/database/workflows/', follow_redirects=True)
|
|
37
|
-
assert response.status_code == 200
|
|
38
|
-
assert b'Workflow ID' in response.data
|
|
39
|
-
# assert b'run_id' in response.data
|
|
40
|
-
|
|
41
|
-
def test_view_specific_workflow(auth):
|
|
42
|
-
"""
|
|
43
|
-
GIVEN an authenticated user and an existing workflow run
|
|
44
|
-
WHEN they access the specific URL for that workflow
|
|
45
|
-
THEN the detailed view for that run should be displayed
|
|
46
|
-
"""
|
|
47
|
-
with auth.application.app_context():
|
|
48
|
-
run = WorkflowRun(name='test_workflow', platform='test_platform', start_time=datetime.now())
|
|
49
|
-
db.session.add(run)
|
|
50
|
-
db.session.commit()
|
|
51
|
-
run_id = run.id
|
|
52
|
-
|
|
53
|
-
step = WorkflowStep(method_name='test_step', workflow_id=run_id, phase="main", run_error=False, start_time=datetime.now())
|
|
54
|
-
db.session.add(step)
|
|
55
|
-
db.session.commit()
|
|
56
|
-
# run_id = run.id
|
|
57
|
-
|
|
58
|
-
response = auth.get(f'/ivoryos/database/workflows/{run_id}', follow_redirects=True)
|
|
59
|
-
assert response.status_code == 200
|
|
60
|
-
# assert b'test_step' in response.data # Check for a title on the view page
|
|
61
|
-
assert b'test_workflow' in response.data
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
def test_design_page_loads_for_auth_user(auth):
|
|
2
|
-
"""
|
|
3
|
-
GIVEN an authenticated user
|
|
4
|
-
WHEN the design page is accessed
|
|
5
|
-
THEN the page should load successfully
|
|
6
|
-
"""
|
|
7
|
-
response = auth.get('/ivoryos/design/script/', follow_redirects=True)
|
|
8
|
-
assert response.status_code == 200
|
|
9
|
-
assert b'<title>IvoryOS | Design</title>' in response.data # Assuming this text exists
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_clear_canvas(auth):
|
|
13
|
-
"""
|
|
14
|
-
Tests clearing the design canvas.
|
|
15
|
-
"""
|
|
16
|
-
response = auth.get('/ivoryos/design/clear', follow_redirects=True)
|
|
17
|
-
assert response.status_code == 200
|
|
18
|
-
# assert b'Operations' in response.data
|
|
19
|
-
|
|
20
|
-
# def test_add_action(auth, test_deck):
|
|
21
|
-
# """
|
|
22
|
-
# Tests adding an action to the design canvas.
|
|
23
|
-
# """
|
|
24
|
-
# response = auth.post('/ivoryos/design/script/deck.dummy/', data={
|
|
25
|
-
# 'hidden_name': 'int_method',
|
|
26
|
-
# 'arg': '10'
|
|
27
|
-
# }, follow_redirects=True)
|
|
28
|
-
# assert response.status_code == 200
|
|
29
|
-
|
|
30
|
-
def test_experiment_run_page(auth):
|
|
31
|
-
"""
|
|
32
|
-
Tests the experiment run page.
|
|
33
|
-
"""
|
|
34
|
-
response = auth.get('/ivoryos/design/campaign')
|
|
35
|
-
assert response.status_code == 200
|
|
36
|
-
assert b'Run Panel' in response.data
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
from flask_login import current_user
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def test_home_page_authenticated(auth, app):
|
|
5
|
-
"""
|
|
6
|
-
GIVEN an authenticated user (using the 'auth' fixture)
|
|
7
|
-
WHEN the home page is accessed
|
|
8
|
-
THEN check that they see the main application page
|
|
9
|
-
"""
|
|
10
|
-
with auth.application.test_request_context('/ivoryos/'):
|
|
11
|
-
# Manually trigger the before_request functions that Flask-Login uses
|
|
12
|
-
app.preprocess_request()
|
|
13
|
-
|
|
14
|
-
# Assert that the `current_user` proxy is now populated and authenticated
|
|
15
|
-
assert current_user.is_authenticated
|
|
16
|
-
assert current_user.username == 'testuser'
|
|
17
|
-
|
|
18
|
-
def test_help_page(client):
|
|
19
|
-
"""
|
|
20
|
-
GIVEN an unauthenticated user
|
|
21
|
-
WHEN they access the help page
|
|
22
|
-
THEN check that the page loads successfully and contains documentation content
|
|
23
|
-
"""
|
|
24
|
-
response = client.get('/ivoryos/help')
|
|
25
|
-
assert response.status_code == 200
|
|
26
|
-
assert b'Documentations' in response.data
|
|
27
|
-
|
|
28
|
-
def test_prefix_redirect(auth):
|
|
29
|
-
"""
|
|
30
|
-
GIVEN an authenticated user (using the 'auth' fixture)
|
|
31
|
-
WHEN the home page is accessed without prefix
|
|
32
|
-
THEN check that they see the main application page
|
|
33
|
-
"""
|
|
34
|
-
response = auth.get('/', follow_redirects=True)
|
|
35
|
-
assert response.status_code == 200
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
def test_socket_connection(socketio_client):
|
|
2
|
-
"""
|
|
3
|
-
Test that a client can successfully connect to the Socket.IO server.
|
|
4
|
-
"""
|
|
5
|
-
assert socketio_client.is_connected()
|
|
6
|
-
socketio_client.disconnect()
|
|
7
|
-
assert not socketio_client.is_connected()
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# def test_logger_socket_event(socketio_client):
|
|
11
|
-
# """
|
|
12
|
-
# Test the custom logging event handler.
|
|
13
|
-
# (This assumes you have a handler like `@socketio.on('start_log')`)
|
|
14
|
-
# """
|
|
15
|
-
# # Connect the client
|
|
16
|
-
# socketio_client.connect()
|
|
17
|
-
#
|
|
18
|
-
# # Emit an event from the client to the server
|
|
19
|
-
# socketio_client.emit('start_log', {'logger_name': 'my_test_logger'})
|
|
20
|
-
#
|
|
21
|
-
# # Check what the server sent back to the client
|
|
22
|
-
# received = socketio_client.get_received()
|
|
23
|
-
#
|
|
24
|
-
# assert len(received) > 0
|
|
25
|
-
# assert received[0]['name'] == 'log_message' # Check for the event name
|
|
26
|
-
# assert 'Logger my_test_logger started' in received[0]['args'][0]['data']
|
|
File without changes
|