cornflow-client 2.0.0a23__tar.gz → 2.0.0rc2__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 (78) hide show
  1. {cornflow_client-2.0.0a23/cornflow_client.egg-info → cornflow_client-2.0.0rc2}/PKG-INFO +9 -9
  2. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/airflow/api.py +182 -2
  3. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/core/application.py +4 -6
  4. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/core/experiment.py +5 -50
  5. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/core/instance.py +4 -41
  6. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/core/instance_solution.py +61 -1
  7. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/unit/test_core.py +146 -0
  8. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2/cornflow_client.egg-info}/PKG-INFO +9 -9
  9. cornflow_client-2.0.0rc2/cornflow_client.egg-info/requires.txt +7 -0
  10. cornflow_client-2.0.0rc2/requirements.txt +7 -0
  11. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/setup.py +2 -2
  12. cornflow_client-2.0.0a23/cornflow_client.egg-info/requires.txt +0 -7
  13. cornflow_client-2.0.0a23/requirements.txt +0 -7
  14. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/LICENSE +0 -0
  15. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/MANIFEST.in +0 -0
  16. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/README.rst +0 -0
  17. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/__init__.py +0 -0
  18. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/airflow/__init__.py +0 -0
  19. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/airflow/dag_utilities.py +0 -0
  20. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/constants.py +0 -0
  21. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/core/__init__.py +0 -0
  22. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/core/read_tools.py +0 -0
  23. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/core/solution.py +0 -0
  24. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/core/tools.py +0 -0
  25. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/cornflow_client.py +0 -0
  26. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/data/__init__.py +0 -0
  27. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/data/empty_schema.json +0 -0
  28. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/data/pulp_json_schema.json +0 -0
  29. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/data/schema_validator.json +0 -0
  30. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/data/solver_config.json +0 -0
  31. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/data/vrp_solution_schema.json +0 -0
  32. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/databricks/__init__.py +0 -0
  33. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/databricks/api.py +0 -0
  34. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/raw_cornflow_client.py +0 -0
  35. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/schema/__init__.py +0 -0
  36. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/schema/dictSchema.py +0 -0
  37. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/schema/dict_functions.py +0 -0
  38. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/schema/manager.py +0 -0
  39. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/schema/tools.py +0 -0
  40. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/__init__.py +0 -0
  41. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/const.py +0 -0
  42. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/__init__.py +0 -0
  43. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/data_input_bad.json +0 -0
  44. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/data_schema.json +0 -0
  45. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/endpoints_access.json +0 -0
  46. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/endpoints_methods.json +0 -0
  47. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/gc_input.json +0 -0
  48. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/gc_output.json +0 -0
  49. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/graph_coloring_input.json +0 -0
  50. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/graph_coloring_output.json +0 -0
  51. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/hk_data_input.json +0 -0
  52. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/hk_data_schema.json +0 -0
  53. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/hk_solution_schema.json +0 -0
  54. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/instance-hackathon2.json +0 -0
  55. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/name_problem_schema.json +0 -0
  56. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/pulp_example_data.json +0 -0
  57. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/schema_with_fk.json +0 -0
  58. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/schema_without_fk.json +0 -0
  59. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/test_mps.mps +0 -0
  60. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/xl_with_access.xlsx +0 -0
  61. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/xl_with_fk.xlsx +0 -0
  62. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/xl_with_methods.xlsx +0 -0
  63. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/data/xl_without_fk.xlsx +0 -0
  64. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/integration/__init__.py +0 -0
  65. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/integration/test_airflow_integration.py +0 -0
  66. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/integration/test_cornflow_integration.py +0 -0
  67. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/integration/test_raw_cornflow_integration.py +0 -0
  68. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/unit/__init__.py +0 -0
  69. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/unit/test_abc.py +0 -0
  70. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/unit/test_dag_utilities.py +0 -0
  71. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/unit/test_instance_solution_methods.py +0 -0
  72. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/unit/test_schema_from_excel.py +0 -0
  73. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/unit/test_schema_manager.py +0 -0
  74. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client/tests/unit/test_varnames.py +0 -0
  75. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client.egg-info/SOURCES.txt +0 -0
  76. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client.egg-info/dependency_links.txt +0 -0
  77. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/cornflow_client.egg-info/top_level.txt +0 -0
  78. {cornflow_client-2.0.0a23 → cornflow_client-2.0.0rc2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cornflow-client
3
- Version: 2.0.0a23
3
+ Version: 2.0.0rc2
4
4
  Summary: Client to connect to a cornflow server
5
5
  Home-page: https://github.com/baobabsoluciones/cornflow
6
6
  Author: baobab soluciones
@@ -9,16 +9,16 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: License :: OSI Approved :: Apache Software License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
- Requires-Python: >=3.9
12
+ Requires-Python: >=3.10
13
13
  Description-Content-Type: text/x-rst
14
14
  License-File: LICENSE
15
- Requires-Dist: requests<=2.32.4
16
- Requires-Dist: genson<=1.2.2
17
- Requires-Dist: jsonschema<=4.21.1
18
- Requires-Dist: marshmallow<=3.20.2
19
- Requires-Dist: pytups<=0.86.2
20
- Requires-Dist: ortools<=9.8.3296
21
- Requires-Dist: PuLP<=2.9.0
15
+ Requires-Dist: requests==2.32.4
16
+ Requires-Dist: genson==1.2.2
17
+ Requires-Dist: jsonschema==4.21.1
18
+ Requires-Dist: marshmallow==3.26.2
19
+ Requires-Dist: pytups==0.86.2
20
+ Requires-Dist: ortools==9.8.3296
21
+ Requires-Dist: PuLP==2.9.0
22
22
  Dynamic: author
23
23
  Dynamic: author-email
24
24
  Dynamic: classifier
@@ -31,6 +31,12 @@ class Airflow(object):
31
31
 
32
32
  @classmethod
33
33
  def from_config(cls, config):
34
+ """
35
+ Create an Airflow client from a configuration dictionary.
36
+
37
+ :param config: The configuration dictionary
38
+ :return: The Airflow client
39
+ """
34
40
  data = dict(
35
41
  url=config["AIRFLOW_URL"],
36
42
  user=config["AIRFLOW_USER"],
@@ -39,6 +45,12 @@ class Airflow(object):
39
45
  return cls(**data)
40
46
 
41
47
  def is_alive(self, config=None):
48
+ """
49
+ Check if the Airflow server is alive.
50
+
51
+ :param config: The configuration dictionary
52
+ :return: True if the Airflow server is alive, False otherwise
53
+ """
42
54
  try:
43
55
  response = requests.get(f"{self.url}/health")
44
56
  except (ConnectionError, HTTPError):
@@ -55,6 +67,13 @@ class Airflow(object):
55
67
  return database and scheduler
56
68
 
57
69
  def request_headers_auth(self, status=200, **kwargs):
70
+ """
71
+ Request headers with authentication.
72
+
73
+ :param status: The status code to check
74
+ :param kwargs: The keyword arguments to pass to the request
75
+ :return: The response
76
+ """
58
77
  def_headers = {"Content-type": "application/json", "Accept": "application/json"}
59
78
  headers = kwargs.get("headers", def_headers)
60
79
  response = requests.request(headers=headers, auth=self.auth, **kwargs)
@@ -63,6 +82,16 @@ class Airflow(object):
63
82
  return response
64
83
 
65
84
  def consume_dag_run(self, dag_name, payload, dag_run_id=None, method="POST"):
85
+ """
86
+ Consume a DAG run.
87
+
88
+ :param dag_name: The name of the DAG
89
+ :param payload: The payload to pass to the request
90
+ :param dag_run_id: The ID of the DAG run
91
+ :param method: The method to use to consume the DAG run
92
+ :return: The response
93
+ """
94
+ # TODO: cleanup method input arguments
66
95
  url = f"{self.url}/dags/{dag_name}/dagRuns"
67
96
  if dag_run_id is not None:
68
97
  url = url + f"/{dag_run_id}"
@@ -70,6 +99,13 @@ class Airflow(object):
70
99
  return response
71
100
 
72
101
  def set_dag_run_state(self, dag_name, payload):
102
+ """
103
+ Set the state of a DAG run.
104
+
105
+ :param dag_name: The name of the DAG
106
+ :param payload: The payload to pass to the request
107
+ :return: The response
108
+ """
73
109
  url = f"{self.url}/dags/{dag_name}/updateTaskInstancesState"
74
110
  return self.request_headers_auth(method="POST", url=url, json=payload)
75
111
 
@@ -80,6 +116,15 @@ class Airflow(object):
80
116
  checks_only=False,
81
117
  case_id=None,
82
118
  ):
119
+ """
120
+ Run a workflow.
121
+
122
+ :param execution_id: The ID of the execution
123
+ :param workflow_name: The name of the DAG
124
+ :param checks_only: Whether to run the checks only
125
+ :param case_id: The ID of the case
126
+ :return: The response
127
+ """
83
128
  conf = dict(exec_id=execution_id, checks_only=checks_only)
84
129
  if case_id is not None:
85
130
  conf["case_id"] = case_id
@@ -90,10 +135,21 @@ class Airflow(object):
90
135
  self, execution_id, dag_name="solve_model_dag", checks_only=False, case_id=None
91
136
  ):
