psr-factory 4.1.0b11__py3-none-win_amd64.whl → 5.0.0b1__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:
@@ -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)
@@ -617,20 +630,23 @@ class Client:
617
630
  if self._dry_run:
618
631
  return xml_content
619
632
 
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"
633
+ if self._python_client:
634
+ case_id = self._execute_case(parameters)
635
+ else:
636
+ self._run_console(xml_content)
637
+ xml = ET.parse(
638
+ f"{self._get_console_parent_path()}\\fake{case.program}_async.xml"
631
639
  )
640
+ _check_for_errors(xml, self._logger)
641
+ id_parameter = xml.find("./Parametro[@nome='repositorioId']")
642
+ if id_parameter is None:
643
+ xml_str = _xml_to_str(xml)
644
+ raise CloudError(
645
+ f"Case id not found on returned XML response.\n"
646
+ f"Contact PSR support at psrcloud@psr-inc.com with following data:\n\n{xml_str}\n\n"
647
+ )
632
648
 
633
- case_id = int(id_parameter.text)
649
+ case_id = int(id_parameter.text)
634
650
  if not wait:
635
651
  self._logger.info(f"Case {case.name} started with id {case_id}")
636
652
 
@@ -648,7 +664,6 @@ class Client:
648
664
  return case_id
649
665
 
650
666
  def get_status(self, case_id: int) -> tuple["ExecutionStatus", str]:
651
- # case = self.get_case(case_id)
652
667
  delete_xml = not self._debug_mode
653
668
  xml_content = ""
654
669
  with CreateTempFile(
@@ -666,10 +681,14 @@ class Client:
666
681
  "comando": "obterstatusresultados",
667
682
  "arquivoSaida": status_xml_path,
668
683
  }
669
- run_xml_content = create_case_xml(parameters)
670
684
 
671
- self._run_console(run_xml_content)
672
- xml = ET.parse(status_xml_path)
685
+ xml = None
686
+ if self._python_client:
687
+ xml = self._get_status_python(parameters)
688
+ else:
689
+ run_xml_content = create_case_xml(parameters)
690
+ self._run_console(run_xml_content)
691
+ xml = ET.parse(status_xml_path)
673
692
  parameter_status = xml.find("./Parametro[@nome='statusExecucao']")
674
693
  if parameter_status is None:
675
694
  xml_str = _xml_to_str(xml)
@@ -689,7 +708,7 @@ class Client:
689
708
  self._logger.info(f"Status: {STATUS_MAP_TEXT[status]}")
690
709
  return status, STATUS_MAP_TEXT[status]
691
710
 
692
- def list_download_files(self, case_id: int) -> List[str]:
711
+ def list_download_files(self, case_id: int) -> List[dict]:
693
712
  xml_files = self._make_soap_request(
694
713
  "prepararListaArquivosRemotaDownload",
695
714
  "listaArquivoRemota",
@@ -719,7 +738,7 @@ class Client:
719
738
  files: Optional[List[str]] = None,
720
739
  extensions: Optional[List[str]] = None,
721
740
  ) -> None:
722
- filter = ".*("
741
+ filter = ""
723
742
 
724
743
  if not extensions and not files:
725
744
  extensions = ["csv", "log", "hdr", "bin", "out", "ok"]
@@ -728,13 +747,12 @@ class Client:
728
747
 
729
748
  if extensions:
730
749
  Client._validate_extensions(extensions)
731
- filter_elements.extend([f".*.{ext}$" for ext in extensions])
750
+ filter_elements.extend([f".*.{ext}" for ext in extensions])
732
751
 
733
752
  if files:
734
753
  filter_elements.extend(files)
735
754
 
736
755
  filter += "|".join(filter_elements)
737
- filter += ")$"
738
756
 
739
757
  self._logger.info("Download filter: " + filter)
740
758
  case = self.get_case(case_id)
@@ -752,9 +770,15 @@ class Client:
752
770
  "filtroDownloadPorMascara": filter,
