1+ from __future__ import annotations
2+
13import base64
24import gzip
35import json
810from dataclasses import dataclass , field
911from datetime import datetime , timedelta
1012from enum import Enum
13+ from typing import TYPE_CHECKING , Any
14+
15+ if TYPE_CHECKING :
16+ from linopy .model import Model
1117
1218try :
1319 import requests
@@ -42,11 +48,97 @@ class OetcSettings:
4248 orchestrator_server_url : str
4349 compute_provider : ComputeProvider = ComputeProvider .GCP
4450 solver : str = "highs"
45- solver_options : dict = field (default_factory = dict )
51+ solver_options : dict [ str , Any ] = field (default_factory = dict )
4652 cpu_cores : int = 2
4753 disk_space_gb : int = 10
4854 delete_worker_on_error : bool = False
4955
56+ @classmethod
57+ def from_env (
58+ cls ,
59+ * ,
60+ email : str | None = None ,
61+ password : str | None = None ,
62+ name : str | None = None ,
63+ authentication_server_url : str | None = None ,
64+ orchestrator_server_url : str | None = None ,
65+ cpu_cores : int | None = None ,
66+ disk_space_gb : int | None = None ,
67+ delete_worker_on_error : bool | None = None ,
68+ ) -> OetcSettings :
69+ required_fields = {
70+ "email" : ("OETC_EMAIL" , email ),
71+ "password" : ("OETC_PASSWORD" , password ),
72+ "name" : ("OETC_NAME" , name ),
73+ "authentication_server_url" : ("OETC_AUTH_URL" , authentication_server_url ),
74+ "orchestrator_server_url" : (
75+ "OETC_ORCHESTRATOR_URL" ,
76+ orchestrator_server_url ,
77+ ),
78+ }
79+
80+ resolved : dict [str , Any ] = {}
81+ missing : list [str ] = []
82+
83+ for field_name , (env_var , kwarg ) in required_fields .items ():
84+ if kwarg is not None :
85+ resolved [field_name ] = kwarg
86+ else :
87+ env_val = os .environ .get (env_var , "" ).strip ()
88+ if env_val :
89+ resolved [field_name ] = env_val
90+ else :
91+ missing .append (env_var )
92+
93+ if missing :
94+ raise ValueError (
95+ f"Missing required OETC configuration: { ', ' .join (missing )} "
96+ )
97+
98+ kwargs : dict [str , Any ] = {
99+ "credentials" : OetcCredentials (
100+ email = resolved ["email" ], password = resolved ["password" ]
101+ ),
102+ "name" : resolved ["name" ],
103+ "authentication_server_url" : resolved ["authentication_server_url" ],
104+ "orchestrator_server_url" : resolved ["orchestrator_server_url" ],
105+ }
106+
107+ if cpu_cores is not None :
108+ kwargs ["cpu_cores" ] = cpu_cores
109+ elif (cpu_env := os .environ .get ("OETC_CPU_CORES" )) is not None :
110+ try :
111+ kwargs ["cpu_cores" ] = int (cpu_env )
112+ except ValueError as e :
113+ raise ValueError (
114+ f"OETC_CPU_CORES is not a valid integer: { cpu_env } "
115+ ) from e
116+
117+ if disk_space_gb is not None :
118+ kwargs ["disk_space_gb" ] = disk_space_gb
119+ elif (disk_env := os .environ .get ("OETC_DISK_SPACE_GB" )) is not None :
120+ try :
121+ kwargs ["disk_space_gb" ] = int (disk_env )
122+ except ValueError as e :
123+ raise ValueError (
124+ f"OETC_DISK_SPACE_GB is not a valid integer: { disk_env } "
125+ ) from e
126+
127+ if delete_worker_on_error is not None :
128+ kwargs ["delete_worker_on_error" ] = delete_worker_on_error
129+ elif (del_env := os .environ .get ("OETC_DELETE_WORKER_ON_ERROR" )) is not None :
130+ low = del_env .lower ()
131+ if low in ("true" , "1" , "yes" ):
132+ kwargs ["delete_worker_on_error" ] = True
133+ elif low in ("false" , "0" , "no" ):
134+ kwargs ["delete_worker_on_error" ] = False
135+ else :
136+ raise ValueError (
137+ f"OETC_DELETE_WORKER_ON_ERROR has invalid value: { del_env } "
138+ )
139+
140+ return cls (** kwargs )
141+
50142
51143@dataclass
52144class GcpCredentials :
@@ -226,12 +318,16 @@ def __get_gcp_credentials(self) -> GcpCredentials:
226318 except Exception as e :
227319 raise Exception (f"Error fetching GCP credentials: { e } " )
228320
229- def _submit_job_to_compute_service (self , input_file_name : str ) -> str :
321+ def _submit_job_to_compute_service (
322+ self , input_file_name : str , solver : str , solver_options : dict [str , Any ]
323+ ) -> str :
230324 """
231325 Submit a job to the compute service.
232326
233327 Args:
234328 input_file_name: Name of the input file uploaded to GCP
329+ solver: Solver name to use
330+ solver_options: Solver options dict
235331
236332 Returns:
237333 CreateComputeJobResult: The job creation result with UUID
@@ -243,8 +339,8 @@ def _submit_job_to_compute_service(self, input_file_name: str) -> str:
243339 logger .info ("OETC - Submitting compute job..." )
244340 payload = {
245341 "name" : self .settings .name ,
246- "solver" : self . settings . solver ,
247- "solver_options" : self . settings . solver_options ,
342+ "solver" : solver ,
343+ "solver_options" : solver_options ,
248344 "provider" : self .settings .compute_provider .value ,
249345 "cpu_cores" : self .settings .cpu_cores ,
250346 "disk_space_gb" : self .settings .disk_space_gb ,
@@ -534,13 +630,19 @@ def _download_file_from_gcp(self, file_name: str) -> str:
534630 except Exception as e :
535631 raise Exception (f"Failed to download file from GCP: { e } " )
536632
537- def solve_on_oetc (self , model ): # type: ignore
633+ def solve_on_oetc (
634+ self , model : Model , solver_name : str | None = None , ** solver_options : Any
635+ ) -> Model :
538636 """
539637 Solve a linopy model on the OET Cloud compute app.
540638
541639 Parameters
542640 ----------
543641 model : linopy.model.Model
642+ solver_name : str, optional
643+ Override the solver from settings.
644+ **solver_options
645+ Override/extend solver_options from settings.
544646
545647 Returns
546648 -------
@@ -552,17 +654,19 @@ def solve_on_oetc(self, model): # type: ignore
552654 Exception: If solving fails at any stage
553655 """
554656 try :
555- # Save model to temporary file and upload
657+ effective_solver = solver_name or self .settings .solver
658+ merged_solver_options = {** self .settings .solver_options , ** solver_options }
659+
556660 with tempfile .NamedTemporaryFile (prefix = "linopy-" , suffix = ".nc" ) as fn :
557661 fn .file .close ()
558662 model .to_netcdf (fn .name )
559663 input_file_name = self ._upload_file_to_gcp (fn .name )
560664
561- # Submit job and wait for completion
562- job_uuid = self ._submit_job_to_compute_service (input_file_name )
665+ job_uuid = self ._submit_job_to_compute_service (
666+ input_file_name , effective_solver , merged_solver_options
667+ )
563668 job_result = self .wait_and_get_job_data (job_uuid )
564669
565- # Download and load the solution
566670 if not job_result .output_files :
567671 raise Exception ("No output files found in completed job" )
568672
@@ -572,26 +676,22 @@ def solve_on_oetc(self, model): # type: ignore
572676
573677 solution_file_path = self ._download_file_from_gcp (output_file_name )
574678
575- # Load the solved model
576679 solved_model = linopy .read_netcdf (solution_file_path )
577680
578- # Clean up downloaded file
579681 os .remove (solution_file_path )
580682
581683 logger .info (
582684 f"OETC - Model solved successfully. Status: { solved_model .status } "
583685 )
584- if hasattr (solved_model , "objective" ) and hasattr (
585- solved_model .objective , "value"
586- ):
686+ if solved_model .objective .value is not None :
587687 logger .info (
588688 f"OETC - Objective value: { solved_model .objective .value :.2e} "
589689 )
590690
591691 return solved_model
592692
593693 except Exception as e :
594- raise Exception (f"Error solving model on OETC: { e } " )
694+ raise Exception (f"Error solving model on OETC: { e } " ) from e
595695
596696 def _gzip_compress (self , source_path : str ) -> str :
597697 """
0 commit comments