92
137
  """
93
- Run workflow
138
+ Run workflow.
139
+
140
+ DEPRECATION: This method is deprecated and will be removed in version 2.0.0.
141
+
142
+ :param execution_id: The ID of the execution
143
+ :param dag_name: The name of the DAG
144
+ :param checks_only: Whether to run the checks only
145
+ :param case_id: The ID of the case
146
+ :return: The response
94
147
  """
95
148
  warnings.warn(
96
- "This method will be deprecated on later versions, please use newer run_workflow method"
149
+ "This method is deprecated and will be removed in version 2.0.0 "
150
+ "Please use run_workflow() instead.",
151
+ DeprecationWarning,
152
+ stacklevel=2,
97
153
  )
98
154
  return self.run_workflow(
99
155
  execution_id,
@@ -103,17 +159,62 @@ class Airflow(object):
103
159
  )
104
160
 
105
161
  def update_schemas(self, dag_name="update_all_schemas"):
162
+ """
163
+ Update the schemas.
164
+
165
+ :param dag_name: The name of the DAG
166
+ :return: The response
167
+ """
106
168
  return self.consume_dag_run(dag_name, payload={}, method="POST")
107
169
 
108
170
  def update_dag_registry(self, dag_name="update_dag_registry"):
171
+ """
172
+ Update the DAG registry.
173
+
174
+ :param dag_name: The name of the DAG
175
+ :return: The response
176
+ """
109
177
  return self.consume_dag_run(dag_name, payload={}, method="POST")
