wrfrun 0.1.9__py3-none-any.whl → 0.2.0__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.
wrfrun/cli.py CHANGED
@@ -7,7 +7,7 @@ from shutil import copyfile
7
7
  import tomli
8
8
  import tomli_w
9
9
 
10
- from .core import WRFRUNConfig
10
+ from .core import WRFRunConfig
11
11
  from .res import CONFIG_MAIN_TOML_TEMPLATE, CONFIG_WRF_TOML_TEMPLATE
12
12
  from .utils import logger
13
13
 
@@ -16,6 +16,9 @@ MODEL_MAP = {
16
16
  }
17
17
 
18
18
 
19
+ wrfrun_config = WRFRunConfig("./.wrfrun")
20
+
21
+
19
22
  def _entry_init(args: argparse.Namespace):
20
23
  """
21
24
  Initialize a wrfrun project.
@@ -35,11 +38,11 @@ def _entry_init(args: argparse.Namespace):
35
38
  makedirs(f"{project_name}/configs")
36
39
  makedirs(f"{project_name}/data")
37
40
 
38
- copyfile(WRFRUNConfig.parse_resource_uri(CONFIG_MAIN_TOML_TEMPLATE), f"{project_name}/config.toml")
41
+ copyfile(wrfrun_config.parse_resource_uri(CONFIG_MAIN_TOML_TEMPLATE), f"{project_name}/config.toml")
39
42
 
40
43
  if models is not None:
41
44
  for _model in models:
42
- src_path = WRFRUNConfig.parse_resource_uri(MODEL_MAP[_model])
45
+ src_path = wrfrun_config.parse_resource_uri(MODEL_MAP[_model])
43
46
  copyfile(src_path, f"{project_name}/configs/{_model}.toml")
44
47
 
45
48
  logger.info(f"Created project {project_name}.")
@@ -95,7 +98,7 @@ def _entry_model(args: argparse.Namespace):
95
98
  }
96
99
 
97
100
  for _new_model in new_models:
98
- copyfile(WRFRUNConfig.parse_resource_uri(model_config_map[_new_model]), f"{config_dir_path}/{_new_model}.toml")
101
+ copyfile(wrfrun_config.parse_resource_uri(model_config_map[_new_model]), f"{config_dir_path}/{_new_model}.toml")
99
102
 
100
103
  with open(config_path, "wb") as f:
101
104
  tomli_w.dump(main_config, f)
wrfrun/core/base.py CHANGED
@@ -13,7 +13,8 @@ Defines what :class:`ExecutableBase <Executable>` is, how it works and how ``wrf
13
13
  FileConfigDict
14
14
  ExecutableClassConfig
15
15
  ExecutableConfig
16
- _ExecutableConfigRecord
16
+ ExecutableConfigRecord
17
+ create_recorder
17
18
  ExecutableBase
18
19
 
19
20
  Executable
@@ -54,7 +55,7 @@ from typing import Optional, TypedDict, Union
54
55
  import numpy as np
55
56
 
56
57
  from .config import WRFRUNConfig
57
- from .error import CommandError, ConfigError, OutputFileError
58
+ from .error import CommandError, ConfigError, OutputFileError, RecordError
58
59
  from ..utils import check_path, logger
59
60
 
60
61
 
@@ -276,14 +277,14 @@ class ExecutableConfig(TypedDict):
276
277
  custom_config: Optional[dict]
277
278
 
278
279
 
279
- class _ExecutableConfigRecord:
280
+ class ExecutableConfigRecord:
280
281
  """
281
282
  A class to helps store configs of various executables and exports them to a file.
282
283
  """
283
284
  _instance = None
284
285
  _initialized = False
285
286
 
286
- def __init__(self, save_path: Optional[str] = None, include_data=False):
287
+ def __init__(self, save_path: str, include_data=False):
287
288
  """
288
289
 
289
290
  :param save_path: Save path of the exported config file.
@@ -305,7 +306,6 @@ class _ExecutableConfigRecord:
305
306
 
306
307
  self.work_path = WRFRUNConfig.parse_resource_uri(WRFRUNConfig.WRFRUN_WORKSPACE_REPLAY)
