psr-factory 4.1.0b10__py3-none-win_amd64.whl → 4.1.0b12__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.
psr/cloud/cloud.py CHANGED
@@ -4,10 +4,12 @@
4
4
 
5
5
  import copy
6
6
  import functools
7
+ import gzip
7
8
  import hashlib
8
9
  import logging
9
10
  import os
10
11
  import re
12
+ import shutil
11
13
  import subprocess
12
14
  import sys
13
15
  import warnings
@@ -21,6 +23,7 @@ import pefile
21
23
  import zeep
22
24
  from filelock import FileLock
23
25
 
26
+ from .aws import download_case_from_s3, upload_case_to_s3
24
27
  from .data import Case, CloudError, CloudInputError
25
28
  from .desktop import import_case
26
29
  from .log import enable_log_timestamp, get_logger
@@ -49,8 +52,12 @@ def thread_safe():
49
52
  return decorator
50
53
 
51
54
 
52
- def _md5sum(value: str) -> str:
53
- return hashlib.md5(value.encode("utf-8")).hexdigest() # nosec
55
+ def _md5sum(value: str, enconding=True) -> str:
56
+ if enconding:
57
+ return hashlib.md5(value.encode("utf-8")).hexdigest() # nosec
58
+ else:
59
+ # hash binary data
60
+ return hashlib.md5(value).hexdigest() # nosec
54
61
 
55
62
 
56
63
  def hash_password(password: str) -> str:
@@ -93,7 +100,7 @@ _CONSOLE_REL_PARENT_PATH = r"Oper\Console"
93
100
 
94
101
  _CONSOLE_APP = r"FakeConsole.exe"
95
102
 
96
- _ALLOWED_PROGRAMS = ["SDDP", "OPTGEN", "PSRIO", "GRAF"]
103
+ _ALLOWED_PROGRAMS = ["SDDP", "OPTGEN", "PSRIO", "GRAF", "MyModel"]
97
104
 
98
105
  if os.name == "nt":
99
106
  _PSRCLOUD_CREDENTIALS_PATH = os.path.expandvars(
@@ -134,6 +141,7 @@ class Client:
134
141
  self._debug_mode = kwargs.get("debug", False)
135
142
  self._dry_run = kwargs.get("dry_run", False)
136
143
  self._timeout = kwargs.get("timeout", None)
144
+ self._python_client = kwargs.get("python_client", False)
137
145
 
138
146
  # Client version
139
147
  self.application_version = kwargs.get("application_version", None)
@@ -151,6 +159,11 @@ class Client:
151
159
 
152
160
  self._logger.info(f"Client uid {log_id} initialized.")
153
161
 
162
+ if self._python_client:
163
+ self._logger.info(
164
+ "Using Python client for PSR Cloud. Some features may not be available."
165
+ )
166
+
154
167
  self._console_path_setup(**kwargs)
155
168
 
156
169
  self._credentials_setup(**kwargs)
@@ -491,6 +504,14 @@ class Client:
491
504
  # Replace partial with complete budget name
492
505
  case.budget = match_list[0]
493
506
 
507
+ # MyModel
508
+ if case.program == "MyModel":
509
+ if case.mymodel_program_files is None:
510
+ raise CloudInputError("MyModel program files not provided")
511
+
512
+ if case.program != "MyModel" and case.mymodel_program_files is not None:
513
+ msg = "Ignoring mymodel_program_files parameter for non MyModel case."
514
+ warnings.warn(msg)
494
515
  return case
495
516
 
496
517
  def _pre_process_graph(self, path: str, case_id: int) -> None:
@@ -605,6 +626,7 @@ class Client:
605
626
  "userTag": "(Untagged)",
606
627
  "lifecycle": case.repository_duration,
607
628
  "versaoInterface": interface_version,
629
+ "pathPrograma": case.mymodel_program_files,
608
630
  }
609
631
 
610
632
  if case.budget:
@@ -617,20 +639,23 @@ class Client:
617
639
  if self._dry_run:
618
640
  return xml_content
619
641
 
620
- self._run_console(xml_content)
621
- xml = ET.parse(
622
- f"{self._get_console_parent_path()}\\fake{case.program}_async.xml"
623
- )
624
- _check_for_errors(xml, self._logger)
625
- id_parameter = xml.find("./Parametro[@nome='repositorioId']")
626
- if id_parameter is None:
627
- xml_str = _xml_to_str(xml)
628
- raise CloudError(
629
- f"Case id not found on returned XML response.\n"
630
- f"Contact PSR support at psrcloud@psr-inc.com with following data:\n\n{xml_str}\n\n"
642
+ if self._python_client:
643
+ case_id = self._execute_case(parameters)
644
+ else:
645
+ self._run_console(xml_content)
646
+ xml = ET.parse(
647
+ f"{self._get_console_parent_path()}\\fake{case.program}_async.xml"
631
648
  )
649
+ _check_for_errors(xml, self._logger)
650
+ id_parameter = xml.find("./Parametro[@nome='repositorioId']")
651
+ if id_parameter is None:
652
+ xml_str = _xml_to_str(xml)
653
+ raise CloudError(
654
+ f"Case id not found on returned XML response.\n"
655
+ f"Contact PSR support at psrcloud@psr-inc.com with following data:\n\n{xml_str}\n\n"
656
+ )
632
657
 
633
- case_id = int(id_parameter.text)
658
+ case_id = int(id_parameter.text)
634
659
  if not wait:
635
660
  self._logger.info(f"Case {case.name} started with id {case_id}")
636
661
 
@@ -648,7 +673,6 @@ class Client:
648
673
  return case_id
649
674
 
650
675
  def get_status(self, case_id: int) -> tuple["ExecutionStatus", str]:
651
- # case = self.get_case(case_id)
652
676
  delete_xml = not self._debug_mode
653
677
  xml_content = ""
654
678
  with CreateTempFile(
@@ -666,10 +690,14 @@ class Client:
666
690
  "comando": "obterstatusresultados",
667
691
  "arquivoSaida": status_xml_path,
668
692
  }
669
- run_xml_content = create_case_xml(parameters)
670
693
 
671
- self._run_console(run_xml_content)
672
- xml = ET.parse(status_xml_path)
694
+ xml = None
695
+ if self._python_client:
696
+ xml = self._get_status_python(parameters)
697
+ else:
698
+ run_xml_content = create_case_xml(parameters)
699
+ self._run_console(run_xml_content)
700
+ xml = ET.parse(status_xml_path)
673
701
  parameter_status = xml.find("./Parametro[@nome='statusExecucao']")
674
702
  if parameter_status is None:
675
703
  xml_str = _xml_to_str(xml)
@@ -689,7 +717,7 @@ class Client:
689
717
  self._logger.info(f"Status: {STATUS_MAP_TEXT[status]}")
690
718
  return status, STATUS_MAP_TEXT[status]
691
719
 
692
- def list_download_files(self, case_id: int) -> List[str]:
720
+ def list_download_files(self, case_id: int) -> List[dict]:
693
721
  xml_files = self._make_soap_request(
694
722
  "prepararListaArquivosRemotaDownload",
695
723
  "listaArquivoRemota",
@@ -719,7 +747,7 @@ class Client:
719
747
  files: Optional[List[str]] = None,
720
748
  extensions: Optional[List[str]] = None,
721
749
  ) -> None:
722
- filter = ".*("
750
+ filter = ""
723
751
 
724
752
  if not extensions and not files:
725
753
  extensions = ["csv", "log", "hdr", "bin", "out", "ok"]
@@ -728,13 +756,12 @@ class Client:
728
756
 
729
757
  if extensions:
730
758
  Client._validate_extensions(extensions)
731
- filter_elements.extend([f".*.{ext}$" for ext in extensions])
759
+ filter_elements.extend([f".*.{ext}" for ext in extensions])
732
760
 
733
761
  if files:
734
762
  filter_elements.extend(files)
735
763
 
736
764
  filter += "|".join(filter_elements)
737
- filter += ")$"
738
765
 
739
766
  self._logger.info("Download filter: " + filter)
740
767
  case = self.get_case(case_id)
@@ -752,9 +779,15 @@ class Client:
752
779
  "filtroDownloadPorMascara": filter,
753
780
  }