110
178
 
111
179
  def get_run_status(self, schema, run_id):
180
+ """
181
+ Get the status of a DAG run.
182
+
183
+ :param schema: The name of the DAG
184
+ :param run_id: The ID of the DAG run
185
+ :return: The status of the DAG run
186
+ """
112
187
  return self.consume_dag_run(
113
188
  schema, payload=None, dag_run_id=run_id, method="GET"
114
189
  )
115
190
 
191
+ def get_dag_run_status(self, dag_name, dag_run_id):
192
+ """
193
+ Get the status of a DAG run.
194
+
195
+ DEPRECATION: This method is deprecated and will be removed in version 2.0.0.
196
+
197
+ :param dag_name: The name of the DAG
198
+ :param dag_run_id: The ID of the DAG run
199
+ :return: The status of the DAG run
200
+ """
201
+ warnings.warn(
202
+ "This method is deprecated and will be removed in version 2.0.0. "
203
+ "Please use get_run_status() instead.",
204
+ DeprecationWarning,
205
+ stacklevel=2,
206
+ )
207
+ return self.get_run_status(dag_name, dag_run_id)
208
+
116
209
  def set_dag_run_to_fail(self, dag_name, run_id, new_status="failed"):
210
+ """
211
+ Set the status of a DAG run to failed.
212
+
213
+ :param dag_name: The name of the DAG
214
+ :param run_id: The ID of the DAG run
215
+ :param new_status: The new status of the DAG run
216
+ :return: The response
217
+ """
117
218
  # here, two calls have to be done:
118
219
  # first we get information on the dag_run