307
308
  self.content_path = f"{self.work_path}/config_and_data"
308
- check_path(self.content_path)
309
309
 
310
310
  self._recorded_config = []
311
311
  self._name_count = {}
@@ -318,15 +318,20 @@ class _ExecutableConfigRecord:
318
318
 
319
319
  return cls._instance
320
320
 
321
- def reinit(self, save_path: Optional[str] = None, include_data=False):
321
+ @classmethod
322
+ def reinit(cls, save_path: str, include_data=False):
322
323
  """
323
324
  Reinitialize this instance.
324
325
 
326
+ :param save_path: Save path of the exported config file.
327
+ :type save_path: str
328
+ :param include_data: If includes input data.
329
+ :type include_data: bool
325
330
  :return: New instance.
326
- :rtype: _ExecutableConfigRecord
331
+ :rtype: ExecutableConfigRecord
327
332
  """
328
- self._initialized = False
329
- return _ExecutableConfigRecord(save_path, include_data)
333
+ cls._initialized = False
334
+ return cls(save_path, include_data)
330
335
 
331
336
  def record(self, exported_config: ExecutableConfig):
332
337
  """
@@ -417,7 +422,25 @@ class _ExecutableConfigRecord:
417
422
  logger.info(f"Replay config exported to {self.save_path}")
418
423
 
419
424
 
420
- ExecConfigRecorder = _ExecutableConfigRecord()
425
+ ExecConfigRecorder: Optional[ExecutableConfigRecord] = None
426
+
427
+
428
+ def create_recorder(save_path: str, include_data=False) -> ExecutableConfigRecord:
429
+ """
430
+ Create a recorder to record simulations.
431
+
432
+ :param save_path: Save path of the exported config file.
433
+ :type save_path: str
434
+ :param include_data: If includes input data.
435
+ :type include_data: bool
436
+ :return: Recorder instance.
437
+ :rtype: ExecutableConfigRecord
438
+ """
439
+ global ExecConfigRecorder
440
+
441
+ ExecConfigRecorder = ExecutableConfigRecord.reinit(save_path, include_data)
442
+
443
+ return ExecConfigRecorder
421
444
 
422
445
 
423
446
  class ExecutableBase:
@@ -588,25 +611,27 @@ class ExecutableBase:
588
611
 
589
612
  You can give more information with a ``FileConfigDict``.
590
613
 
614
+ >>> from wrfrun.workspace.wrf import get_wrf_workspace_path
591
615
  >>> file_dict: FileConfigDict = {
592
616
  ... "file_path": "data/custom_file.nc",
593
- ... "save_path": f"{WRFRUNConfig.WPS_WORK_PATH}",
617
+ ... "save_path": get_wrf_workspace_path("wps"),
594
618
  ... "save_name": "custom_file.nc",
595
619
  ... "is_data": True,
596
620
  ... "is_output": False
597
621
  ... }
598
622
  >>> self.add_input_files(file_dict)
599
623
 
624
+ >>> from wrfrun.workspace.wrf import get_wrf_workspace_path
600
625
  >>> file_dict_1: FileConfigDict = {
601
626
  ... "file_path": "data/custom_file",
602
- ... "save_path": f"{WRFRUNConfig.WPS_WORK_PATH}/geogrid",
627
+ ... "save_path": f"{get_wrf_workspace_path('wps')}/geogrid",
603
628
  ... "save_name": "GEOGRID.TBL",
604
629
  ... "is_data": False,
605
630
  ... "is_output": False
606
631
  ... }
607
632
  >>> file_dict_2: FileConfigDict = {
608
633
  ... "file_path": "data/custom_file",
609
- ... "save_path": f"{WRFRUNConfig.WPS_WORK_PATH}/outputs",
634
+ ... "save_path": f"{get_wrf_workspace_path('wps')}/outputs",
610
635
  ... "save_name": "test_file",
611
636
  ... "is_data": True,
612
637
  ... "is_output": True
@@ -845,7 +870,12 @@ class ExecutableBase:
845
870
  self.after_exec()
846
871
 
847
872
  if not WRFRUNConfig.IS_IN_REPLAY and WRFRUNConfig.IS_RECORDING:
873
+ global ExecConfigRecorder
874
+ if ExecConfigRecorder is None:
875
+ logger.error(f"Trying to record simulation before create the recorder.")
876
+ raise RecordError(f"Trying to record simulation before create the recorder.")
877
+
848
878
  ExecConfigRecorder.record(self.export_config())
849
879
 
850
880
 
851
- __all__ = ["ExecutableBase", "FileConfigDict", "InputFileType", "ExecutableConfig", "ExecutableClassConfig", "ExecConfigRecorder"]
881
+ __all__ = ["ExecutableBase", "FileConfigDict", "InputFileType", "ExecutableConfig", "ExecutableClassConfig", "ExecConfigRecorder", "create_recorder"]
wrfrun/core/config.py CHANGED
@@ -11,6 +11,9 @@ All classes in this module is used to manage various configurations of ``wrfrun`
11
11
  _WRFRunConstants
12
12
  _WRFRunNamelist
13
13
  _WRFRunResources
14
+ init_wrfrun_config
15
+ get_wrfrun_config
16
+ set_register_func
14
17
 
15
18
  WRFRunConfig
16
19
  ************
@@ -20,18 +23,24 @@ It inherits from three classes: :class:`_WRFRunResources`, :class:`_WRFRunConsta
20
23
  Users can use the global variable ``WRFRUNConfig``, which is the instance of this class being created when users import ``wrfrun``.
21
24
  """
