psr-factory 5.0.0b32__py3-none-win_amd64.whl → 5.0.0b36__py3-none-win_amd64.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 psr-factory might be problematic. Click here for more details.

psr/cloud/aws.py CHANGED
@@ -5,6 +5,7 @@ from typing import Dict, List, Optional
5
5
 
6
6
  import boto3
7
7
  from botocore.exceptions import ClientError
8
+ from tqdm import tqdm
8
9
 
9
10
 
10
11
  class AWS:
@@ -14,7 +15,6 @@ class AWS:
14
15
  secret: str,
15
16
  session_token: str,
16
17
  url: str,
17
- bucket_name: str,
18
18
  Logger=None,
19
19
  ):
20
20
  self.s3_client = boto3.client(
@@ -24,7 +24,6 @@ class AWS:
24
24
  aws_session_token=session_token,
25
25
  region_name=AWS.get_region(url),
26
26
  )
27
- self.bucket_name = bucket_name
28
27
  self.logger = Logger
29
28
 
30
29
  @staticmethod
@@ -38,15 +37,21 @@ class AWS:
38
37
  def upload_file(
39
38
  self,
40
39
  file_path: str,
40
+ bucket_name: str,
41
41
  object_name: Optional[str] = None,
42
42
  extra_args: Optional[dict] = None,
43
+ Callback=None,
43
44
  ) -> bool:
44
45
  """Upload a file to an S3 bucket using the AWS instance's S3 client."""
45
46
  if object_name is None:
46
47
  object_name = os.path.basename(file_path)
47
48
  try:
48
49
  self.s3_client.upload_file(
49
- file_path, self.bucket_name, object_name, ExtraArgs=extra_args
50
+ file_path,
51
+ bucket_name,
52
+ object_name,
53
+ ExtraArgs=extra_args,
54
+ Callback=Callback,
50
55
  )
51
56
  return True
52
57
  except ClientError as e:
@@ -57,7 +62,7 @@ class AWS:
57
62
  self,
58
63
  files: List[str],
59
64
  repository_id: str,
60
- cluster_name: str,
65
+ bucket_name: str,
61
66
  checksums: Optional[Dict[str, str]] = None,
62
67
  zip_compress: bool = False,
63
68
  compress_zip_name: str = None,
@@ -80,7 +85,7 @@ class AWS:
80
85
  suffix=".zip", delete=False
81
86
  ) as tmp_zip_file:
82
87
  zip_path = tmp_zip_file.name
83
- tmp_zip_file.close() # Close the file handle so zipfile can open it
88
+ tmp_zip_file.close()
84
89
 
85
90
  try:
86
91
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
@@ -90,9 +95,11 @@ class AWS:
90
95
  object_name = f"{repository_id}/uploaded/{compress_zip_name}.zip"
91
96
  extra_args = {"Metadata": base_metadata.copy()}
92
97
 
93
- if not self.upload_file(zip_path, object_name, extra_args=extra_args):
98
+ if not self.upload_file(
99
+ zip_path, bucket_name, object_name, extra_args=extra_args
100
+ ):
94
101
  raise ValueError(
95
- f"Failed to upload zip file {zip_path} to S3 bucket {self.bucket_name}."
102
+ f"Failed to upload zip file {zip_path} to S3 bucket {bucket_name}."
96
103
  )
97
104
  finally:
98
105
  if os.path.exists(zip_path):
@@ -110,7 +117,7 @@ class AWS:
110
117
 
111
118
  if not self.upload_file(file_path, object_name, extra_args=extra_args):
112
119
  raise ValueError(
113
- f"Failed to upload file {file_path} to S3 bucket {self.bucket_name}."
120
+ f"Failed to upload file {file_path} to S3 bucket {bucket_name}."
114
121
  )
115
122
 
116
123
  # Always upload .metadata files if the source 'files' list is provided
@@ -136,15 +143,74 @@ class AWS:
136
143
  extra_args=extra_args,
137
144
  ):
138
145
  raise ValueError(
139
- f"Failed to upload metadata file {local_metadata_file_path} to S3 bucket {self.bucket_name}."
146
+ f"Failed to upload metadata file {local_metadata_file_path} to S3 bucket {bucket_name}."
140
147
  )