119
220
  dag_run = self.consume_dag_run(
@@ -135,45 +236,124 @@ class Airflow(object):
135
236
  return self.set_dag_run_state(dag_name, payload=payload)
136
237
 
137
238
  def get_all_dag_runs(self, dag_name):
239
+ """
240
+ Get all the DAG runs.
241
+
242
+ :param dag_name: The name of the DAG
243
+ :return: The response
244
+ """
138
245
  return self.consume_dag_run(dag_name=dag_name, payload=None, method="GET")
139
246
 
140
247
  def get_workflow_info(self, workflow_name, method="GET"):
248
+ """
249
+ Get the information of a DAG.
250
+
251
+ :param workflow_name: The name of the DAG
252
+ :param method: The method to use to get the information
253
+ :return: The information of the DAG
254
+ """
255
+ # TODO: cleanup method input arguments
141
256
  url = f"{self.url}/dags/{workflow_name}"
142
257
  schema_info = self.request_headers_auth(method=method, url=url)
143
258
  if schema_info.status_code != 200:
144
259
  raise AirflowError("DAG not available")
145
260
  return schema_info
146
261
 
262
+ def get_dag_info(self, dag_name, method="GET"):
263
+ """
264
+ Get the information of a DAG.
265
+
266
+ DEPRECATION: This method is deprecated and will be removed in version 2.0.0.
267
+
268
+ :param dag_name: The name of the DAG
269
+ :param method: The method to use to get the information
270
+ :return: The information of the DAG
271
+ """
272
+ warnings.warn(
273
+ "This method is deprecated and will be removed in version 2.0.0. "
274
+ "Please use get_workflow_info() instead.",
275
+ DeprecationWarning,
276
+ stacklevel=2,
277
+ )
278
+ return self.get_workflow_info(dag_name, method)
279
+
147
280
  def get_one_variable(self, variable):
281
+ """
282
+ Get one variable.
283
+
284
+ :param variable: The name of the variable
285
+ :return: The variable
286
+ """
148
287
  url = f"{self.url}/variables/{variable}"
149
288
  return self.request_headers_auth(method="GET", url=url).json()
150
289
 
151
290
  def get_all_variables(self):
291
+ """
292
+ Get all variables.
293
+
294
+ :return: The variables
295
+ """
152
296
  return self.request_headers_auth(
153
297
  method="GET", url=f"{self.url}/variables"
154
298
  ).json()
155
299
 
156
300
  def get_one_schema(self, dag_name, schema):
301
+ """
302
+ Get one schema.
303
+
304
+ :param dag_name: The name of the DAG
305
+ :param schema: The name of the schema
306
+ :return: The schema
307
+ """
157
308
  return self.get_schemas_for_dag_name(dag_name)[schema]
158
309
 
159
310
  def get_schemas_for_dag_name(self, dag_name):
311
+ """
312
+ Get all schemas for a DAG name.
313
+
314
+ :param dag_name: The name of the DAG
315
+ :return: The schemas
316
+ """
160
317
  response = self.get_one_variable(dag_name)
161
318
  result = json.loads(response["value"])
162
319
  result["name"] = response["key"]
163
320
  return result
164
321
 
165
322
  def get_all_schemas(self):
323
+ """
324
+ Get all schemas.
325
+
326
+ :return: The schemas
327
+ """
166
328
  response = self.get_all_variables()
167
329
  return [dict(name=variable["key"]) for variable in response["variables"]]
168
330
 
169
331
  def get_all_dags(self, method="GET"):
332
+ """
333
+ Get all DAGs.
334
+
335
+ :param method: The method to use to get the DAGs
336
+ :return: The DAGs
337
+ """
170
338
  url = f"{self.url}/dags"
171
339
  return self.request_headers_auth(method=method, url=url)
172
340
 
173
341
  def get_internal_dags(self, method="GET"):
342
+ """
343
+ Get all internal DAGs.
344
+
345
+ :param method: The method to use to get the DAGs
346
+ :return: The DAGs
347
+ """
174
348
  url = f"{self.url}/dags?tags=internal"
175
349
  return self.request_headers_auth(method=method, url=url)
176
350
 
177
351
  def get_model_dags(self, method="GET"):
352
+ """
353
+ Get all model DAGs.
354
+
355
+ :param method: The method to use to get the DAGs
356
+ :return: The DAGs
357
+ """
178
358
  url = f"{self.url}/dags?tags=model"
179
359
  return self.request_headers_auth(method=method, url=url)
@@ -8,6 +8,7 @@ from timeit import default_timer as timer
8
8
  from typing import Type, Dict, List, Tuple, Union
9
9
 
10
10
  from jsonschema import Draft7Validator
11
+ from cornflow_client.constants import BadSolutionChecks
11
12
  from pytups import SuperDict
12
13
 
13
14
  from cornflow_client.constants import (
@@ -263,12 +264,9 @@ class ApplicationCore(ABC):
263
264
 
264
265
  # Perform data checks only if a valid solution dict was obtained
265
266
  # and the solver implements data_checks
266
- if final_sol_dict: # Checks for non-None and non-empty dict
267
- try:
268
- solution_checks = algo.data_checks()
269
- except AttributeError:
270
- # Solver doesn't implement data_checks, which is acceptable.
271
- solution_checks = None
267
+ # Checks for non-None and non-empty dict
268
+ if final_sol_dict:
269
+ solution_checks = algo.data_checks()
272
270
 
273
271
  return final_sol_dict, solution_checks
274
272
 
@@ -2,7 +2,7 @@
2
2
  Base code for the experiment template.
3
3
  """
4
4
 
5
- import warnings
5
+ import logging as log
6
6
  from abc import ABC, abstractmethod
7
7
  from typing import List, Dict, Union
8
8
 
@@ -14,10 +14,11 @@ from cornflow_client.constants import (
14
14
  BadSolutionChecks,
15
15
  )
16
16
  from .instance import InstanceCore
17
+ from .instance_solution import CheckCore
17
18
  from .solution import SolutionCore
18
19
 
19
20
 
20
- class ExperimentCore(ABC):
21
+ class ExperimentCore(CheckCore, ABC):
21
22
  """
22
23
  The solver template.
23
24
  """
@@ -74,17 +75,8 @@ class ExperimentCore(ABC):
74
75
  """
75
76
  Method that executes the ExperimentCore.check() method and validates the result against the schema_checks
76
77
  """
77
- try:
78
- checks = self.check()
79
- if checks is None:
80
- checks = self.check_solution()
81
- except NotImplementedError:
82
- warnings.warn(
83
- "The check_solution() method is deprecated. Please use check() instead. "
84
- "Support for check_solution() will be removed on cornflow-client 2.0.0",
85
- DeprecationWarning,
86
- )
87
- checks = self.check_solution()
78
+ # Check method always exists since it is implemented in the ExperimentCore class
79
+ checks = self.check()
88
80
  validator = Draft7Validator(self.schema_checks)
89
81
  if not validator.is_valid(checks):
90
82
  raise BadSolutionChecks(
@@ -118,43 +110,6 @@ class ExperimentCore(ABC):
118
110
  """
119
111
  return self.launch_all_checks()
120
112
 
121
- def get_check_methods(self) -> list:
122
- """
123
- Finds all class methods starting with check_ and returns them in a list.
124
-
125
- :return: A list of check methods.
126
- """
127
- check_methods = [
128
- m
129
- for m in dir(self)
130
- if m.startswith("check_")
131
- and callable(getattr(self, m))
132
- and m != "check_schema"
133
- and m != "check_solution"
134
- ]
135
- return check_methods
136
-
137
- def launch_all_checks(self) -> Dict[str, Union[List, Dict]]:
138
- """
139
- Launch every check method and return a dict with the check method name as key
140
- and the list/dict of errors / warnings as value.
141
-
142
- It will only return those checks that return a non-empty list/dict.
143
- """
144
- check_methods = {m: getattr(self, m)() for m in self.get_check_methods()}
145
- failed_checks = {}
146
- for k, v in check_methods.items():
147
- if v is None:
148
- continue
149
-
150
- try:
151
- if len(v) > 0:
152
- failed_checks[k.split("check_")[1]] = v
153
- except TypeError:
154
- failed_checks[k.split("check_")[1]] = v
155
-
156
- return dict(failed_checks)
157
-
158
113
  @property
159
114
  @abstractmethod
160
115
  def schema_checks(self) -> dict:
@@ -1,17 +1,16 @@
1
- """
2
-
3
- """
1
+ """ """
4
2
 
3
+ import logging as log
5
4
  from abc import ABC, abstractmethod
6
5
  from typing import List, Dict, Union
7
6
 
8
7
  from jsonschema import Draft7Validator
9
8
 
10
9
  from cornflow_client.constants import BadInstanceChecks
11
- from .instance_solution import InstanceSolutionCore
10
+ from .instance_solution import InstanceSolutionCore, CheckCore
12
11
 
13
12
 
14
- class InstanceCore(InstanceSolutionCore, ABC):
13
+ class InstanceCore(InstanceSolutionCore, CheckCore, ABC):
15
14
  """
16
15
  The instance template.
17
16
  """
@@ -54,39 +53,3 @@ class InstanceCore(InstanceSolutionCore, ABC):
54
53
  the method Instance.check()
55
54
  """
56
55
  raise NotImplementedError()
57
-
58
- def get_check_methods(self) -> list:
59
- """
60
- Finds all class methods starting with check_ and returns them in a list.
61
-
62
- :return: A list of check methods.
63
- """
64
- check_methods = [
65
- m
66
- for m in dir(self)
67
- if m.startswith("check_")
68
- and callable(getattr(self, m))
69
- and m != "check_schema"
70
- ]
71
- return check_methods
72
-
73
- def launch_all_checks(self) -> Dict[str, Union[List, Dict]]:
74
- """
75
- Launch every check method and return a dict with the check method name as key
76
- and the list/dict of errors / warnings as value.
77
-
78
- It will only return those checks that return a non-empty list/dict.
79
- """
80
- check_methods = {m: getattr(self, m)() for m in self.get_check_methods()}
81
- failed_checks = {}
82
- for k, v in check_methods.items():
83
- if v is None:
84
- continue
85
-
86
- try:
87
- if len(v) > 0:
88
- failed_checks[k.split("check_")[1]] = v
89
- except TypeError:
90
- failed_checks[k.split("check_")[1]] = v
91
-
92
- return dict(failed_checks)
@@ -3,7 +3,8 @@
3
3
  import json
4
4
  from abc import ABC, abstractmethod
5
5
  from datetime import datetime, timedelta
6
- from typing import List
6
+ from typing import List, Dict, Union
7
+ import logging as log
7
8
 
8
9
  from genson import SchemaBuilder
9
10
  from jsonschema import Draft7Validator
@@ -431,3 +432,62 @@ class InstanceSolutionCore(ABC):
431
432
  """Returns the integer value of the hour (in number) from ts string in format 'YYYY-MM-DDTh:m:s'"""
432
433
  datetime_obj = datetime.strptime(string, DATETIME_FORMAT)
433
434
  return datetime_obj.hour
435
+
436
+
437
+ class CheckCore:
438
+ def get_check_methods(self) -> list:
439
+ """
440
+ Finds all class methods starting with check_ and returns them in a list.
441
+
442
+ :return: A list of check methods.
443
+ """
444
+ check_methods = [
445
+ m
446
+ for m in dir(self)
447
+ if m.startswith("check_")
448
+ and callable(getattr(self, m))
449
+ and m != "check_schema"
450
+ and m != "check_solution"
451
+ ]
452
+ return check_methods
453
+
454
+ def launch_all_checks(self) -> Dict[str, Union[List, Dict]]:
455
+ """
456
+ Launch every check method and return a dict with the check method name as key
457
+ and the list/dict of errors / warnings as value.
458
+
459
+ It will only return those checks that return a non-empty list/dict.
460
+ If a check method raises an exception, it will be caught and a generic error
461
+ message will be added to the checks dictionary.
462
+ """
463
+ check_method_names = self.get_check_methods()
464
+ check_methods = {}
465
+
466
+ # Execute each check method and catch any exceptions
467
+ for method_name in check_method_names:
468
+ try:
469
+ check_methods[method_name] = getattr(self, method_name)()
470
+ except Exception:
471
+ # If a check fails, add a generic error message
472
+ check_methods[method_name] = [
473
+ {
474
+ "error_type": "Check execution error",
475
+ "error_message": "The execution of the check has failed, please contact support",
476
+ }
477
+ ]
478
+ log.warning(
479
+ f"The execution of the check {method_name} has failed, please contact support"
480
+ )
481
+
482
+ failed_checks = {}
483
+ for k, v in check_methods.items():
484
+ if v is None:
485
+ continue
486
+
487
+ try:
488
+ if len(v) > 0:
489
+ failed_checks[k.split("check_")[1]] = v
490
+ except TypeError:
491
+ failed_checks[k.split("check_")[1]] = v
492
+
493
+ return dict(failed_checks)
@@ -85,6 +85,39 @@ class TestCore(TestCase):
85
85
 
86
86
  self.config = SuperDict(msg=False, timeLimit=1, solver="default", seconds=10)
87
87
 
88
+ @property
89
+ def error_check_schema(self):
90
+ """Common schema for error check testing used across multiple test methods."""
91
+ return get_empty_schema(
92
+ properties=dict(
93
+ test_check=dict(
94
+ type="array",
95
+ objects=dict(
96
+ type="object",
97
+ properties=dict(
98
+ error_type=dict(type="string"),
99
+ error_message=dict(type="string"),
100
+ ),
101
+ ),
102
+ ),
103
+ working_check=dict(
104
+ type="array",
105
+ objects=dict(type="object"),
106
+ ),
107
+ )
108
+ )
109
+
110
+ @property
111
+ def error_test_cases(self):
112
+ """Common test cases for error handling used across multiple test methods."""
113
+ return [
114
+ (RuntimeError, "Runtime error occurred"),
115
+ (AttributeError, "Attribute not found"),
116
+ (ValueError, "Invalid value provided"),
117
+ (TypeError, "Type mismatch error"),
118
+ (KeyError, "Missing key in dictionary"),
119
+ ]
120
+
88
121
  def tearDown(self):
89
122
  pass
90
123
 
@@ -293,3 +326,116 @@ class TestCore(TestCase):
293
326
  "b_equal_c": [{"b": 2, "c": 1}],
294
327
  },
295
328
  )
329
+
330
+ def test_solution_check_method_exception_handling(self):
331
+ """Test that when a check_* method raises an exception, a generic error message is added."""
332
+
333
+ class DummySolution(SolutionCore):
334
+ schema = get_empty_schema(properties=dict(sleep=dict(type="number")))
335
+
336
+ class ErrorCheckSolver(ExperimentCore):
337
+ schema_checks = self.error_check_schema
338
+
339
+ def __init__(
340
+ self,
341
+ instance,
342
+ solution=None,
343
+ error_class=RuntimeError,
344
+ error_message="Error",
345
+ ):
346
+ super().__init__(instance, solution)
347
+ self.error_class = error_class
348
+ self.error_message = error_message
349
+
350
+ def solve(self, options):
351
+ self.solution = DummySolution({"sleep": 1})
352
+ return dict(status=STATUS_OPTIMAL, status_sol=SOLUTION_STATUS_FEASIBLE)
353
+
354
+ def check_test_check(self):
355
+ # Simulate different types of unexpected errors in a check method
356
+ raise self.error_class(self.error_message)
357
+
358
+ def check_working_check(self):
359
+ # This check should work correctly and return empty list (no errors)
360
+ return []
361
+
362
+ def get_objective(self):
363
+ return 0
364
+
365
+ class ErrorApp(self.app.__class__):
366
+ solvers = dict(default=ErrorCheckSolver)
367
+ solution = DummySolution
368
+
369
+ error_app = ErrorApp()
370
+
371
+ for error_class, error_message in self.error_test_cases:
372
+ with self.subTest(error_type=error_class.__name__):
373
+ # Create instance and solution for the solver
374
+ instance = error_app.instance.from_dict({"seconds": 1})
375
+ solution = error_app.solution.from_dict({"sleep": 1})
376
+ error_solver = ErrorCheckSolver(
377
+ instance, solution, error_class, error_message
378
+ )
379
+
380
+ # Should NOT raise an exception, but return the checks with generic error
381
+ checks = error_solver.launch_all_checks()
382
+
383
+ # Verify the generic error message is in the checks for the failing check
384
+ self.assertIn("test_check", checks)
385
+ self.assertEqual(
386
+ checks["test_check"],
387
+ [
388
+ {
389
+ "error_type": "Check execution error",
390
+ "error_message": "The execution of the check has failed, please contact support",
391
+ }
392
+ ],
393
+ )
394
+
395
+ # Verify that the working check is NOT in the failed checks (empty list means no errors)
396
+ self.assertNotIn("working_check", checks)
397
+
398
+ def test_instance_check_method_exception_handling(self):
399
+ """Test that when a check_* method of an instance raises an exception, a generic error message is added."""
400
+
401
+ class ErrorCheckInstance(InstanceCore):
402
+ schema = get_empty_schema(properties=dict(seconds=dict(type="number")))
403
+ schema_checks = self.error_check_schema
404
+
405
+ def __init__(self, data, error_class=RuntimeError, error_message="Error"):
406
+ super().__init__(data)
407
+ self.error_class = error_class
408
+ self.error_message = error_message
409
+
410
+ def check_test_check(self):
411
+ # Simulate different types of unexpected errors in a check method
412
+ raise self.error_class(self.error_message)
413
+
414
+ def check_working_check(self):
415
+ # This check should work correctly and return empty list (no errors)
416
+ return []
417
+
418
+ for error_class, error_message in self.error_test_cases:
419
+ with self.subTest(error_type=error_class.__name__):
420
+ # Create instance
421
+ error_instance = ErrorCheckInstance(
422
+ {"seconds": 1}, error_class, error_message
423
+ )
424
+
425
+ # Should NOT raise an exception, but return the checks with generic error
426
+ checks = error_instance.launch_all_checks()
427
+
428
+ # Verify the generic error message is in the checks for the failing check
429
+ self.assertIn("test_check", checks)
430
+ self.assertEqual(
431
+ checks["test_check"],
432
+ [
433
+ {
434
+ "error_type": "Check execution error",
435
+ "error_message": "The execution of the check has failed, please contact support",
436
+ }
437
+ ],
438
+ )
439
+
440
+ # Verify that the working check is NOT in the failed checks (empty list means no errors)
441
+ self.assertNotIn("working_check", checks)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cornflow-client
3
- Version: 2.0.0a23
3
+ Version: 2.0.0rc2
4
4
  Summary: Client to connect to a cornflow server
5
5
  Home-page: https://github.com/baobabsoluciones/cornflow
6
6
  Author: baobab soluciones
@@ -9,16 +9,16 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: License :: OSI Approved :: Apache Software License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
- Requires-Python: >=3.9
12
+ Requires-Python: >=3.10
13
13
  Description-Content-Type: text/x-rst
14
14
  License-File: LICENSE
15
- Requires-Dist: requests<=2.32.4
16
- Requires-Dist: genson<=1.2.2
17
- Requires-Dist: jsonschema<=4.21.1
18
- Requires-Dist: marshmallow<=3.20.2
19
- Requires-Dist: pytups<=0.86.2
20
- Requires-Dist: ortools<=9.8.3296
21
- Requires-Dist: PuLP<=2.9.0
15
+ Requires-Dist: requests==2.32.4
16
+ Requires-Dist: genson==1.2.2
17
+ Requires-Dist: jsonschema==4.21.1
18
+ Requires-Dist: marshmallow==3.26.2
19
+ Requires-Dist: pytups==0.86.2
20
+ Requires-Dist: ortools==9.8.3296
21
+ Requires-Dist: PuLP==2.9.0
22
22
  Dynamic: author
23
23
  Dynamic: author-email
24
24
  Dynamic: classifier
@@ -0,0 +1,7 @@
1
+ requests==2.32.4
2
+ genson==1.2.2
3
+ jsonschema==4.21.1
4
+ marshmallow==3.26.2
5
+ pytups==0.86.2
6
+ ortools==9.8.3296
7
+ PuLP==2.9.0
@@ -0,0 +1,7 @@
1
+ requests==2.32.4
2
+ genson==1.2.2
3
+ jsonschema==4.21.1
4
+ marshmallow==3.26.2
5
+ pytups==0.86.2
6
+ ortools==9.8.3296
7
+ PuLP==2.9.0
@@ -12,7 +12,7 @@ extra_required = {"excel": ["openpyxl", "pandas"]}
12
12
 
13
13
  setuptools.setup(
14
14
  name="cornflow-client",
15
- version="2.0.0a23",
15
+ version="2.0.0rc2",
16
16
  author="baobab soluciones",
17
17
  author_email="sistemas@baobabsoluciones.es",
18
18
  description="Client to connect to a cornflow server",
@@ -26,7 +26,7 @@ setuptools.setup(
26
26
  "Operating System :: OS Independent",
27
27
  "Development Status :: 5 - Production/Stable",
28
28
  ],
29
- python_requires=">=3.9",
29
+ python_requires=">=3.10",
30
30
  include_package_data=True,
31
31
  install_requires=required,
32
32
  extra_require=extra_required,
@@ -1,7 +0,0 @@
1
- requests<=2.32.4
2
- genson<=1.2.2
3
- jsonschema<=4.21.1
4
- marshmallow<=3.20.2
5
- pytups<=0.86.2
6
- ortools<=9.8.3296
7
- PuLP<=2.9.0
@@ -1,7 +0,0 @@
1
- requests<=2.32.4
2
- genson<=1.2.2
3
- jsonschema<=4.21.1
4
- marshmallow<=3.20.2
5
- pytups<=0.86.2
6
- ortools<=9.8.3296
7
- PuLP<=2.9.0