22
25
 
26
+ # TODO:
27
+ # 1. NEW FEATURE: Allow reading work directory from config file.
28
+ # 2. The first one will be a break change, fix the following errors.
29
+ # 3. The structure of wrfrun may need to be changed again.
30
+
31
+ import threading
23
32
  from copy import deepcopy
24
33
  from os import environ, makedirs
25
34
  from os.path import abspath, dirname, exists
26
35
  from shutil import copyfile
27
36
  from sys import platform
28
- from typing import Optional, Tuple, Union
37
+ from typing import Callable, Optional, Tuple, Union
29
38
 
30
39
  import f90nml
31
40
  import tomli
32
41
  import tomli_w
33
42
 
34
- from .error import ModelNameError, NamelistError, NamelistIDError, ResourceURIError, WRFRunContextError
43
+ from .error import ModelNameError, NamelistError, NamelistIDError, ResourceURIError, WRFRunContextError, ConfigError
35
44
  from ..utils import logger
36
45
 
37
46
 
@@ -140,38 +149,42 @@ class _WRFRunConstants:
140
149
  Define all variables that will be used by other components.
141
150
  """
142
151
 
143
- def __init__(self):
152
+ def __init__(self, work_dir: str):
144
153
  """
145
154
  Define all variables that will be used by other components.
146
155
 
147
156
  These variables are related to ``wrfrun`` installation environments, configuration files and more.
148
157
  They are defined either directly or mapped using URIs to ensure consistent access across all components.