753
771
  }
754
772
 
755
- xml_content = create_case_xml(parameters)
756
- self._run_console(xml_content)
757
- self._logger.info(f"Results downloaded to {output_path}")
773
+ os.makedirs(output_path, exist_ok=True)
774
+
775
+ if self._python_client:
776
+ self._download_results_python(parameters) ## Not implemented yet
777
+ else:
778
+ # Download results using Console
779
+ xml_content = create_case_xml(parameters)
780
+ self._run_console(xml_content)
781
+ self._logger.info(f"Results downloaded to {output_path}")
758
782
 
759
783
  def cancel_case(self, case_id: int, wait: bool = False) -> bool:
760
784
  parameters = {
@@ -768,8 +792,12 @@ class Client:
768
792
  "idFila": str(case_id),
769
793
  }
770
794
 
771
- xml_content = create_case_xml(parameters)
772
- self._run_console(xml_content)
795
+ if self._python_client:
796
+ self._cancel_case_python(case_id, parameters)
797
+ else:
798
+ # Cancel case using Console
799
+ xml_content = create_case_xml(parameters)
800
+ self._run_console(xml_content)
773
801
  self._logger.info(f"Request to cancel case {case_id} was sent")
774
802
 
775
803
  if wait:
@@ -898,7 +926,6 @@ class Client:
898
926
  if additional_arguments:
899
927
  parameters.update(additional_arguments)
900
928
 
901
- # FIXME make additional arguments work as a dictionary to work with this code
902
929
  xml_input = create_case_xml(parameters)
903
930
 
904
931
  try:
@@ -941,6 +968,284 @@ class Client:
941
968
  )
942
969
  return self._cloud_clusters_xml_cache
943
970
 