754
781
 
755
- xml_content = create_case_xml(parameters)
756
- self._run_console(xml_content)
757
- self._logger.info(f"Results downloaded to {output_path}")
782
+ os.makedirs(output_path, exist_ok=True)
783
+
784
+ if self._python_client:
785
+ self._download_results_python(parameters) ## Not implemented yet
786
+ else:
787
+ # Download results using Console
788
+ xml_content = create_case_xml(parameters)
789
+ self._run_console(xml_content)
790
+ self._logger.info(f"Results downloaded to {output_path}")
758
791
 
759
792
  def cancel_case(self, case_id: int, wait: bool = False) -> bool:
760
793
  parameters = {
@@ -768,8 +801,12 @@ class Client:
768
801
  "idFila": str(case_id),
769
802
  }
770
803
 
771
- xml_content = create_case_xml(parameters)
772
- self._run_console(xml_content)
804
+ if self._python_client:
805
+ self._cancel_case_python(case_id, parameters)
806
+ else:
807
+ # Cancel case using Console
808
+ xml_content = create_case_xml(parameters)
809
+ self._run_console(xml_content)
773
810
  self._logger.info(f"Request to cancel case {case_id} was sent")
774
811
 
775
812
  if wait:
@@ -881,8 +918,6 @@ class Client:
881
918
  password_md5 = (
882
919
  password_md5
883
920
  if self.cluster["name"] == "PSR-US"
884
- or self.cluster["name"] == "PSR-HOTFIX"
885
- or self.cluster["name"] == "PSR-US_OHIO"
886
921
  else self.__password.upper()
887
922
  )
888
923
  additional_arguments = kwargs.get("additional_arguments", None)
@@ -890,15 +925,12 @@ class Client:
890
925
  "sessao_id": section,
891
926
  "tipo_autenticacao": "portal"
892
927
  if self.cluster["name"] == "PSR-US"
893
- or self.cluster["name"] == "PSR-HOTFIX"
894
- or self.cluster["name"] == "PSR-US_OHIO"
895
928
  else "bcrypt",
896
929
  "idioma": "3",
897
930
  }
898
931
  if additional_arguments:
899
932
  parameters.update(additional_arguments)
900
933
 
901
- # FIXME make additional arguments work as a dictionary to work with this code
902
934
  xml_input = create_case_xml(parameters)
903
935
 
904
936
  try:
@@ -941,6 +973,284 @@ class Client:
941
973
  )
942
974
  return self._cloud_clusters_xml_cache
943
975
 