158
+
159
+ :param work_dir: ``wrfrun`` work directory path.
160
+ :type work_dir: str
149
161
  """
150
162
  # check system
151
163
  if platform != "linux":
152
164
  logger.debug(f"Not Linux system!")
153
165
 
166
+ if work_dir != "" or platform != "linux":
154
167
  # set temporary dir path
155
- self._WRFRUN_TEMP_PATH = "./tmp/wrfrun"
156
- user_home_path = "./tmp/wrfrun"
168
+ self._WRFRUN_TEMP_PATH = abspath(f"{work_dir}/tmp")
169
+ self._WRFRUN_HOME_PATH = abspath(work_dir)
157
170
 
158
171
  else:
159
-
160
172
  # the path we may need to store temp files,
161
173
  # don't worry, it will be deleted once the system reboots
162
174
  self._WRFRUN_TEMP_PATH = "/tmp/wrfrun"
163
175
  user_home_path = f"{environ['HOME']}"
164
176
 
165
- # WRF may need a large disk space to store output, we can't run wrf in /tmp,
166
- # so we will create a folder in $HOME/.config to run wrf.
167
- # we need to check if we're running as a root user
168
- if user_home_path in ["/", "/root", ""]:
169
- logger.warning(f"User's home path is '{user_home_path}', which means you are running this program as a root user")
170
- logger.warning("It's not recommended to use wrfrun as a root user")
171
- logger.warning("Set user_home_path as /root")
172
- user_home_path = "/root"
177
+ # WRF may need a large disk space to store output, we can't run wrf in /tmp,
178
+ # so we will create a folder in $HOME/.config to run wrf.
179
+ # we need to check if we're running as a root user
180
+ if user_home_path in ["/", "/root", ""]:
181
+ logger.warning(f"User's home path is '{user_home_path}', which means you are running this program as a root user")
182
+ logger.warning("It's not recommended to use wrfrun as a root user")
183
+ logger.warning("Set user_home_path as /root")
184
+ user_home_path = "/root"
185
+
186
+ self._WRFRUN_HOME_PATH = f"{user_home_path}/.config/wrfrun"
173
187
 
174
- self._WRFRUN_HOME_PATH = f"{user_home_path}/.config/wrfrun"
175
188
  # workspace root path
176
189
  self._WRFRUN_WORKSPACE_ROOT = f"{self._WRFRUN_HOME_PATH}/workspace"
177
190
  self._WRFRUN_WORKSPACE_MODEL = f"{self._WRFRUN_WORKSPACE_ROOT}/model"
@@ -534,21 +547,25 @@ class _WRFRunNamelist:
534
547
  :return: ``True`` if it is registered and loaded, else ``False``.
535
548
  :rtype: bool
536
549
  """
537
- if namelist_id in self._namelist_id_list and self._namelist_dict:
550
+ if namelist_id in self._namelist_id_list and namelist_id in self._namelist_dict:
538
551
  return True
539
552
 
540
553
  else:
541
554
  return False
542
555
 
543
556
 
557
+ _URI_REGISTER_FUNC_LIST: list[Callable[["WRFRunConfig"], None]] = []
558
+
559
+
544
560
  class WRFRunConfig(_WRFRunConstants, _WRFRunNamelist, _WRFRunResources):
545
561
  """
546
562
  Class to manage wrfrun config, runtime constants, namelists and resource files.
547
563
  """
548
564
  _instance = None
549
565
  _initialized = False
566
+ _lock = threading.Lock()
550
567
 
551
- def __init__(self):
568
+ def __init__(self, work_dir: str):
552
569
  """
553
570
  This class provides various interfaces to access ``wrfrun``'s config, namelist values of NWP models,
554
571
  runtime constants and resource files by inheriting from:
@@ -556,23 +573,32 @@ class WRFRunConfig(_WRFRunConstants, _WRFRunNamelist, _WRFRunResources):
556
573
 
557
574
  An instance of this class called ``WRFRUNConfig`` will be created after the user import ``wrfrun``,
558
575
  and you should use the instance to access configs or other things instead of creating another instance.
576
+
577
+ :param work_dir: ``wrfrun`` work directory path.
578
+ :type work_dir: str
559
579
  """
560
580
  if self._initialized:
561
581
  return
562
582
 
563
- _WRFRunConstants.__init__(self)
564
- _WRFRunNamelist.__init__(self)
565
- _WRFRunResources.__init__(self)
583
+ with self._lock:
584
+ global _URI_REGISTER_FUNC_LIST
566
585
 
567
- self._config = {}
586
+ self._initialized = True
568
587
 
569
- # register uri for wrfrun constants
570
- for key, value in self._get_uri_map().items():
571
- self.register_resource_uri(key, value)
588
+ _WRFRunConstants.__init__(self, work_dir)
589
+ _WRFRunNamelist.__init__(self)
590
+ _WRFRunResources.__init__(self)
591
+
592
+ self._config = {}
593
+
594
+ self._config_template_file_path = None
595
+
596
+ self._register_wrfrun_uris()
572
597
 
573
- self._config_template_file_path = None
598
+ for _fun in _URI_REGISTER_FUNC_LIST:
599
+ _fun(self)
574
600
 
575
- self._initialized = True
601
+ _URI_REGISTER_FUNC_LIST = []
576
602
 
