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 +361 -33
- psr/cloud/data.py +18 -1
- psr/cloud/xml.py +2 -0
- psr/factory/__init__.py +1 -1
- psr/factory/api.py +5 -0
- psr/factory/factory.dll +0 -0
- psr/factory/factory.pmd +448 -74
- psr/factory/factory.pmk +409 -101
- psr/factory/libcurl-x64.dll +0 -0
- psr/factory/samples/{sddp_1_stage_case01.py → sddp_case01.py} +6 -10
- psr/factory/samples/sddp_case21.py +242 -0
- {psr_factory-4.1.0b10.dist-info → psr_factory-4.1.0b12.dist-info}/METADATA +3 -3
- {psr_factory-4.1.0b10.dist-info → psr_factory-4.1.0b12.dist-info}/RECORD +16 -15
- {psr_factory-4.1.0b10.dist-info → psr_factory-4.1.0b12.dist-info}/WHEEL +0 -0
- {psr_factory-4.1.0b10.dist-info → psr_factory-4.1.0b12.dist-info}/licenses/LICENSE.txt +0 -0
- {psr_factory-4.1.0b10.dist-info → psr_factory-4.1.0b12.dist-info}/top_level.txt +0 -0
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
|
-
|
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.
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
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
|
-
|
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
|
-
|
672
|
-
|
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[
|
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}
|
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
|
-
|
756
|
-
|
757
|
-
self.
|
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
|
-
|
772
|
-
|
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(
|
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
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
|