141
148
 
142
- def download_file(self, s3_object_key: str, local_file_path: str) -> bool:
143
- """Downloads a single object from S3 to a local file path."""
149
+ def upload_version(
150
+ self,
151
+ model_name: str,
152
+ version_name: str,
153
+ bucket_name: str,
154
+ file_path: str,
155
+ ):
156
+ """
157
+ Uploads a new version of the model to S3.
158
+ """
159
+ build = os.path.basename(file_path)
160
+ object_name = f"{model_name}/{version_name}/linux/{build}"
161
+ file_size = os.path.getsize(file_path)
162
+ custom_format = (
163
+ "{desc} [{percentage:3.0f}%] |{bar}| {n_fmt}/{total_fmt} @ {rate_fmt}"
164
+ )
165
+ with tqdm(
166
+ bar_format=custom_format,
167
+ total=file_size,
168
+ unit="B",
169
+ unit_scale=True,
170
+ desc=f"Uploading {build}...",
171
+ ) as pbar:
172
+ if not self.upload_file(
173
+ file_path,
174
+ bucket_name,
175
+ object_name,
176
+ Callback=lambda bytes_transferred: pbar.update(bytes_transferred),
177
+ ):
178
+ raise ValueError(
179
+ f"Failed to upload file {file_path} to S3 bucket {bucket_name}."
180
+ )
181
+
182
+ # Delete all objects in the "latest" folder
183
+ latest_prefix = f"{model_name}/{version_name}/linux/latest/"
184
+ try:
185
+ response = self.s3_client.list_objects_v2(
186
+ Bucket=bucket_name, Prefix=latest_prefix
187
+ )
188
+ if "Contents" in response:
189
+ for obj in response["Contents"]:
190
+ self.s3_client.delete_object(Bucket=bucket_name, Key=obj["Key"])
191
+ except ClientError as e:
192
+ self.logger.error(f"Error deleting objects in 'latest' folder: {e}")
193
+ raise RuntimeError(f"Failed to clean 'latest' folder in S3: {e}")
194
+
195
+ # Copy the uploaded version file to the "latest" folder
196
+ latest_object_name = f"{model_name}/{version_name}/linux/latest/{build}"
197
+ copy_source = {"Bucket": bucket_name, "Key": object_name}
144
198
  try:
145
- self.s3_client.download_file(
146
- self.bucket_name, s3_object_key, local_file_path
199
+ self.s3_client.copy_object(
200
+ Bucket=bucket_name, CopySource=copy_source, Key=latest_object_name
147
201
  )
202
+ except ClientError as e:
203
+ self.logger.error(f"Error copying version file to 'latest' folder: {e}")
204
+ raise RuntimeError(
205
+ f"Failed to copy version file to 'latest' folder in S3: {e}"
206
+ )
207
+
208
+ def download_file(
209
+ self, bucket_name: str, s3_object_key: str, local_file_path: str
210
+ ) -> bool:
211
+ """Downloads a single object from S3 to a local file path."""
212
+ try:
213
+ self.s3_client.download_file(bucket_name, s3_object_key, local_file_path)
148
214
  return True
149
215
  except ClientError as e:
150
216
  self.logger.error(f"ERROR: Failed to download {s3_object_key} from S3: {e}")
@@ -154,6 +220,7 @@ class AWS:
154
220
  self,
155
221
  repository_id: str,
156
222
  cluster_name: str,
223
+ bucket_name: str,
157
224
  output_path: str,
158
225
  file_list: List[str],
159
226
  ) -> List[str]:
@@ -170,7 +237,7 @@ class AWS:
170
237
  self.logger.info(
171
238
  f"Downloading {s3_object_key} to {local_file_path}"
172
239
  )
173
- if self.download_file(s3_object_key, local_file_path):
240
+ if self.download_file(bucket_name, s3_object_key, local_file_path):
174
241
  downloaded_files.append(os.path.basename(local_file_path))
175
242
  except ClientError as e:
176
243
  self.logger.error(f"ERROR: S3 ClientError during download: {e}")
@@ -182,3 +249,23 @@ class AWS:
182
249
  raise RuntimeError(f"An unexpected error occurred during S3 download: {e}")