577
603
  def __new__(cls, *args, **kwargs):
578
604
  if cls._instance is None:
@@ -580,6 +606,39 @@ class WRFRunConfig(_WRFRunConstants, _WRFRunNamelist, _WRFRunResources):
580
606
 
581
607
  return cls._instance
582
608
 
609
+ def __getattribute__(self, item):
610
+ if item not in ("_initialized", "_instance", "_lock", "__class__"):
611
+ if not object.__getattribute__(self, "_initialized"):
612
+ logger.error(f"`WRFRUNConfig` hasn't been initialized.")
613
+ logger.error(f"Use `WRFRun` to load config automatically, or use function `init_wrfrun_config` to load config manually.")
614
+ raise ConfigError(f"`WRFRUNConfig` hasn't been initialized.")
615
+
616
+ return object.__getattribute__(self, item)
617
+
618
+ @classmethod
619
+ def from_config_file(cls, config_file: str) -> "WRFRunConfig":
620
+ """
621
+ Read the config file and reinitialize.
622
+
623
+ :param config_file: Config file path.
624
+ :type config_file: str
625
+ :return: New instance
626
+ :rtype: WRFRunConfig
627
+ """
628
+ cls._initialized = False
629
+
630
+ with open(config_file, "rb") as f:
631
+ config = tomli.load(f)
632
+
633
+ instance = cls(work_dir=config["work_dir"])
634
+ instance.load_wrfrun_config(config_file)
635
+
636
+ return instance
637
+
638
+ def _register_wrfrun_uris(self):
639
+ for key, value in self._get_uri_map().items():
640
+ self.register_resource_uri(key, value)
641
+
583
642
  def set_config_template_path(self, file_path: str):
584
643
  """
585
644
  Set file path of the config template file.
@@ -652,6 +711,11 @@ class WRFRunConfig(_WRFRunConstants, _WRFRunNamelist, _WRFRunResources):
652
711
  output_path = abspath(self["output_path"])
653
712
  self.register_resource_uri(self.WRFRUN_OUTPUT_PATH, output_path)
654
713
 
714
+ # some additional check
715
+ if self._config["input_data_path"] == "":
716
+ logger.warning("It seems you forget to set 'input_data_path', set it to 'data'.")
717
+ self._config["input_data_path"] = "data"
718
+
655
719
  def save_wrfrun_config(self, save_path: str):
656
720
  """
657
721
  Save ``wrfrun``'s config to a file.
@@ -685,6 +749,18 @@ class WRFRunConfig(_WRFRunConstants, _WRFRunNamelist, _WRFRunResources):
685
749
 
686
750
  return deepcopy(self._config[item])
687
751
 
752
+ def __setitem__(self, key: str, value):
753
+ if key == "model":
754
+ logger.error(f"Use `update_model_config` to change model configurations.")
755
+ raise KeyError(f"Use `update_model_config` to change model configurations.")
756
+
757
+ if key in self._config:
758
+ self._config[key] = value
759
+
760
+ else:
761
+ logger.error(f"Can't find key '{key}' in your config.")
762
+ raise KeyError(f"Can't find key '{key}' in your config.")
763
+
688
764
  def get_input_data_path(self) -> str:
689
765
  """
690
766
  Get the path of directory in which stores the input data.
@@ -711,6 +787,23 @@ class WRFRunConfig(_WRFRunConstants, _WRFRunNamelist, _WRFRunResources):
711
787
 
712
788
  return deepcopy(self["model"][model_name])
713
789
 
790
+ def update_model_config(self, model_name: str, value: dict):
791
+ """
792
+ Update the config of a NWP model.
793
+
794
+ An exception :class:`ModelNameError` will be raised if the config can't be found.
795
+
796
+ :param model_name: Name of the model. For example, ``wrf``.
797
+ :type model_name: str
798
+ :param value: Dictionary contains new values.
799
+ :type value: dict
800
+ """
801
+ if model_name not in self["model"]:
802
+ logger.error(f"Config of model '{model_name}' isn't found in your config file.")
803
+ raise ModelNameError(f"Config of model '{model_name}' isn't found in your config file.")
804
+
805
+ self["model"][model_name] = self["model"][model_name] | value
806
+
714
807
  def get_log_path(self) -> str:
715
808
  """