971
+ def _execute_case(self, case_dict) -> int:
972
+ """
973
+ Execute a case on the PSR Cloud.
974
+ :param case_dict: Dictionary containing the case parameters.
975
+ :return: Case ID of the executed case.
976
+ """
977
+ case_dict["programa"] = case_dict["modelo"]
978
+ case_dict["numeroProcessos"] = case_dict["nproc"]
979
+ case_dict["versao_cliente"] = "5.4.0"
980
+ filter_request_result = self._make_soap_request(
981
+ "obterFiltros", additional_arguments=case_dict
982
+ )
983
+ upload_filter = filter_request_result.find(
984
+ "./Parametro[@nome='filtroUpload']"
985
+ ).text
986
+ upload_filter = "^[a-zA-Z0-9./_]*(" + upload_filter + ")$"
987
+
988
+ # Create Repository
989
+ self._logger.info("Creating remote repository")
990
+ repository_request_result = self._make_soap_request(
991
+ "criarRepositorio", additional_arguments=case_dict
992
+ )
993
+
994
+ # Add all parameters from the XML response to case_dict
995
+ # Iterates over each <Parametro> element in the XML response,
996
+ # extracts the 'nome' attribute for the key and the element's text for the value,
997
+ # then adds this key-value pair to case_dict.
998
+ for parametro_element in repository_request_result.findall("./Parametro"):
999
+ param_name = parametro_element.get("nome")
1000
+ param_value = (
1001
+ parametro_element.text
1002
+ ) # This will be None if the element has no text.
1003
+ if param_name: # Ensure the parameter has a name before adding.
1004
+ case_dict[param_name] = param_value
1005
+
1006
+ repository_id = repository_request_result.find(
1007
+ "./Parametro[@nome='repositorioId']"
1008
+ )
1009
+ cloud_access = repository_request_result.find(
1010
+ "./Parametro[@nome='cloudAccess']"
1011
+ )
1012
+ cloud_secret = repository_request_result.find(
1013
+ "./Parametro[@nome='cloudSecret']"
1014
+ )
1015
+ cloud_session_token = repository_request_result.find(
1016
+ "./Parametro[@nome='cloudSessionToken']"
1017
+ )
1018
+ cloud_aws_url = repository_request_result.find("./Parametro[@nome='cloudUrl']")
1019
+ bucket_name = repository_request_result.find(
1020
+ "./Parametro[@nome='diretorioBase']"
1021
+ )
1022
+
1023
+ self._logger.info(f"Remote repository created with ID {repository_id.text}")
1024
+ case_dict["repositorioId"] = repository_id.text
1025
+
1026
+ # Filtering files to upload
1027
+ self._logger.info("Checking list of files to send")
1028
+
1029
+ file_list = _filter_upload_files(case_dict["diretorioDados"], upload_filter)
1030
+
1031
+ if not file_list:
1032
+ self._logger.warning(
1033
+ "No files found to upload. Please check the upload filter."
1034
+ )
1035
+ return
1036
+
1037
+ # generating .metadata folder with checksum for each file
1038
+ checksum_dictionary = {}
1039
+ metadata_folder = Path(case_dict["diretorioDados"]) / ".metadata"
1040
+ metadata_folder.mkdir(parents=True, exist_ok=True)
1041
+ for file_path in file_list:
1042
+ file_path = Path(file_path)
1043
+ if not file_path.is_absolute():
1044
+ file_path = Path(case_dict["diretorioDados"]) / file_path
1045
+ if not file_path.exists():
1046
+ self._logger.warning(f"File {file_path} does not exist. Skipping.")
1047
+ continue
1048
+ with open(file_path, "rb") as f:
1049
+ checksum = _md5sum(f.read(), enconding=False).upper()
1050
+ checksum_dictionary[file_path.name] = checksum
1051
+ metadata_file = metadata_folder / (file_path.name)
1052
+ with open(metadata_file, "w") as f:
1053
+ f.write(checksum)
1054
+
1055
+ self._logger.info(
1056
+ f"Uploading list of files to remote repository {repository_id.text}"
1057
+ )
1058
+
1059
+ # Uploading files to S3
1060
+ upload_case_to_s3(
1061
+ files=file_list,
1062
+ repository_id=repository_id.text,
1063
+ cluster_name=self.cluster["name"],
1064
+ checksums=checksum_dictionary,
1065
+ access=cloud_access.text if cloud_access is not None else None,
1066
+ secret=cloud_secret.text if cloud_secret is not None else None,
1067
+ session_token=cloud_session_token.text
1068
+ if cloud_session_token is not None
1069
+ else None,
1070
+ bucket_name=bucket_name.text if bucket_name is not None else None,
1071
+ url=cloud_aws_url.text if cloud_aws_url is not None else None,
1072
+ zip_compress=True,
1073
+ )
1074
+
1075
+ self._make_soap_request(
1076
+ "finalizarUpload",
1077
+ additional_arguments={"repositorioId": repository_id.text},
1078
+ )
1079
+ self._logger.info("Files uploaded successfully. Enqueuing case.")
1080
+ self._make_soap_request("enfileirarProcesso", additional_arguments=case_dict)
1081
+
1082
+ return repository_id.text
1083
+
1084
+ def _get_status_python(self, case_dict: dict) -> ET.Element:
1085
+ """
1086
+ Get the status of a case using the Python client.
1087
+ :param case_dict: Dictionary containing the case parameters.
1088
+ :return: XML Element with the status information.
1089
+ """
1090
+ try:
1091
+ response = self._make_soap_request(
1092
+ "obterStatusResultados", additional_arguments=case_dict
1093
+ )
1094
+
1095
+ # change response "status" parameter to "statusExecucao", as it is with current PSR Cloud
1096
+ status_param = response.find("./Parametro[@nome='status']")
1097
+ if status_param is not None:
1098
+ status_param.attrib["nome"] = "statusExecucao"
1099
+
1100
+ result_log = response.find("./Parametro[@nome='resultado']")
1101
+ if self._verbose and result_log is not None:
1102
+ self._logger.info(result_log.text)
1103
+ return response
1104
+ except Exception as e:
1105
+ self._logger.error(f"Error getting status: {str(e)}")
1106
+ raise CloudError(f"Failed to get status: {str(e)}")
1107
+
1108
+ def _cancel_case_python(self, case_id: int, xml_content: str) -> None:
1109
+ """
1110
+ Cancel a case using the Python client.
1111
+ :param case_id: The ID of the case to cancel.
1112
+ :param xml_content: XML content for the cancel request.
1113
+ """
1114
+ try:
1115
+ self._make_soap_request(
1116
+ "cancelarFila", additional_arguments={"idFila": str(case_id)}
1117
+ )
1118
+ except Exception as e:
1119
+ self._logger.error(f"Error cancelling case: {str(e)}")
1120
+ raise CloudError(f"Failed to cancel case: {str(e)}")
1121
+
1122
+ def _download_results_python(self, parameters: dict) -> None:
1123
+ """
1124
+ Download results using the Python client.
1125
+ :param parameters: Dictionary containing the download parameters.
1126
+ """
1127
+
1128
+ repository_id = parameters.get("repositorioId")
1129
+ download_filter = parameters.get("filtroDownloadPorMascara")
1130
+ output_path = parameters.get("diretorioDestino")
1131
+
1132
+ download_filter = (
1133
+ "^[a-zA-Z0-9./_]*(" + download_filter + ")$" if download_filter else None
1134
+ )
1135
+ self._logger.info("Obtaining credentials for download")
1136
+ credentials = self._make_soap_request(
1137
+ "buscaCredenciasDownload", additional_arguments=parameters
1138
+ )
1139
+
1140
+ access = credentials.find("./Parametro[@nome='cloudAccess']").text
1141
+ secret = credentials.find("./Parametro[@nome='cloudSecret']").text
1142
+ session_token = credentials.find("./Parametro[@nome='cloudSessionToken']").text
1143
+ url = credentials.find("./Parametro[@nome='cloudUrl']").text
1144
+ bucket_name = credentials.find("./Parametro[@nome='diretorioBase']").text
1145
+ bucket_name = bucket_name.replace("repository", "repository-download")
1146
+
1147
+ if access is None or secret is None or session_token is None or url is None:
1148
+ raise CloudError("Failed to retrieve credentials for downloading results.")
1149
+
1150
+ file_list = self.list_download_files(repository_id)
1151
+
1152
+ # filtering files to download
1153
+ if download_filter:
1154
+ filtered_file_list = [
1155
+ file["name"]
1156
+ for file in file_list
1157
+ if re.match(download_filter, file["name"])
1158
+ ]
1159
+ else:
1160
+ filtered_file_list = [file["name"] for file in file_list]
1161
+
1162
+ self._logger.info("Downloading results")
1163
+ downloaded_list = download_case_from_s3(
1164
+ repository_id=parameters["repositorioId"],
1165
+ cluster_name=self.cluster["name"],
1166
+ access=access,
1167
+ secret=secret,
1168
+ session_token=session_token,
1169
+ bucket_name=bucket_name,
1170
+ url=url,
1171
+ output_path=output_path,
1172
+ file_list=filtered_file_list,
1173
+ )
1174
+
1175
+ # Decompress gzipped files
1176
+ for file in filtered_file_list:
1177
+ if self._is_file_gzipped(os.path.join(output_path, file)):
1178
+ self._decompress_gzipped_file(os.path.join(output_path, file))
1179
+
1180
+ # Check if all requested files were downloaded
1181
+ for file in filtered_file_list:
1182
+ if file not in downloaded_list:
1183
+ self._logger.warning(f"File {file} was not downloaded.")
1184
+
1185
+ self._logger.info(f"Results downloaded to {output_path}")
1186
+
1187
+ def _is_file_gzipped(self, file_path: str) -> bool:
1188
+ """
1189
+ Checks if a file is gzipped by inspecting its magic number.
1190
+
1191
+ :param file_path: The path to the file.
1192
+ :return: True if the file is gzipped, False otherwise.
1193
+ """
1194
+ try:
1195
+ with open(file_path, "rb") as f_check:
1196
+ return f_check.read(2) == b"\x1f\x8b"
1197
+ except IOError:
1198
+ # TODO: Replace print with proper logging
1199
+ print(
1200
+ f"WARNING: Could not read {file_path} to check for gzip magic number."
1201
+ )
1202
+ return False
1203
+
1204
+ def _decompress_gzipped_file(self, gzipped_file_path: str) -> str:
1205
+ """
1206
+ Decompresses a gzipped file.
1207
+
1208
+ If the original filename ends with .gz, the .gz is removed for the
1209
+ decompressed filename. Otherwise, the file is decompressed in-place.
1210
+ The original gzipped file is removed upon successful decompression.
1211
+
1212
+ :param gzipped_file_path: The path to the gzipped file.
1213
+ :return: The path to the decompressed file. If decompression fails,
1214
+ the original gzipped_file_path is returned.
1215
+ """
1216
+ decompressed_target_path = (
1217
+ gzipped_file_path[:-3]
1218
+ if gzipped_file_path.lower().endswith(".gz")
1219
+ else gzipped_file_path
1220
+ )
1221
+ # Use a temporary file for decompression to avoid data loss if issues occur
1222
+ temp_decompressed_path = decompressed_target_path + ".decompressing_tmp"
1223
+
1224
+ try:
1225
+ with gzip.open(gzipped_file_path, "rb") as f_in, open(
1226
+ temp_decompressed_path, "wb"
1227
+ ) as f_out:
1228
+ shutil.copyfileobj(f_in, f_out)
1229
+ os.remove(gzipped_file_path)
1230
+ os.rename(temp_decompressed_path, decompressed_target_path)
1231
+ return decompressed_target_path
1232
+ except (gzip.BadGzipFile, EOFError, IOError) as e:
1233
+ print(
1234
+ f"ERROR: Failed to decompress {gzipped_file_path}: {e}. Original file kept."
1235
+ )
1236
+ except (
1237
+ Exception
1238
+ ) as e: # Catch other errors like permission issues during rename/remove
1239
+ print(
1240
+ f"ERROR: Error during post-decompression file operations for {gzipped_file_path}: {e}. Original file kept."
1241
+ )
1242
+ finally:
1243
+ if os.path.exists(
1244
+ temp_decompressed_path
1245
+ ): # Clean up temp file if it still exists
1246
+ os.remove(temp_decompressed_path)
1247
+ return gzipped_file_path # Return original path if decompression failed
1248
+
944
1249
  def get_programs(self) -> List[str]:
945
1250
  xml = self._get_cloud_versions_xml()
946
1251
  programs = [model.attrib["nome"] for model in xml]
@@ -1057,6 +1362,24 @@ def _budget_matches_list(budget_part: str, all_budgets: List[str]) -> List[str]:
1057
1362
  return [budget for budget in all_budgets if lowered_budget_part in budget.lower()]
1058
1363
 
1059
1364
 
1365
+ def _filter_upload_files(directory: str, upload_filter: str) -> List[str]:
1366
+ """
1367
+ Filter files in a directory based on the upload filter.
1368
+ :param directory: Directory to filter files from.
1369
+ :param upload_filter: Regular expression filter for file names.
1370
+ :return: List of filtered file paths.
1371
+ """
1372
+ if not os.path.exists(directory):
1373
+ raise CloudInputError(f"Directory {directory} does not exist")
1374
+
1375
+ regex = re.compile(upload_filter)
1376
+ filtered_files = []
1377
+ for file in os.listdir(directory):
1378
+ if regex.match(file):
1379
+ filtered_files.append(os.path.join(directory, file))
1380
+ return filtered_files
1381
+
1382
+
1060
1383
  def replace_case_str_values(client: Client, case: Case) -> Case:
1061
1384
  """Create a new case object using internal integer IDs instead of string values."""
1062
1385
  # Model Version
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.0b11"
5
+ __version__ = "5.0.0b1"
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