183
250
 
184
251
  return downloaded_files
252
+
253
+ def check_version_build_exists(
254
+ self,
255
+ bucket_name: str,
256
+ model_name: str,
257
+ version_name: str,
258
+ build_id: str,
259
+ ) -> bool:
260
+ """
261
+ Checks if a specific version build exists in the S3 bucket.
262
+ """
263
+ object_key = f"{model_name}/{version_name}/linux/{build_id}.zip"
264
+ try:
265
+ self.s3_client.head_object(Bucket=bucket_name, Key=object_key)
266
+ return True
267
+ except ClientError as e:
268
+ if e.response["Error"]["Code"] == "404":
269
+ return False
270
+ self.logger.error(f"Error checking version build existence: {e}")
271
+ raise RuntimeError(f"Failed to check version build existence in S3: {e}")
psr/cloud/cloud.py CHANGED
@@ -618,7 +618,6 @@ class Client:
618
618
  "nproc": case.number_of_processes,
619
619
  "repositorioId": "0",
620
620
  "instanciaTipo": instance_type_id,
621
- "validacaoModelo": "True",
622
621
  "validacaoUsuario": "False",
623
622
  "idVersao": case.program_version,
624
623
  "modeloVersao": case.program_version_name,
@@ -810,9 +809,12 @@ class Client:
810
809
  f.write("")
811
810
  else:
812
811
  # Download results using Console
813
- xml_content = create_case_xml(parameters)
814
- self._run_console(xml_content)
815
- self._logger.info(f"Results downloaded to {output_path}")
812
+ try:
813
+ xml_content = create_case_xml(parameters)
814
+ self._run_console(xml_content)
815
+ self._logger.info(f"Results downloaded to {output_path}")
816
+ except Exception as e:
817
+ self._logger.error(f"Error downloading results: {e}")
816
818
 
817
819
  def cancel_case(self, case_id: int, wait: bool = False) -> bool:
818
820
  parameters = {
@@ -848,42 +850,59 @@ class Client:
848
850
  return True
849
851
 
850
852
  def _cases_from_xml(self, xml: ET.Element) -> List["Case"]:
853
+ def get_attribute(fila, key, type_func, default=None, format_str=None):
854
+ value = fila.attrib.get(key)
855
+ try:
856
+ if value is None:
857
+ return default
858
+ if format_str and type_func == datetime.strptime:
859
+ return datetime.strptime(value, format_str)
860
+ return type_func(value)
861
+ except Exception as e:
862
+ if default is None:
863
+ case_id = fila.attrib.get("repositorioId")
864
+ self._logger.error(
865
+ f"Error parsing field '{key}' with value '{value}' for case ID {case_id}: {e}"
866
+ )
867
+ return default
868
+
851
869
  instance_type_map = self._get_instance_type_map()
852
870
  cases = []
853
871
  for fila in xml.findall("Fila"):
854
872
  try:
855
873
  case = Case(
856
- name=fila.attrib.get("nomeCaso"),
874
+ name=get_attribute(fila, "nomeCaso", str),
857
875
  data_path=None,
858
- program=fila.attrib.get("programa"),
859
- program_version=int(fila.attrib.get("idVersao")),
860
- execution_type=int(fila.attrib.get("idTipoExecucao")),
861
- price_optimized=bool(fila.attrib.get("flagSpot")),
862
- number_of_processes=int(fila.attrib.get("numeroProcesso")),
863
- id=int(fila.attrib.get("repositorioId")),
864
- user=fila.attrib.get("usuario"),
865
- execution_date=datetime.strptime(
866
- fila.attrib.get("dataInicio"), "%d/%m/%Y %H:%M"
876
+ program=get_attribute(fila, "programa", str),
877
+ program_version=get_attribute(fila, "idVersao", int),
878
+ execution_type=get_attribute(fila, "idTipoExecucao", int),
879
+ price_optimized=get_attribute(fila, "flagSpot", bool),
880
+ number_of_processes=get_attribute(fila, "numeroProcesso", int),
881
+ id=get_attribute(fila, "repositorioId", int),
882
+ user=get_attribute(fila, "usuario", str),
883
+ execution_date=get_attribute(
884
+ fila,
885
+ "dataInicio",
886
+ datetime.strptime,
887
+ format_str="%d/%m/%Y %H:%M",
888
+ ),
889
+ parent_case_id=get_attribute(
890
+ fila, "repositorioPai", int, default=0
867
891
  ),
868
- parent_case_id=int(fila.attrib.get("repositorioPai"))
869
- if fila.attrib.get("repositorioPai")
870
- else 0,
871
892
  memory_per_process_ratio=(
872
- instance_type_map[int(fila.attrib.get("instanciaTipo"))][0]
873
- if fila.attrib.get("instanciaTipo") in instance_type_map
893
+ instance_type_map[get_attribute(fila, "instanciaTipo", int)][0]
894
+ if get_attribute(fila, "instanciaTipo", int)
895
+ in instance_type_map
874
896
  else min([value[0] for value in instance_type_map.values()])
875
897
  ),
876
- repository_duration=int(fila.attrib.get("duracaoRepositorio")),
877
- budget=fila.attrib.get("budget"),
898
+ repository_duration=get_attribute(fila, "duracaoRepositorio", int),
899
+ budget=get_attribute(fila, "budget", str),
878
900
  )
879
901
  cases.append(case)
880
- except (TypeError, ValueError):
881
- pass
882
- # case_id = fila.attrib.get("repositorioId")
883
- # Optionally, log the error or handle it as needed
884
- # self._logger.error(f"Error processing case with ID {case_id}: {e}")
902
+ except Exception as e:
903
+ case_id = fila.attrib.get("repositorioId")
904
+ self._logger.error(f"Error processing case with ID {case_id}: {e}")
885
905
 
886
- # sort cases by execution date desc
887
906
  cases.sort(key=lambda x: x.execution_date, reverse=True)
888
907
  return cases
889
908
 
@@ -979,6 +998,9 @@ class Client:
979
998
  if not self._python_client
980
999
  else "5.4.0",
981
1000
  }
1001
+ if service != "listarFila":
1002
+ parameters["cluster"] = self.cluster["name"]
1003
+
982
1004
  if additional_arguments:
983
1005
  parameters.update(additional_arguments)
984
1006
 
@@ -1023,6 +1045,16 @@ class Client:
1023
1045
  )
1024
1046
  return self._cloud_clusters_xml_cache
1025
1047
 
1048
+ def _get_upload_filter(self, parameters, category: str) -> str:
1049
+ filter_request_result = self._make_soap_request(
1050
+ "obterFiltros", additional_arguments=parameters
1051
+ )
1052
+ upload_filter = filter_request_result.find(
1053
+ f"./Parametro[@nome='{category}']"
1054
+ ).text
1055
+ upload_filter = "^[a-zA-Z0-9./_]*(" + upload_filter + ")$"
1056
+ return upload_filter
1057
+
1026
1058
  def _execute_case(self, case_dict) -> int:
1027
1059
  """
1028
1060
  Execute a case on the PSR Cloud.
@@ -1032,13 +1064,8 @@ class Client:
1032
1064
  case_dict["programa"] = case_dict["modelo"]
1033
1065
  case_dict["numeroProcessos"] = case_dict["nproc"]
1034
1066
  case_dict["versao_cliente"] = "5.4.0"
1035
- filter_request_result = self._make_soap_request(
1036
- "obterFiltros", additional_arguments=case_dict
1037
- )
1038
- upload_filter = filter_request_result.find(
1039
- "./Parametro[@nome='filtroUpload']"
1040
- ).text
1041
- upload_filter = "^[a-zA-Z0-9./_]*(" + upload_filter + ")$"
1067
+
1068
+ upload_filter = self._get_upload_filter(case_dict, category="filtroUpload")
1042
1069
 
1043
1070
  # Create Repository
1044
1071
  self._logger.info("Creating remote repository")
@@ -1081,7 +1108,9 @@ class Client:
1081
1108
  # Filtering files to upload
1082
1109
  self._logger.info("Checking list of files to send")
1083
1110
 
1084
- file_list = _filter_upload_files(case_dict["diretorioDados"], upload_filter)
1111
+ file_list = self._filter_upload_files(
1112
+ case_dict["diretorioDados"], upload_filter
1113
+ )
1085
1114
 
1086
1115
  if not file_list:
1087
1116
  self._logger.warning(
@@ -1265,12 +1294,28 @@ class Client:
1265
1294
  with open(file_path, "rb") as f_check:
1266
1295
  return f_check.read(2) == b"\x1f\x8b"
1267
1296
  except IOError:
1268
- # TODO: Replace print with proper logging
1269
- print(
1297
+ self._logger.warning(
1270
1298
  f"WARNING: Could not read {file_path} to check for gzip magic number."
1271
1299
  )
1272
1300
  return False
1273
1301
 
1302
+ def _filter_upload_files(self, directory: str, upload_filter: str) -> List[str]:
1303
+ """
1304
+ Filter files in a directory based on the upload filter.
1305
+ :param directory: Directory to filter files from.
1306
+ :param upload_filter: Regular expression filter for file names.
1307
+ :return: List of filtered file paths.
1308
+ """
1309
+ if not os.path.exists(directory):
1310
+ raise CloudInputError(f"Directory {directory} does not exist")
1311
+
1312
+ regex = re.compile(upload_filter)
1313
+ filtered_files = []
1314
+ for file in os.listdir(directory):
1315
+ if regex.match(file):
1316
+ filtered_files.append(os.path.join(directory, file))
1317
+ return filtered_files
1318
+
1274
1319
  def _decompress_gzipped_file(self, gzipped_file_path: str) -> str:
1275
1320
  """
1276
1321
  Decompresses a gzipped file.
@@ -1300,13 +1345,13 @@ class Client:
1300
1345
  os.rename(temp_decompressed_path, decompressed_target_path)
1301
1346
  return decompressed_target_path
1302
1347
  except (gzip.BadGzipFile, EOFError, IOError) as e:
1303
- print(
1348
+ self._logger.warning(
1304
1349
  f"ERROR: Failed to decompress {gzipped_file_path}: {e}. Original file kept."
1305
1350
  )
1306
1351
  except (
1307
1352
  Exception
1308
1353
  ) as e: # Catch other errors like permission issues during rename/remove
1309
- print(
1354
+ self._logger.warning(
1310
1355
  f"ERROR: Error during post-decompression file operations for {gzipped_file_path}: {e}. Original file kept."
1311
1356
  )
1312
1357
  finally:
@@ -1432,24 +1477,6 @@ def _budget_matches_list(budget_part: str, all_budgets: List[str]) -> List[str]:
1432
1477
  return [budget for budget in all_budgets if lowered_budget_part in budget.lower()]
1433
1478
 
1434
1479
 
1435
- def _filter_upload_files(directory: str, upload_filter: str) -> List[str]:
1436
- """
1437
- Filter files in a directory based on the upload filter.
1438
- :param directory: Directory to filter files from.
1439
- :param upload_filter: Regular expression filter for file names.
1440
- :return: List of filtered file paths.
1441
- """
1442
- if not os.path.exists(directory):
1443
- raise CloudInputError(f"Directory {directory} does not exist")
1444
-
1445
- regex = re.compile(upload_filter)
1446
- filtered_files = []
1447
- for file in os.listdir(directory):
1448
- if regex.match(file):
1449
- filtered_files.append(os.path.join(directory, file))
1450
- return filtered_files
1451
-
1452
-
1453
1480
  def replace_case_str_values(client: Client, case: Case) -> Case:
1454
1481
  """Create a new case object using internal integer IDs instead of string values."""
1455
1482
  # Model Version
psr/cloud/version.py CHANGED
@@ -2,4 +2,4 @@
2
2
  # Unauthorized copying of this file, via any medium is strictly prohibited
3
3
  # Proprietary and confidential
4
4
 
5
- __version__ = "0.3.9"
5
+ __version__ = "0.3.10"
psr/execqueue/config.py CHANGED
@@ -4,6 +4,7 @@ import tomllib
4
4
  __version__ = "0.3.0"
5
5
  _app_name = "PSR Factory ExecQueue"
6
6
  DEFAULT_PORT = 5000
7
+ DEFAULT_HOST = "127.0.0.1"
7
8
  FLASK_DEBUG = False
8
9
  _SETTINGS_FILE_PATH = "server_settings.toml"
9
10
 
psr/execqueue/server.py CHANGED
@@ -403,7 +403,7 @@ if __name__ == '__main__':
403
403
  print("Starting server...")
404
404
  session = initialize_db()
405
405
  try:
406
- app.run(debug=FLASK_DEBUG,
406
+ app.run(host=settings.get("host", DEFAULT_HOST), debug=FLASK_DEBUG,
407
407
  port=settings.get("port", DEFAULT_PORT),
408
408
  threaded=True,
409
409
  use_reloader=False,)
psr/execqueue/watcher.py CHANGED
@@ -12,7 +12,7 @@ SERVER_URL = os.getenv("SERVER_URL", "http://127.0.0.1:5000")
12
12
  WATCH_DIR = os.getenv("WATCH_DIR")
13
13
  PROCESSED_DIR = os.getenv("PROCESSED_DIR")
14
14
  RESULTS_DIR = os.getenv("RESULTS_DIR", "results")
15
- SLEEP_SECONDS = int(os.getenv("WATCHER_SLEEP", "30"))
15
+ SLEEP_SECONDS = int(os.getenv("WATCHER_SLEEP", "10"))
16
16
  DB_PATH = os.getenv("WATCHER_DB_PATH", "watcher.sqlite")
17
17
 
18
18
 
psr/factory/__init__.py CHANGED
@@ -2,6 +2,6 @@
2
2
  # Unauthorized copying of this file, via any medium is strictly prohibited
3
3
  # Proprietary and confidential
4
4
 
5
- __version__ = "5.0.0b32"
5
+ __version__ = "5.0.0b36"
6
6
 
7
7
  from .api import *
psr/factory/factory.dll CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: psr-factory
3
- Version: 5.0.0b32
3
+ Version: 5.0.0b36
4
4
  Summary: PSR database management module.
5
5
  Author-email: "PSR Inc." <psrfactory@psr-inc.com>
6
6
  License-Expression: MIT
@@ -33,6 +33,7 @@ Requires-Dist: filelock; extra == "cloud"
33
33
  Requires-Dist: pefile; extra == "cloud"
34
34
  Requires-Dist: boto3; extra == "cloud"
35
35
  Requires-Dist: botocore; extra == "cloud"
36
+ Requires-Dist: tqdm; extra == "cloud"
36
37
  Provides-Extra: execqueue-client
37
38
  Requires-Dist: requests; extra == "execqueue-client"
38
39
  Provides-Extra: execqueue-server
@@ -55,3 +56,69 @@ Requires-Dist: pefile; extra == "all"
55
56
  Requires-Dist: boto3; extra == "all"
56
57
  Requires-Dist: botocore; extra == "all"
57
58
  Dynamic: license-file
59
+
60
+ PSR Factory (version 4.0.40)
61
+ ============================
62
+
63
+ Factory is a library that helps to manage SDDP cases.
64
+ It contains functions that create, load, and save studies, and also functions that create,
65
+ access, and modify objects in a study.
66
+
67
+
68
+ Installation
69
+ ------------
70
+
71
+ ### System-wide installation
72
+
73
+ Open the command prompt and run the following command:
74
+
75
+ ```bash
76
+ pip install psr_factory-4.0.40-py3-none-win_amd64.whl
77
+ ```
78
+
79
+ Factory will be available to all Python scripts in your system after importing it:
80
+
81
+ ```python
82
+ import psr.factory
83
+ ```
84
+
85
+ ### Local/project-specific usage
86
+
87
+ Copy the folder `psr` and its contents to your project folder or a specific folder (e.g., `C:\path\to\factory`). Then, in your Python script, add the following lines:
88
+
89
+ ```python
90
+ import sys
91
+ sys.path.append(r"C:\path\to\factory")
92
+ import psr.factory
93
+ ```
94
+
95
+
96
+ Usage sample
97
+ ------------
98
+
99
+ ```python
100
+ import psr.factory
101
+
102
+ study = psr.factory.load_study(r"C:\temp\my\study")
103
+ system_1 = study.find("System.*")[0]
104
+
105
+ battery = psr.factory.create("Battery")
106
+ battery.code = 1
107
+ battery.name = "Battery 1"
108
+ battery.set("InstalledCapacity", 10.0)
109
+ battery.set("RefSystem", system_1)
110
+ study.add(battery)
111
+
112
+ study.save(r"C:\temp\my\updated_study")
113
+ ```
114
+
115
+
116
+ Full documentation
117
+ ------------------
118
+
119
+ The full documentation and reference is available at [https://docs.psr-inc.com/factory/](https://docs.psr-inc.com/manual/factory/).
120
+
121
+ Releases
122
+ --------
123
+
124
+ New releases can be found in the release notes at [https://psrenergy-docs.github.io/factory/releases.html](https://psrenergy-docs.github.io/factory/releases.html).
@@ -2,23 +2,23 @@ psr/apps/__init__.py,sha256=frSq1WIy5vIdU21xJIGX7U3XoAZRj0pcQmFb-R00b7I,228
2
2
  psr/apps/apps.py,sha256=V8Ewht7P1I-3sSkV3dnbxbLjF2slxPjcmtzmVaLjiNY,6746
3
3
  psr/apps/version.py,sha256=vs459L6JsatAkUxna7BNG-vMCaXpO1Ye8c1bmkEx4U4,194
4
4
  psr/cloud/__init__.py,sha256=inZMwG7O9Fca9hg1BhqYObOYtTTJOkpuTIuXnkHJZkI,246
5
- psr/cloud/aws.py,sha256=aq3yqDC_D1tplPICqa39pmMbyp_liRQ8B_Ubbl7q2Dw,7048
6
- psr/cloud/cloud.py,sha256=XnTYk5t27iTbbkfyQtD9gjIDUM2aNEly0-AHhaOpktw,60368
5
+ psr/cloud/aws.py,sha256=lpfaP2qUq5s7Gffb-k2jwyCP00BhvbB13y1nTTvM1dU,10249
6
+ psr/cloud/cloud.py,sha256=4NpJkVriVJHuBVDi8ubezcQ835vPfWTTbPpG_BBsDfE,61575
7
7
  psr/cloud/data.py,sha256=oDJyzcNsA7aAYi_qJKCUjCeGZvN-25E8KjZ-5RamNLE,4160
8
8
  psr/cloud/desktop.py,sha256=JFroCMEFV1Nz3has74n7OVrGCg2lS7Ev5bcjdw2hRxY,2980
9
9
  psr/cloud/log.py,sha256=Dvhz1enIWlFWeaRK7JAAuZVPfODgoEIRNcHEmbEliyQ,1366
10
10
  psr/cloud/status.py,sha256=vcI4B9S6wCt9maT5NNrVwYaEgGIvy6kkC1UVpJjYbtw,3607
11
11
  psr/cloud/tempfile.py,sha256=1IOeye0eKWnmBynK5K5FMWiTaEVhn4GbQ8_y0THEva0,3893
12
- psr/cloud/version.py,sha256=jwq5nQsan38iZF0lj5GFK7l9EIe4aSF1NzdcupAfHP4,192
12
+ psr/cloud/version.py,sha256=-oC5DNaS_iYTfpsusfmeG-WGktZ5b-x8xT-4zszWvz8,193
13
13
  psr/cloud/xml.py,sha256=ac2lyflOQm8khPvJn0zmI26I4sfUDY6A_OTsxzbMQEs,1896
14
14
  psr/execqueue/client.py,sha256=P89Yt76W2GqRXaG_MLsa0kXf0jPp-weBd3aSTRcDzcs,4443
15
- psr/execqueue/config.py,sha256=3KVwASOgRlymOSPeabotgBdLVB5sPKnPQ9og2q3LQfw,1418
15
+ psr/execqueue/config.py,sha256=rUOzO5dtTkwWoZlZfk06K9RE94xCx53T1bJ1h5JaDUo,1446
16
16
  psr/execqueue/db.py,sha256=sNr_StNEgZZQCKcyCWiB1WrQJIhE9UvLUxPA2tWiXGs,8498
17
- psr/execqueue/server.py,sha256=nW-Hi5zWHgPeLicASKJND7u6rz6eqwC16k91tUUQPxk,15741
18
- psr/execqueue/watcher.py,sha256=7dZZm9TiYVF7SdU0c_6Vq2_SZRobxgcspfBMzKFSsjQ,5637
19
- psr/factory/__init__.py,sha256=lYK_-FqnoMfNtG-BWVrfdHP7dQOFqGXl5GbHYv6ENJg,219
17
+ psr/execqueue/server.py,sha256=LolYERWRt96P_ip4yKU7DsN7M_n9d_pbflbT0ckUV0E,15782
18
+ psr/execqueue/watcher.py,sha256=R1dyXJ-OYn_QjqdItBwbLJZQ2LcbtdHqnRaYkyphi4w,5637
19
+ psr/factory/__init__.py,sha256=lItGdpqHejAKmFze9F9LK5wf2reJToeK9ZEKvke6WaE,219
20
20
  psr/factory/api.py,sha256=QASwrk5SbbAqz63u7EhGoBBqqXOqMnicjL-eJ3gOGe0,104270
21
- psr/factory/factory.dll,sha256=81JoLaZBp9AM1ZIwjLfft7xAau9Djh6s7fNRPQjP5-w,18362192
21
+ psr/factory/factory.dll,sha256=960_HytvI81_q4jM0wSyIkE8iosfPPOl96NHzP-rNDw,18362192
22
22
  psr/factory/factory.pmd,sha256=kr5xf2knYu_SJeyCsmoyYVgFwd4-VURi28rno40GIRY,250936
23
23
  psr/factory/factory.pmk,sha256=OvpqDnaCc1eeOWGQxogD0Nbg9M0PE1UZPcD65PeePV8,580337
24
24
  psr/factory/factorylib.py,sha256=o5Irbw6k-yIOJVUtDu2YYqw2x16P2LmCdouImwSssdw,28290
@@ -33,8 +33,8 @@ psr/psrfcommon/tempfile.py,sha256=5S13wa2DCLYTUdwbLm_KMBRnDRJ0WDlu8GO2BmZoNdg,39
33
33
  psr/runner/__init__.py,sha256=kI9HDX-B_LMQJUHHylFHas2rNpWfNNa0pZXoIvX_Alw,230
34
34
  psr/runner/runner.py,sha256=hCVH62HAZK_M9YUiHQgqCkMevN17utegjfRIw49MdvM,27542
35
35
  psr/runner/version.py,sha256=mch2Y8anSXGMn9w72Z78PhSRhOyn55EwaoLAYhY4McE,194
36
- psr_factory-5.0.0b32.dist-info/licenses/LICENSE.txt,sha256=N6mqZK2Ft3iXGHj-by_MHC_dJo9qwn0URjakEPys3H4,1089
37
- psr_factory-5.0.0b32.dist-info/METADATA,sha256=TyeuvA05pyZSMHbFxavpNmDAxyfVs5fednEN8zIhY2k,2333
38
- psr_factory-5.0.0b32.dist-info/WHEEL,sha256=ZjXRCNaQ9YSypEK2TE0LRB0sy2OVXSszb4Sx1XjM99k,97
39
- psr_factory-5.0.0b32.dist-info/top_level.txt,sha256=Jb393O96WQk3b5D1gMcrZBLKJJgZpzNjTPoldUi00ck,4
40
- psr_factory-5.0.0b32.dist-info/RECORD,,
36
+ psr_factory-5.0.0b36.dist-info/licenses/LICENSE.txt,sha256=N6mqZK2Ft3iXGHj-by_MHC_dJo9qwn0URjakEPys3H4,1089
37
+ psr_factory-5.0.0b36.dist-info/METADATA,sha256=rKr2yS66SWjgNP54XAv_ed7aIL83LeecoBO8TgIlPpk,3996
38
+ psr_factory-5.0.0b36.dist-info/WHEEL,sha256=ZjXRCNaQ9YSypEK2TE0LRB0sy2OVXSszb4Sx1XjM99k,97
39
+ psr_factory-5.0.0b36.dist-info/top_level.txt,sha256=Jb393O96WQk3b5D1gMcrZBLKJJgZpzNjTPoldUi00ck,4
40
+ psr_factory-5.0.0b36.dist-info/RECORD,,