716
809
  Get the directory path to save logs.
@@ -762,6 +855,69 @@ class WRFRunConfig(_WRFRunConstants, _WRFRunNamelist, _WRFRunResources):
762
855
  super().write_namelist(save_path, namelist_id, overwrite)
763
856
 
764
857
 
765
- WRFRUNConfig = WRFRunConfig()
858
+ WRFRUNConfig = WRFRunConfig.__new__(WRFRunConfig)
859
+
860
+
861
+ def set_register_func(func: Callable[["WRFRunConfig"], None]):
862
+ """
863
+ Set the function to register URIs.
864
+
865
+ These functions should accept ``WRFRUNConfig`` instance,
866
+ and will be called when initializing ``WRFRUNConfig``.
867
+
868
+ If ``WRFRUNConfig`` has been initialized, ``func`` will be called immediately.
869
+
870
+ Normal users should use :meth:`WRFRunConfig.register_resource_uri`,
871
+ because ``WRFRUNConfig`` should (and must) be initialized.
872
+
873
+ :param func: Functions to register URIs.
874
+ :type func: Callable
875
+ """
876
+ global _URI_REGISTER_FUNC_LIST
877
+
878
+ if object.__getattribute__(WRFRUNConfig, "_initialized"):
879
+ func(WRFRUNConfig)
880
+
881
+ else:
882
+ if func not in _URI_REGISTER_FUNC_LIST:
883
+ _URI_REGISTER_FUNC_LIST.append(func)
884
+
885
+
886
+ def init_wrfrun_config(config_file: str) -> WRFRunConfig:
887
+ """
888
+ Initialize ``WRFRUNConfig`` with the given config file.
889
+
890
+ :param config_file: Config file path.
891
+ :type config_file: str
892
+ :return: ``WRFRUNConfig`` instance.
893
+ :rtype: WRFRunConfig
894
+ """
895
+ global WRFRUNConfig
896
+
897
+ logger.info(f"Initialize `WRFRUNConfig` with config: {config_file}")
898
+
899
+ WRFRUNConfig = WRFRunConfig.from_config_file(config_file)
900
+
901
+ return WRFRUNConfig
902
+
903
+
904
+ def get_wrfrun_config() -> WRFRunConfig:
905
+ """
906
+ Get ``WRFRUNConfig`` instance.
907
+
908
+ An exception :class:`ConfigError` will be raised if you haven't initialized it.
909
+
910
+ :return: ``WRFRUNConfig`` instance.
911
+ :rtype: WRFRunConfig
912
+ """
913
+ global WRFRUNConfig
914
+
915
+ if WRFRUNConfig is None:
916
+ logger.error(f"`WRFRUNConfig` hasn't been initialized.")
917
+ logger.error(f"Use `WRFRun` to load config automatically, or use function `init_wrfrun_config` to load config manually.")
918
+ raise ConfigError(f"`WRFRUNConfig` hasn't been initialized.")
919
+
920
+ return WRFRUNConfig
921
+
766
922
 
767
- __all__ = ["WRFRUNConfig"]
923
+ __all__ = ["WRFRunConfig", "WRFRUNConfig", "init_wrfrun_config", "get_wrfrun_config", "set_register_func"]
wrfrun/core/error.py CHANGED
@@ -106,5 +106,12 @@ class ModelNameError(WRFRunBasicError):
106
106
  pass
107
107
 
108
108
 
109
+ class RecordError(WRFRunBasicError):
110
+ """
111
+ Exception indicates ``wrfrun`` can't record simulations.
112
+ """
113
+ pass
114
+
115
+
109
116
  __all__ = ["WRFRunBasicError", "ConfigError", "WRFRunContextError", "CommandError", "OutputFileError", "ResourceURIError", "InputFileError",
110
- "NamelistError", "ExecRegisterError", "GetExecClassError", "ModelNameError", "NamelistIDError"]
117
+ "NamelistError", "ExecRegisterError", "GetExecClassError", "ModelNameError", "NamelistIDError", "RecordError"]
wrfrun/core/server.py CHANGED
@@ -8,6 +8,7 @@ In order to report the progress to user, ``wrfrun`` provides :class:`WRFRunServe
8
8
  .. autosummary::
9
9
  :toctree: generated/
10
10
 
11
+ set_log_parse_func
11
12
  WRFRunServer
12
13
  WRFRunServerHandler
13
14
  stop_server
@@ -15,6 +16,7 @@ In order to report the progress to user, ``wrfrun`` provides :class:`WRFRunServe
15
16
 