976
+ def _execute_case(self, case_dict) -> int:
977
+ """
978
+ Execute a case on the PSR Cloud.
979
+ :param case_dict: Dictionary containing the case parameters.
980
+ :return: Case ID of the executed case.
981
+ """
982
+ case_dict["programa"] = case_dict["modelo"]
983
+ case_dict["numeroProcessos"] = case_dict["nproc"]
984
+ case_dict["versao_cliente"] = "5.4.0"
985
+ filter_request_result = self._make_soap_request(
986
+ "obterFiltros", additional_arguments=case_dict
987
+ )
988
+ upload_filter = filter_request_result.find(
989
+ "./Parametro[@nome='filtroUpload']"
990
+ ).text
991
+ upload_filter = "^[a-zA-Z0-9./_]*(" + upload_filter + ")$"
992
+
993
+ # Create Repository
994
+ self._logger.info("Creating remote repository")
995
+ repository_request_result = self._make_soap_request(
996
+ "criarRepositorio", additional_arguments=case_dict
997
+ )
998
+
999
+ # Add all parameters from the XML response to case_dict
1000
+ # Iterates over each <Parametro> element in the XML response,
1001
+ # extracts the 'nome' attribute for the key and the element's text for the value,
1002
+ # then adds this key-value pair to case_dict.
1003
+ for parametro_element in repository_request_result.findall("./Parametro"):
1004
+ param_name = parametro_element.get("nome")
1005
+ param_value = (
1006
+ parametro_element.text
1007
+ ) # This will be None if the element has no text.
1008
+ if param_name: # Ensure the parameter has a name before adding.
1009
+ case_dict[param_name] = param_value
1010
+
1011
+ repository_id = repository_request_result.find(
1012
+ "./Parametro[@nome='repositorioId']"
1013
+ )
1014
+ cloud_access = repository_request_result.find(
1015
+ "./Parametro[@nome='cloudAccess']"
1016
+ )
1017
+ cloud_secret = repository_request_result.find(
1018
+ "./Parametro[@nome='cloudSecret']"
1019
+ )
1020
+ cloud_session_token = repository_request_result.find(
1021
+ "./Parametro[@nome='cloudSessionToken']"
1022
+ )
1023
+ cloud_aws_url = repository_request_result.find("./Parametro[@nome='cloudUrl']")
1024
+ bucket_name = repository_request_result.find(
1025
+ "./Parametro[@nome='diretorioBase']"
1026
+ )
1027
+
1028
+ self._logger.info(f"Remote repository created with ID {repository_id.text}")
1029
+ case_dict["repositorioId"] = repository_id.text
1030
+
1031
+ # Filtering files to upload
1032
+ self._logger.info("Checking list of files to send")
1033
+
1034
+ file_list = _filter_upload_files(case_dict["diretorioDados"], upload_filter)
1035
+
1036
+ if not file_list:
1037
+ self._logger.warning(
1038
+ "No files found to upload. Please check the upload filter."
1039
+ )
1040
+ return
1041
+
1042
+ # generating .metadata folder with checksum for each file
1043
+ checksum_dictionary = {}
1044
+ metadata_folder = Path(case_dict["diretorioDados"]) / ".metadata"
1045
+ metadata_folder.mkdir(parents=True, exist_ok=True)
1046
+ for file_path in file_list:
1047
+ file_path = Path(file_path)
1048
+ if not file_path.is_absolute():
1049
+ file_path = Path(case_dict["diretorioDados"]) / file_path
1050
+ if not file_path.exists():
1051
+ self._logger.warning(f"File {file_path} does not exist. Skipping.")
1052
+ continue
1053
+ with open(file_path, "rb") as f:
1054
+ checksum = _md5sum(f.read(), enconding=False).upper()
1055
+ checksum_dictionary[file_path.name] = checksum
1056
+ metadata_file = metadata_folder / (file_path.name)
1057
+ with open(metadata_file, "w") as f:
1058
+ f.write(checksum)
1059
+
1060
+ self._logger.info(
1061
+ f"Uploading list of files to remote repository {repository_id.text}"
1062
+ )
1063
+
1064
+ # Uploading files to S3
1065
+ upload_case_to_s3(
1066
+ files=file_list,
1067
+ repository_id=repository_id.text,
1068
+ cluster_name=self.cluster["name"],
1069
+ checksums=checksum_dictionary,
1070
+ access=cloud_access.text if cloud_access is not None else None,
1071
+ secret=cloud_secret.text if cloud_secret is not None else None,
1072
+ session_token=cloud_session_token.text
1073
+ if cloud_session_token is not None
1074
+ else None,
1075
+ bucket_name=bucket_name.text if bucket_name is not None else None,
1076
+ url=cloud_aws_url.text if cloud_aws_url is not None else None,
1077
+ zip_compress=True,
1078
+ )
1079
+
1080
+ self._make_soap_request(
1081
+ "finalizarUpload",
1082
+ additional_arguments={"repositorioId": repository_id.text},
1083
+ )
1084
+ self._logger.info("Files uploaded successfully. Enqueuing case.")
1085
+ self._make_soap_request("enfileirarProcesso", additional_arguments=case_dict)
1086
+
1087
+ return repository_id.text
1088
+
1089
+ def _get_status_python(self, case_dict: dict) -> ET.Element:
1090
+ """
1091
+ Get the status of a case using the Python client.
1092
+ :param case_dict: Dictionary containing the case parameters.
1093
+ :return: XML Element with the status information.
1094
+ """
1095
+ try:
1096
+ response = self._make_soap_request(
1097
+ "obterStatusResultados", additional_arguments=case_dict
1098
+ )
1099
+
1100
+ # change response "status" parameter to "statusExecucao", as it is with current PSR Cloud
1101
+ status_param = response.find("./Parametro[@nome='status']")
1102
+ if status_param is not None:
1103
+ status_param.attrib["nome"] = "statusExecucao"
1104
+
1105
+ result_log = response.find("./Parametro[@nome='resultado']")
1106
+ if self._verbose and result_log is not None:
1107
+ self._logger.info(result_log.text)
1108
+ return response
1109
+ except Exception as e:
1110
+ self._logger.error(f"Error getting status: {str(e)}")
1111
+ raise CloudError(f"Failed to get status: {str(e)}")
1112
+
1113
+ def _cancel_case_python(self, case_id: int, xml_content: str) -> None:
1114
+ """
1115
+ Cancel a case using the Python client.
1116
+ :param case_id: The ID of the case to cancel.
1117
+ :param xml_content: XML content for the cancel request.
1118
+ """
1119
+ try:
1120
+ self._make_soap_request(
1121
+ "cancelarFila", additional_arguments={"idFila": str(case_id)}
1122
+ )
1123
+ except Exception as e:
1124
+ self._logger.error(f"Error cancelling case: {str(e)}")
1125
+ raise CloudError(f"Failed to cancel case: {str(e)}")
1126
+
1127
+ def _download_results_python(self, parameters: dict) -> None:
1128
+ """
1129
+ Download results using the Python client.
1130
+ :param parameters: Dictionary containing the download parameters.
1131
+ """
1132
+
1133
+ repository_id = parameters.get("repositorioId")
1134
+ download_filter = parameters.get("filtroDownloadPorMascara")
1135
+ output_path = parameters.get("diretorioDestino")
1136
+
1137
+ download_filter = (
1138
+ "^[a-zA-Z0-9./_]*(" + download_filter + ")$" if download_filter else None
1139
+ )
1140
+ self._logger.info("Obtaining credentials for download")
1141
+ credentials = self._make_soap_request(
1142
+ "buscaCredenciasDownload", additional_arguments=parameters
1143
+ )
1144
+
1145
+ access = credentials.find("./Parametro[@nome='cloudAccess']").text
1146
+ secret = credentials.find("./Parametro[@nome='cloudSecret']").text
1147
+ session_token = credentials.find("./Parametro[@nome='cloudSessionToken']").text
1148
+ url = credentials.find("./Parametro[@nome='cloudUrl']").text
1149
+ bucket_name = credentials.find("./Parametro[@nome='diretorioBase']").text
1150
+ bucket_name = bucket_name.replace("repository", "repository-download")
1151
+
1152
+ if access is None or secret is None or session_token is None or url is None:
1153
+ raise CloudError("Failed to retrieve credentials for downloading results.")
1154
+
1155
+ file_list = self.list_download_files(repository_id)
1156
+
1157
+ # filtering files to download
1158
+ if download_filter:
1159
+ filtered_file_list = [
1160
+ file["name"]
1161
+ for file in file_list
1162
+ if re.match(download_filter, file["name"])
1163
+ ]
1164
+ else:
1165
+ filtered_file_list = [file["name"] for file in file_list]
1166
+
1167
+ self._logger.info("Downloading results")
1168
+ downloaded_list = download_case_from_s3(
1169
+ repository_id=parameters["repositorioId"],
1170
+ cluster_name=self.cluster["name"],
1171
+ access=access,
1172
+ secret=secret,
1173
+ session_token=session_token,
1174
+ bucket_name=bucket_name,
1175
+ url=url,
1176
+ output_path=output_path,
1177
+ file_list=filtered_file_list,
1178
+ )
1179
+
1180
+ # Decompress gzipped files
1181
+ for file in filtered_file_list:
1182
+ if self._is_file_gzipped(os.path.join(output_path, file)):
1183
+ self._decompress_gzipped_file(os.path.join(output_path, file))
1184
+
1185
+ # Check if all requested files were downloaded
1186
+ for file in filtered_file_list:
1187
+ if file not in downloaded_list:
1188
+ self._logger.warning(f"File {file} was not downloaded.")
1189
+
1190
+ self._logger.info(f"Results downloaded to {output_path}")
1191
+
1192
+ def _is_file_gzipped(self, file_path: str) -> bool:
1193
+ """
1194
+ Checks if a file is gzipped by inspecting its magic number.
1195
+
1196
+ :param file_path: The path to the file.
1197
+ :return: True if the file is gzipped, False otherwise.
1198
+ """
1199
+ try:
1200
+ with open(file_path, "rb") as f_check:
1201
+ return f_check.read(2) == b"\x1f\x8b"
1202
+ except IOError:
1203
+ # TODO: Replace print with proper logging
1204
+ print(
1205
+ f"WARNING: Could not read {file_path} to check for gzip magic number."
1206
+ )
1207
+ return False
1208
+
1209
+ def _decompress_gzipped_file(self, gzipped_file_path: str) -> str:
1210
+ """
1211
+ Decompresses a gzipped file.
1212
+
1213
+ If the original filename ends with .gz, the .gz is removed for the
1214
+ decompressed filename. Otherwise, the file is decompressed in-place.
1215
+ The original gzipped file is removed upon successful decompression.
1216
+
1217
+ :param gzipped_file_path: The path to the gzipped file.
1218
+ :return: The path to the decompressed file. If decompression fails,
1219
+ the original gzipped_file_path is returned.
1220
+ """
1221
+ decompressed_target_path = (
1222
+ gzipped_file_path[:-3]
1223
+ if gzipped_file_path.lower().endswith(".gz")
1224
+ else gzipped_file_path
1225
+ )
1226
+ # Use a temporary file for decompression to avoid data loss if issues occur
1227
+ temp_decompressed_path = decompressed_target_path + ".decompressing_tmp"
1228
+
1229
+ try:
1230
+ with gzip.open(gzipped_file_path, "rb") as f_in, open(
1231
+ temp_decompressed_path, "wb"
1232
+ ) as f_out:
1233
+ shutil.copyfileobj(f_in, f_out)
1234
+ os.remove(gzipped_file_path)
1235
+ os.rename(temp_decompressed_path, decompressed_target_path)
1236
+ return decompressed_target_path
1237
+ except (gzip.BadGzipFile, EOFError, IOError) as e:
1238
+ print(
1239
+ f"ERROR: Failed to decompress {gzipped_file_path}: {e}. Original file kept."
1240
+ )
1241
+ except (
1242
+ Exception
1243
+ ) as e: # Catch other errors like permission issues during rename/remove
1244
+ print(
1245
+ f"ERROR: Error during post-decompression file operations for {gzipped_file_path}: {e}. Original file kept."
1246
+ )
1247
+ finally:
1248
+ if os.path.exists(
1249
+ temp_decompressed_path
1250
+ ): # Clean up temp file if it still exists
1251
+ os.remove(temp_decompressed_path)
1252
+ return gzipped_file_path # Return original path if decompression failed
1253
+
944
1254
  def get_programs(self) -> List[str]:
945
1255
  xml = self._get_cloud_versions_xml()
946
1256
  programs = [model.attrib["nome"] for model in xml]
@@ -1057,6 +1367,24 @@ def _budget_matches_list(budget_part: str, all_budgets: List[str]) -> List[str]:
1057
1367
  return [budget for budget in all_budgets if lowered_budget_part in budget.lower()]
1058
1368
 
1059
1369
 
1370
+ def _filter_upload_files(directory: str, upload_filter: str) -> List[str]:
1371
+ """
1372
+ Filter files in a directory based on the upload filter.
1373
+ :param directory: Directory to filter files from.
1374
+ :param upload_filter: Regular expression filter for file names.
1375
+ :return: List of filtered file paths.
1376
+ """
1377
+ if not os.path.exists(directory):
1378
+ raise CloudInputError(f"Directory {directory} does not exist")
1379
+
1380
+ regex = re.compile(upload_filter)
1381
+ filtered_files = []
1382
+ for file in os.listdir(directory):
1383
+ if regex.match(file):
1384
+ filtered_files.append(os.path.join(directory, file))
1385
+ return filtered_files
1386
+
1387
+
1060
1388
  def replace_case_str_values(client: Client, case: Case) -> Case:
1061
1389
  """Create a new case object using internal integer IDs instead of string values."""