16
17
  import socket
17
18
  import socketserver
19
+ import threading
18
20
  from collections.abc import Callable
19
21
  from datetime import datetime
20
22
  from json import dumps
@@ -28,6 +30,24 @@ WRFRUN_SERVER_INSTANCE = None
28
30
  WRFRUN_SERVER_THREAD = None
29
31
 
30
32
 
33
+ SET_LOG_PARSER_LOCK = threading.Lock()
34
+ LOG_PARSER: Callable[[datetime], int] | None = None
35
+
36
+
37
+ def set_log_parse_func(func: Callable[[datetime], int]):
38
+ """
39
+ Set log parse function used by socket server.
40
+
41
+ :param func: Function used to get simulated seconds from model's log file.
42
+ If the function can't parse the simulated seconds, it should return ``-1``.
43
+ :type func: Callable[[datetime], int]
44
+ """
45
+ global SET_LOG_PARSER_LOCK, LOG_PARSER
46
+
47
+ with SET_LOG_PARSER_LOCK:
48
+ LOG_PARSER = func
49
+
50
+
31
51
  class WRFRunServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
32
52
  """
33
53
  A socket server to report time usage.
@@ -155,7 +175,7 @@ class WRFRunServerHandler(socketserver.StreamRequestHandler):
155
175
  ``status`` represents work status,
156
176
  ``progress`` represents simulation progress of the status in percentage.
157
177
  """
158
- def __init__(self, request, client_address, server: WRFRunServer, log_parse_func: Callable[[datetime], int] | None) -> None:
178
+ def __init__(self, request, client_address, server: WRFRunServer) -> None:
159
179
  """
160
180
  :class:`WRFRunServer` handler.
161
181
 
@@ -165,15 +185,11 @@ class WRFRunServerHandler(socketserver.StreamRequestHandler):
165
185
  :type client_address:
166
186
  :param server: :class:`WRFRunServer` instance.
167
187
  :type server: WRFRunServer
168
- :param log_parse_func: Function used to get simulated seconds from model's log file.
169
- If the function can't parse the simulated seconds, it should return ``-1``.
170
- :type log_parse_func: Callable[[datetime], int]
171
188
  """
172
189
  super().__init__(request, client_address, server)
173
190
 
174
191
  # get server
175
192
  self.server: WRFRunServer = server
176
- self.log_parse_func = log_parse_func
177
193
 
178
194
  def calculate_time_usage(self) -> int:
179
195
  """
@@ -202,10 +218,11 @@ class WRFRunServerHandler(socketserver.StreamRequestHandler):
202
218
  """
203
219
  start_date, simulate_seconds = self.server.get_model_simulate_settings()
204
220
 
205
- if self.log_parse_func is None:
206
- simulated_seconds = -1
207
- else:
208
- simulated_seconds = self.log_parse_func(start_date)
221
+ with SET_LOG_PARSER_LOCK:
222
+ if LOG_PARSER is None:
223
+ simulated_seconds = -1
224
+ else:
225
+ simulated_seconds = LOG_PARSER(start_date)
209
226
 
210
227
  if simulated_seconds > 0:
211
228
  progress = simulated_seconds * 100 // simulate_seconds
@@ -269,4 +286,4 @@ def stop_server(socket_ip: str, socket_port: int):
269
286
  logger.warning("Fail to stop WRFRunServer, maybe it doesn't start at all.")
270
287
 
271
288
 
272
- __all__ = ["WRFRunServer", "WRFRunServerHandler", "stop_server"]
289
+ __all__ = ["WRFRunServer", "WRFRunServerHandler", "stop_server", "set_log_parse_func"]