1062
1390
  # Model Version
psr/cloud/data.py CHANGED
@@ -76,7 +76,9 @@ class Case:
76
76
  "Memory per process ratio must be a string",
77
77
  )
78
78
 
79
- self.repository_duration: Optional[Union[str, int]] = kwargs.get("repository_duration", 2)
79
+ self.repository_duration: Optional[Union[str, int]] = kwargs.get(
80
+ "repository_duration", 2
81
+ )
80
82
  self._validate_type(
81
83
  self.repository_duration,
82
84
  (int, str),
@@ -94,6 +96,13 @@ class Case:
94
96
  # Save In Cloud
95
97
  self.upload_only = kwargs.get("upload_only", False)
96
98
 
99
+ # Model Optional Attributes
100
+
101
+ # MyModel
102
+ self.mymodel_program_files: Optional[str] = kwargs.get(
103
+ "mymodel_program_files", None
104
+ )
105
+
97
106
  @staticmethod
98
107
  def _validate_type(
99
108
  value, expected_type: Union[List[type], Tuple[type], type], error_message: str
@@ -103,3 +112,11 @@ class Case:
103
112
 
104
113
  def __str__(self):
105
114
  return str(self.__dict__)
115
+
116
+ def to_dict(self) -> dict:
117
+ def serialize(obj):
118
+ if isinstance(obj, datetime):
119
+ return obj.isoformat()
120
+ return obj
121
+
122
+ return {k: serialize(v) for k, v in self.__dict__.items() if v is not None}
psr/cloud/xml.py CHANGED
@@ -9,6 +9,8 @@ from xml.etree import ElementTree as ET
9
9
  def create_case_xml(parameters: Dict[str, Any]) -> str:
10
10
  root = ET.Element("ColecaoParametro")
11
11
  for name, value in parameters.items():
12
+ if value is None:
13
+ continue
12
14
  value = _handle_invalid_xml_chars(value)
13
15
  parameter = ET.SubElement(root, "Parametro", nome=name, tipo="System.String")
14
16
  parameter.text = value
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__ = "4.1.0b10"
5
+ __version__ = "4.1.0b12"
6
6
 
7
7
  from .api import *
psr/factory/api.py CHANGED
@@ -1234,6 +1234,11 @@ class Context(DataObject):
1234
1234
  context_obj._hdr = None
1235
1235
  return context
1236
1236
 
1237
+ def __repr__(self):
1238
+ properties = self.as_dict()
1239
+ props_str = ', '.join(f"{key}={repr(value)}" for key, value in properties.items())
1240
+ return f"psr.factory.Context({props_str})"
1241
+
1237
1242
 
1238
1243
  class Study(_BaseObject):
1239
1244
  def __init__(self):
psr/factory/factory.dll CHANGED
Binary file