protoncloud-sdk 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
proton/__init__.py ADDED
File without changes
@@ -0,0 +1,245 @@
1
+ import os
2
+ import requests
3
+ from proton.proton_env import get_api_url, get_auth_header, get_host, get_id_dataset_run, is_proton_execution
4
+ from proton.run_status import RunStatus
5
+
6
+
7
+ def _get_dataset_run_url():
8
+ return get_api_url(f"dataset-run/{get_id_dataset_run()}")
9
+
10
+
11
+ def _request_json(method, url, params=None, body=None):
12
+ if not is_proton_execution():
13
+ return {}
14
+
15
+ response = requests.request(method, url, params=params, json=body, headers=get_auth_header())
16
+
17
+ # O print vai para o stdout, que o runner envia como log da execucao no Proton.
18
+ if response.status_code != 200:
19
+ print(f"[proton] {method} {response.url} -> HTTP {response.status_code}: {response.text[:300]}")
20
+ return {}
21
+
22
+ try:
23
+ return response.json()
24
+ except ValueError:
25
+ print(f"[proton] {method} {response.url} -> HTTP 200 sem JSON valido: {response.text[:120]!r}")
26
+ return {}
27
+
28
+
29
+ def _get_dataset_run_param(service, response_key):
30
+ data = _request_json("GET", f"{_get_dataset_run_url()}/{service}")
31
+ return data.get(response_key, "")
32
+
33
+
34
+ def start_component():
35
+ """Inicia um componente."""
36
+ if is_proton_execution():
37
+ data = _request_json("PUT", f"{_get_dataset_run_url()}/start-component")
38
+
39
+ if data:
40
+ run_progress_id = data.get("id") or data.get("idRunProgress")
41
+ if run_progress_id is not None:
42
+ os.environ["idRunProgress"] = str(run_progress_id)
43
+
44
+ return data
45
+
46
+ return {}
47
+
48
+ def end_component():
49
+ """Finaliza um componente."""
50
+
51
+ if is_proton_execution():
52
+ data = _request_json("PUT", f"{_get_dataset_run_url()}/end-component")
53
+ os.environ.pop('idRunProgress', None)
54
+
55
+ if data:
56
+ return data.get("returnType", data.get("runType", ""))
57
+
58
+ return ""
59
+
60
+ def update_run_status(status:RunStatus):
61
+ """Atualiza o status de execução de um cenário."""
62
+ if is_proton_execution():
63
+ status_value = status.value if isinstance(status, RunStatus) else str(status)
64
+ _request_json("PUT", f"{_get_dataset_run_url()}/update-status", body={"status": status_value})
65
+
66
+ def get_current_component_system():
67
+ """
68
+ Retorna o tipo de sistema do componente corrente.
69
+
70
+ Retorna:
71
+ str: Tipo de sistema do componente. String vazia se não encontrado ou em caso de erro.
72
+ """
73
+ return _get_dataset_run_param("component-system", "system")
74
+
75
+ def get_current_component_name():
76
+ """
77
+ Retorna o nome do componente corrente.
78
+
79
+ Retorna:
80
+ str: Nome do componente. String vazia se não encontrado ou em caso de erro.
81
+ """
82
+ return _get_dataset_run_param("component-name", "name")
83
+
84
+ def get_scenario_reference():
85
+ """
86
+ Retorna o texto do campo de Referência do cenário
87
+
88
+ Retorna:
89
+ str: Referência do cenário. String vazia se a referência não for encontrada ou ocorrer algum erro.
90
+ """
91
+ return _get_dataset_run_param("automation-reference", "reference")
92
+
93
+
94
+
95
+ def get_cycle_name():
96
+ """Retorna o nome do ciclo."""
97
+ return _get_dataset_run_param("cycle", "cycle")
98
+
99
+ def get_dataset_run_info():
100
+ """Retorna informações do Dataset Run em formato JSON."""
101
+ return _request_json("GET", _get_dataset_run_url())
102
+
103
+ def get_dataset_run_components():
104
+ """Retorna informações do Dataset Run em formato JSON."""
105
+ data = _request_json(
106
+ "GET",
107
+ get_api_url("dataset-run"),
108
+ params={
109
+ "componentsByDatasetRun": "true",
110
+ "idDatasetRun": get_id_dataset_run(),
111
+ },
112
+ )
113
+
114
+ return data if data else get_dataset_run_info()
115
+
116
+ def get_dataset_run_component_names():
117
+ """
118
+ Retorna uma lista com os nomes de todos os componentes do Dataset Run.
119
+
120
+ Retorna:
121
+ list[str]: Lista com os nomes dos componentes.
122
+ """
123
+ dataset_run_info = get_dataset_run_components()
124
+
125
+ # Verifica se o retorno contém a chave 'datasetScenario' e 'datasetComponent'
126
+ components = []
127
+
128
+ if "datasetScenario" in dataset_run_info and "datasetComponent" in dataset_run_info["datasetScenario"]:
129
+ components = dataset_run_info["datasetScenario"]["datasetComponent"]
130
+ elif "dataset" in dataset_run_info and "datasetComponent" in dataset_run_info["dataset"]:
131
+ components = dataset_run_info["dataset"]["datasetComponent"]
132
+ elif "datasetComponent" in dataset_run_info:
133
+ components = dataset_run_info["datasetComponent"]
134
+ elif "components" in dataset_run_info:
135
+ components = dataset_run_info["components"]
136
+
137
+ if components:
138
+ sorted_components = sorted(components, key=lambda x: x.get("componentOrder", 0))
139
+
140
+ component_names = [
141
+ comp.get("component", {}).get("name", comp.get("name", ""))
142
+ for comp in sorted_components
143
+ if comp.get("component") or comp.get("name")
144
+ ]
145
+ return component_names
146
+
147
+ return []
148
+
149
+
150
+
151
+ def get_dataset_scenario_by_dataset_run():
152
+ """Retorna o ID do Dataset Scenario a partir do ID do Dataset Run."""
153
+ execution_info = get_dataset_run_info()
154
+ return execution_info.get("idDataset", execution_info.get("idDatasetScenario", execution_info.get("Id_Dataset_Scenario", "")))
155
+
156
+ def get_dataset_scenario_info(dataset_scenario_id):
157
+ """Retorna informações do dataset Scenario em formato JSON."""
158
+ dataset_info = _request_json("GET", f"{get_host()}/dataset/{dataset_scenario_id}")
159
+ if dataset_info:
160
+ return dataset_info
161
+
162
+ return _request_json("GET", f"{get_host()}/dataset-scenario/{dataset_scenario_id}")
163
+
164
+ def get_execution_client():
165
+ """Retorna informações de onde a automação foi disparada em formato JSON."""
166
+ informacoes_execucao = get_dataset_run_info()
167
+ client_id = informacoes_execucao.get('idClient')
168
+
169
+ if not client_id:
170
+ return {}
171
+
172
+ return _request_json("GET", f"{get_host()}/client/{client_id}")
173
+
174
+ def get_dataset_category():
175
+ """Retorna as informações da categoria atribuída ao dataset."""
176
+ dataset_id = get_dataset_scenario_by_dataset_run()
177
+ dataset_info = get_dataset_scenario_info(dataset_id) if dataset_id else {}
178
+
179
+ if dataset_info:
180
+ return dataset_info
181
+
182
+ execution_info = get_dataset_run_info()
183
+ category_id = execution_info.get("datasetScenario", {}).get("idCategory")
184
+
185
+ if not category_id:
186
+ return {}
187
+
188
+ return _request_json("GET", f"{get_host()}/category/{category_id}")
189
+
190
+ def get_dataset_category_name():
191
+ """Retorna o nome da categoria do dataset definido no Proton."""
192
+ dataset_category_infos = get_dataset_category()
193
+ return dataset_category_infos.get("category") or dataset_category_infos.get("name", "")
194
+
195
+ def get_dataset_system():
196
+ """Retorna o campo 'System' do dataset definido no Proton."""
197
+ dataset_scenario_id = get_dataset_scenario_by_dataset_run()
198
+ scenario_info = get_dataset_scenario_info(dataset_scenario_id)
199
+ return scenario_info.get("system", "")
200
+
201
+ def get_component_types():
202
+ """
203
+ Retorna os tipos de componentes presentes no Dataset Run.
204
+
205
+ Retorna:
206
+ str: Uma string com os tipos de componentes separados por ponto e vírgula (;),
207
+ ou uma string vazia em caso de erro.
208
+ """
209
+ data = _request_json("GET", _get_dataset_run_url(), params={"componentTypes": "true"})
210
+
211
+ if isinstance(data, list):
212
+ component_types = [item.get("Type") for item in data if item.get("Type")]
213
+ return ";".join(component_types)
214
+
215
+ return ""
216
+
217
+ def get_dataset_run_parameters():
218
+ """Retorna os parâmetros do Dataset Run em formato JSON."""
219
+ return _request_json(
220
+ "GET",
221
+ get_api_url("dataset-run-parameter"),
222
+ params={
223
+ "idDatasetRun": get_id_dataset_run(),
224
+ "organized": "true",
225
+ },
226
+ )
227
+
228
+ def get_mobile_app(mobile_app_id):
229
+ """Retorna informações sobre a aplicação mobile em formato JSON."""
230
+ return _request_json("GET", get_api_url(f"mobile-app/{mobile_app_id}"))
231
+
232
+ def get_scenario_name_by_dataset_run():
233
+ """
234
+ Retorna o nome do cenário a partir do Dataset Run ID.
235
+
236
+ Retorna:
237
+ str: Nome do cenário. Uma string vazia caso o nome não seja encontrado ou ocorra um erro.
238
+ """
239
+
240
+ scenario_name = _get_dataset_run_param("automation-name", "name")
241
+ if scenario_name:
242
+ return scenario_name
243
+
244
+ data = _request_json("GET", _get_dataset_run_url(), params={"scenarioNameByDatasetRun": "true"})
245
+ return data.get("Name", "")
@@ -0,0 +1,59 @@
1
+ from enum import Enum
2
+ import os
3
+
4
+ class ProtonClientParam(Enum):
5
+ @classmethod
6
+ def _root_folder(cls):
7
+ return os.path.expanduser("~")
8
+
9
+ @classmethod
10
+ def _proton_client_folder(cls):
11
+ return cls._root_folder() + os.sep + ".proton-client"
12
+
13
+ @classmethod
14
+ def _config_folder(cls):
15
+ return cls._proton_client_folder() + os.sep + "config"
16
+
17
+ @classmethod
18
+ def _scripts_folder(cls):
19
+ return cls._proton_client_folder() + os.sep + "scripts"
20
+
21
+ @classmethod
22
+ def _components_folder(cls):
23
+ return cls._proton_client_folder() + os.sep + "components"
24
+
25
+ @classmethod
26
+ def _library_folder(cls):
27
+ return cls._proton_client_folder() + os.sep + "library"
28
+
29
+ @classmethod
30
+ def _drivers_folder(cls):
31
+ return cls._proton_client_folder() + os.sep + "drivers"
32
+
33
+ @classmethod
34
+ def _screenshots_folder(cls):
35
+ return cls._proton_client_folder() + os.sep + "screenshots"
36
+
37
+ @classmethod
38
+ def _video_folder(cls):
39
+ return cls._proton_client_folder() + os.sep + "video"
40
+
41
+ @classmethod
42
+ def _log_folder(cls):
43
+ return cls._proton_client_folder() + os.sep + "log"
44
+
45
+ @classmethod
46
+ def _repository_folder(cls):
47
+ return cls._root_folder() + os.sep + ".proton-client-repo"
48
+
49
+ @classmethod
50
+ def _attachments_folder(cls):
51
+ return cls._proton_client_folder() + os.sep + "attachments"
52
+
53
+ @classmethod
54
+ def _cmd_folder(cls):
55
+ return cls._proton_client_folder() + os.sep + "cmd"
56
+
57
+ @classmethod
58
+ def _app_folder(cls):
59
+ return cls._proton_client_folder() + os.sep + "app"
proton/proton_env.py ADDED
@@ -0,0 +1,187 @@
1
+ from configparser import ConfigParser
2
+ import os
3
+
4
+
5
+ def _is_localhost(host):
6
+ """Retorna True quando o host aponta para a máquina local."""
7
+ return any(local_address in host for local_address in ("localhost", "127.0.0.1", "::1", "[::1]"))
8
+
9
+ def get_proton_config():
10
+ """
11
+ Lê e retorna a configuração do arquivo `proton.ini`.
12
+
13
+ Returns:
14
+ ConfigParser: Objeto ConfigParser contendo as configurações do arquivo `proton.ini`.
15
+ """
16
+ config = ConfigParser()
17
+ config.read('proton.ini')
18
+ return config
19
+
20
+
21
+ _logged_overrides = set()
22
+
23
+
24
+ def _from_file(option, section='server'):
25
+ """Valor de uma chave do `proton.ini`, ou string vazia se o arquivo/chave não existir."""
26
+ return get_proton_config().get(section, option, fallback='').strip()
27
+
28
+
29
+ def _file_precedence():
30
+ """
31
+ `[server] config_precedence = file` faz o arquivo do projeto vencer o que o runner
32
+ injeta. Serve para teste apontado a outro ambiente e para projetos legados que
33
+ precisam continuar falando com o servidor fixado no repositório.
34
+ """
35
+ return _from_file('config_precedence').lower() == 'file'
36
+
37
+
38
+ def _resolve(env_key, option, sensitive=False):
39
+ """
40
+ Resolve uma configuração com a prioridade ambiente -> `proton.ini`.
41
+
42
+ O runner injeta `PROTON_HOST` e `PROTON_TOKEN` no processo da automação, para que a
43
+ execução reporte ao servidor de onde ela nasceu — um `proton.ini` apontando para outro
44
+ ambiente faz a execução reportar no lugar errado, e como o componente nunca avança lá,
45
+ o runner relança a automação indefinidamente.
46
+
47
+ Mesma convenção de nomes do atomic-framework: a chave `proton.host` vira `PROTON_HOST`.
48
+ """
49
+ file_value = _from_file(option)
50
+
51
+ if _file_precedence() and file_value:
52
+ return file_value
53
+
54
+ runtime_value = (os.getenv(env_key) or '').strip()
55
+
56
+ if not runtime_value:
57
+ return file_value
58
+
59
+ if file_value and file_value != runtime_value and env_key not in _logged_overrides:
60
+ # Sobrescrita silenciosa vira depuração às cegas. O valor sensível nunca é
61
+ # impresso: o stdout da automação sobe como log da execução no Proton.
62
+ _logged_overrides.add(env_key)
63
+ if sensitive:
64
+ print(f'[proton] {env_key} definido em tempo de execução (valor do arquivo ignorado)')
65
+ else:
66
+ print(f'[proton] {env_key} definido em tempo de execução: {runtime_value} '
67
+ f'(arquivo declarava: {file_value})')
68
+
69
+ return runtime_value
70
+
71
+
72
+ def get_host():
73
+ """
74
+ Obtém o host do Proton: `PROTON_HOST` do ambiente, com o `proton.ini` como fallback.
75
+
76
+ Returns:
77
+ str: Host da API do Proton, sempre com protocolo.
78
+ """
79
+ host = _resolve('PROTON_HOST', 'hostname')
80
+
81
+ if not host:
82
+ raise RuntimeError(
83
+ 'Host do Proton não configurado: defina PROTON_HOST no ambiente '
84
+ 'ou a chave hostname na seção [server] do proton.ini'
85
+ )
86
+
87
+ if _is_localhost(host) or host.startswith('https://'):
88
+ return host
89
+
90
+ if host.startswith('http://'):
91
+ return f"https://{host[len('http://'):] }"
92
+
93
+ return f"https://{host}"
94
+
95
+ def get_token():
96
+ """
97
+ Obtém o token do Proton: `PROTON_TOKEN` do ambiente, com o `proton.ini` como fallback.
98
+
99
+ Returns:
100
+ str: Token de acesso, ou string vazia quando não há nenhum configurado.
101
+ """
102
+ return _resolve('PROTON_TOKEN', 'token', sensitive=True)
103
+
104
+ def is_record_video():
105
+ """
106
+ Verifica se a gravação de vídeo está habilitada para os relatórios.
107
+
108
+ Returns:
109
+ bool: `True` se `video_record` estiver configurado como 'true' na seção `[reports]` do arquivo `proton.ini`, caso contrário `False`.
110
+ """
111
+ config = get_proton_config()
112
+ video_record = config.get('reports', 'video_record', fallback='false').lower()
113
+ return video_record == 'true'
114
+
115
+ def get_api_url(path):
116
+ """
117
+ Monta a URL completa de um recurso da API a partir do hostname do `proton.ini`.
118
+
119
+ Não usa `urllib.parse.urljoin`: com um caminho iniciado por '/', o urljoin descarta o
120
+ prefixo '/api' do hostname e a requisição cai na página do frontend (HTML, status 200).
121
+
122
+ Args:
123
+ path (str): Caminho do recurso, com ou sem a barra inicial (ex.: 'dataset-run/10').
124
+
125
+ Returns:
126
+ str: URL absoluta do recurso.
127
+ """
128
+ return f"{get_host().rstrip('/')}/{str(path).lstrip('/')}"
129
+
130
+ def get_auth_header():
131
+ """
132
+ Gera o cabeçalho de autenticação para requisições HTTP, baseado no token do Proton.
133
+
134
+ Aceita o token do `proton.ini` com ou sem o prefixo 'Bearer ' e nunca o duplica:
135
+ 'Bearer Bearer <token>' é recusado pelo Proton 5 com 401.
136
+
137
+ Returns:
138
+ dict: Dicionário com o cabeçalho de autenticação no formato {'Authorization': 'Bearer token'}.
139
+ """
140
+ token = (get_token() or '').strip()
141
+
142
+ if not token:
143
+ return {}
144
+
145
+ if token.lower().startswith('bearer '):
146
+ token = token[len('bearer '):].strip()
147
+
148
+ return {'Authorization': f'Bearer {token}'}
149
+
150
+ def get_id_dataset_run():
151
+ """
152
+ Obtém o ID do Dataset Run, que representa o número de execução do cenário no Proton.
153
+
154
+ Returns:
155
+ str: Valor do ID Dataset Run configurado nas variáveis de ambiente.
156
+ Retorna None se a variável `idDatasetRun` não estiver definida.
157
+ """
158
+ return os.getenv('idDatasetRun')
159
+
160
+ def get_idRunProgress():
161
+ """
162
+ Obtém o ID do progresso de execução (Run Progress) no Proton.
163
+
164
+ Returns:
165
+ str: Valor do ID Run Progress configurado nas variáveis de ambiente.
166
+ Retorna None se a variável `Id_Run_Progress` não estiver definida.
167
+ """
168
+ return os.getenv('idRunProgress')
169
+
170
+ def is_proton_execution():
171
+ """
172
+ Verifica se a execução está sendo realizada no Proton.
173
+
174
+ Returns:
175
+ bool: Retorna True se a variável de ambiente `idDatasetRun` estiver definida,
176
+ indicando execução no Proton. Caso contrário, retorna False.
177
+ """
178
+ return os.getenv('idDatasetRun') is not None
179
+
180
+ def set_id_dataset_run(id_dataset_run):
181
+ """
182
+ Define o valor do ID Dataset Run (ID de execução do cenário) como uma variável de ambiente.
183
+
184
+ Args:
185
+ id_dataset_run (str): Valor do ID Dataset Run a ser configurado.
186
+ """
187
+ os.environ["idDatasetRun"] = id_dataset_run
@@ -0,0 +1,170 @@
1
+
2
+ import os
3
+
4
+ import requests
5
+ from proton.proton_client_parama import ProtonClientParam
6
+ from proton.proton_env import get_api_url, get_auth_header, get_host, get_id_dataset_run, get_idRunProgress, is_proton_execution
7
+ from proton.proton_logs import set_log
8
+
9
+ BUFFER_SIZE = 102400
10
+ ENTITY_RUN_PROGRESS = "run_progress"
11
+ ENTITY_DATASET_RUN_RESOURCE = "dataset_run_resource"
12
+
13
+ def upload_file_resource(file_resource):
14
+ """Realiza o upload de um arquivo como recurso no Proton."""
15
+ try:
16
+ file_name = os.path.basename(file_resource)
17
+ file_path = os.path.dirname(file_resource)
18
+ json_response = insert_proton_file(ENTITY_DATASET_RUN_RESOURCE, get_id_dataset_run(), file_path, file_name)
19
+ return json_response
20
+ except Exception as e:
21
+ set_log(f"Erro ao fazer upload do arquivo: {e}")
22
+ return {} # Retorna um dicionário vazio em caso de falha
23
+
24
+ def upload_image(full_image_path):
25
+ """Realiza upload de uma imagem para o Proton."""
26
+ try:
27
+ image_name = os.path.basename(full_image_path)
28
+ image_path = os.path.dirname(full_image_path)
29
+ return insert_proton_file(ENTITY_RUN_PROGRESS, get_idRunProgress(), image_path, image_name)
30
+ except Exception as e:
31
+ set_log(f"Erro ao fazer upload da imagem: {e}")
32
+ return {}
33
+
34
+ def insert_proton_file(entity, entity_id, file_path, file_name):
35
+ """Lógica central de upload de arquivos. Requer uma implementação da função de upload."""
36
+ if is_proton_execution():
37
+ url = get_host() + f"/file-system/upload?entity={entity}&idEntity={entity_id}"
38
+ return file_upload(url, file_path, file_name)
39
+
40
+ return {}
41
+
42
+
43
+ def file_upload(url_path, file_path, file_name):
44
+ """Realiza upload de arquivos, retornando a resposta JSON do Proton.
45
+
46
+ Args:
47
+ url_path (str): A URL para onde o arquivo será enviado.
48
+ file_path (str): O caminho da pasta contendo o arquivo.
49
+ file_name (str): O nome do arquivo.
50
+
51
+ Retorna:
52
+ dict: Dicionário Python contendo a resposta JSON do Proton, ou um dicionário vazio
53
+ caso ocorra algum erro.
54
+ """
55
+ try:
56
+ with open(os.path.normpath(os.path.join(file_path, file_name)), 'rb') as file_data:
57
+ files = {
58
+ "file": (file_name, file_data)
59
+ }
60
+
61
+ response = requests.post(url_path, headers=get_auth_header(), files=files)
62
+
63
+ if response.status_code != 200:
64
+ set_log(f"Erro ao fazer upload do arquivo. Código de status: {response.status_code}")
65
+ return {}
66
+
67
+ return response.json() if response.content else {}
68
+
69
+ except ValueError:
70
+ set_log(f"Erro ao decodificar a resposta do upload do arquivo: {file_name}")
71
+
72
+ except Exception as e:
73
+ set_log(f"Erro durante o upload do arquivo: {e}")
74
+
75
+ return {}
76
+
77
+ def get_mobile_app(mobile_app_id):
78
+ """Retorna informações sobre a aplicação mobile em formato JSON."""
79
+ url = get_api_url(f"mobile-app/{mobile_app_id}")
80
+ header = get_auth_header()
81
+
82
+ response = requests.get(url, headers=header)
83
+
84
+ if response.status_code == 200:
85
+ return response.json()
86
+ else:
87
+ return {}
88
+
89
+ def get_signed_url_file(entity, entity_id):
90
+ """Retorna a URL assinada para um arquivo."""
91
+ url = get_api_url("file-system")
92
+ header = get_auth_header()
93
+
94
+ params = {
95
+ "entity": entity,
96
+ "idEntity": entity_id
97
+ }
98
+
99
+ response = requests.get(url, params=params, headers=header)
100
+
101
+ if response.status_code == 200:
102
+ return response.json()
103
+ else:
104
+ return {}
105
+
106
+ def download_mobile_app(entity_id):
107
+ """Baixa uma aplicação mobile e retorna o caminho para o arquivo."""
108
+ json_file = get_signed_url_file("Mobile_App", entity_id)
109
+
110
+ try:
111
+ file_name = json_file.get("fileName", "")
112
+ app_file_path = ProtonClientParam._app_folder() + os.sep
113
+
114
+ os.makedirs(app_file_path, exist_ok=True) # Cria a pasta se não existir
115
+ execute_download_file(json_file.get("url", ""), file_name, app_file_path)
116
+ return os.path.join(app_file_path, file_name)
117
+
118
+ except KeyError as e:
119
+ print(f"Erro ao obter informações de download: {e}")
120
+ return ""
121
+
122
+ def execute_download_file(file_url, file_name, save_dir):
123
+ """Realiza o download de um arquivo."""
124
+ try:
125
+ response = requests.get(file_url, stream=True)
126
+ response.raise_for_status() # Lança exceção em caso de código de erro
127
+
128
+ save_file_path = os.path.join(save_dir, file_name)
129
+ with open(save_file_path, 'wb') as f:
130
+ for chunk in response.iter_content(chunk_size=BUFFER_SIZE):
131
+ f.write(chunk)
132
+
133
+ set_log("Arquivo " + file_name + " baixado!")
134
+
135
+ except requests.exceptions.RequestException as e:
136
+ set_log(f"Erro ao baixar arquivo: {e}")
137
+
138
+ def get_component_file_id(component_id):
139
+ """Retorna informações sobre o arquivo anexado no componente."""
140
+ url = get_api_url("file")
141
+ header = get_auth_header()
142
+
143
+ params = {
144
+ "componentFile": "true",
145
+ "Id_Component": component_id
146
+ }
147
+
148
+ response = requests.get(url, params=params, headers=header)
149
+
150
+ if response.status_code == 200:
151
+ return response.text # Retornando texto diretamente (assumindo que o formato se mantém)
152
+ else:
153
+ return ""
154
+
155
+ def get_test_script_scenario_file_id(dataset_run_id):
156
+ """Retorna informações do arquivo de script de teste associado a um dataset run."""
157
+ url = get_api_url("file")
158
+ header = get_auth_header()
159
+
160
+ params = {
161
+ "testScriptScenarioFileByDatasetRun": "true",
162
+ "idDatasetRun": dataset_run_id
163
+ }
164
+
165
+ response = requests.get(url, params=params, headers=header)
166
+
167
+ if response.status_code == 200:
168
+ return response.text
169
+ else:
170
+ return ""
proton/proton_git.py ADDED
@@ -0,0 +1,89 @@
1
+ import os
2
+ import subprocess
3
+ import threading
4
+
5
+ import requests
6
+ from proton.proton_client_parama import ProtonClientParam
7
+ from proton.proton_env import get_api_url, get_auth_header
8
+ from proton.proton_logs import set_log
9
+
10
+
11
+ def get_remote_repository(system_type):
12
+ """Obtém informações sobre um repositório remoto."""
13
+ url = get_api_url("git-repository")
14
+ header = get_auth_header()
15
+
16
+ params = {
17
+ "selectRemoteRepository": "true",
18
+ "System": system_type
19
+ }
20
+
21
+ response = requests.get(url, params=params, headers=header)
22
+
23
+ if response.status_code == 200:
24
+ return response.json()
25
+ else:
26
+ return None
27
+
28
+ def get_remote_repository_legacy(system_type):
29
+ """Obtém informações sobre um repositório remoto (versão legacy)."""
30
+ url = get_api_url("git-repository")
31
+ header = get_auth_header()
32
+
33
+ params = {
34
+ "selectRemoteRepository": "true",
35
+ "Type": system_type
36
+ }
37
+
38
+ response = requests.get(url, params=params, headers=header)
39
+
40
+ if response.status_code == 200:
41
+ return response.json()
42
+ else:
43
+ return None
44
+
45
+ def get_remote_header_hash(remote_authorization, repository, branch):
46
+ """Obtém o hash do cabeçalho de um repositório remoto."""
47
+ url = get_api_url("git-repository")
48
+ header = get_auth_header()
49
+
50
+ params = {
51
+ "selectRemoteHeaderHash": "true",
52
+ "Remote_Authorization": remote_authorization,
53
+ "Repository": repository,
54
+ "Branch": branch
55
+ }
56
+
57
+ response = requests.get(url, params=params, headers=header)
58
+
59
+ if response.status_code == 200:
60
+ data = response.json()
61
+ return data.get("sha", "")
62
+ else:
63
+ return ""
64
+
65
+ def clone_repository(owner_repo, branch, commit_hash, authorization):
66
+ """Clona um repositório Git."""
67
+ repo_folder = os.path.join(ProtonClientParam._repository_folder(), owner_repo, branch, commit_hash)
68
+ os.makedirs(repo_folder, exist_ok=True)
69
+
70
+ def run_git_clone(repo_folder, branch, authorization, owner_repo):
71
+ try:
72
+ url = f"https://{authorization}@github.com/{owner_repo}.git"
73
+ subprocess.run(
74
+ ["git", "clone", "--branch", branch, url],
75
+ cwd=repo_folder,
76
+ check=True, # Levanta exceção se o git retornar código de erro
77
+ capture_output=True, # Captura o output
78
+ text=True
79
+ )
80
+ except subprocess.CalledProcessError as e:
81
+ set_log(f"Erro ao clonar: {e.output}")
82
+
83
+ thread = threading.Thread(target=run_git_clone, args=(repo_folder, branch, authorization, owner_repo))
84
+ thread.start()
85
+
86
+ def get_local_repository_path(owner_repo, repository, branch, commit_hash):
87
+ """Retorna o caminho para um repositório local."""
88
+ repo_folder = os.path.join(ProtonClientParam._repository_folder(), owner_repo, branch, commit_hash, repository)
89
+ return repo_folder
proton/proton_logs.py ADDED
@@ -0,0 +1,53 @@
1
+ import traceback
2
+
3
+ import requests
4
+
5
+ from proton.proton_env import get_auth_header, get_host, get_id_dataset_run, is_proton_execution
6
+
7
+
8
+ def set_log(log_message, disable_date_time=False):
9
+ """Envia uma entrada de log"""
10
+
11
+ if is_proton_execution():
12
+ dataset_run_id = get_id_dataset_run()
13
+
14
+ if dataset_run_id is not None:
15
+ url = f"{get_host()}/dataset-run-log/{dataset_run_id}"
16
+
17
+ body = {
18
+ "idDatasetRun": dataset_run_id,
19
+ "log": log_message,
20
+ "disableDateTime": disable_date_time,
21
+ }
22
+
23
+ return requests.put(url, json=body, headers=get_auth_header())
24
+ else:
25
+ print(log_message)
26
+
27
+ return None
28
+
29
+
30
+ def set_error_log(error_message):
31
+ """Envia um log de erro estruturado para o Proton."""
32
+ if is_proton_execution():
33
+ dataset_run_id = get_id_dataset_run()
34
+
35
+ if dataset_run_id is not None:
36
+ url = f"{get_host()}/dataset-run/{dataset_run_id}/error"
37
+ body = {
38
+ "idDatasetRun": dataset_run_id,
39
+ "error": error_message,
40
+ }
41
+
42
+ return requests.put(url, json=body, headers=get_auth_header())
43
+ else:
44
+ print(error_message)
45
+
46
+ return None
47
+
48
+ # ATENÇÃO -----------------
49
+ def set_log_from_exception(e):
50
+ """Envia um log de exceção formatando o stack trace"""
51
+ stack_trace = "".join(traceback.format_exception(type(e), e, e.__traceback__))
52
+ for line in stack_trace.splitlines():
53
+ set_log(line)
@@ -0,0 +1,127 @@
1
+ import requests
2
+ from proton.proton_env import get_auth_header, get_host, get_id_dataset_run, is_proton_execution
3
+
4
+
5
+ def _get_dataset_run_url():
6
+ return f"{get_host()}/dataset-run/{get_id_dataset_run()}"
7
+
8
+
9
+ def set_proton_value(param, value):
10
+ """
11
+ Insere um valor de output (saída) no Proton.
12
+
13
+ Args:
14
+ param (str): O nome do parâmetro, deve ser idêntico ao nome definido no Proton.
15
+ value (str ou int): O valor a ser inserido no parâmetro de output.
16
+ """
17
+ if is_proton_execution():
18
+ params = {
19
+ "parameter": param
20
+ }
21
+
22
+ params["value"] = str(value)
23
+
24
+ # Proton 5: a rota e "/set-value"; "/setValue" (Proton 4) responde 404 e o valor nunca era gravado.
25
+ response = requests.put(f"{_get_dataset_run_url()}/set-value", params=params, headers=get_auth_header())
26
+
27
+ if response.status_code != 200:
28
+ print(f"[proton] set_proton_value({param!r}) -> HTTP {response.status_code}: {response.text[:300]}")
29
+
30
+ return response.status_code
31
+
32
+ def get_proton_value(param):
33
+ """Busca valor de parâmetro do Proton"""
34
+ if not is_proton_execution():
35
+ return ""
36
+
37
+ params = {
38
+ "parameter": param
39
+ }
40
+
41
+ response = requests.get(f"{_get_dataset_run_url()}/get-value", params=params, headers=get_auth_header())
42
+
43
+ if response.status_code == 200:
44
+ data = response.json()
45
+
46
+ return data.get("value", "") if data.get("value", "") else ''
47
+ else:
48
+ return ""
49
+
50
+ def get_proton_all_scenario_parameters():
51
+ """
52
+ Retorna todos os parâmetros de cenário do Proton.
53
+
54
+ Retorna:
55
+ dict: Um dicionário contendo todos os parâmetros de cenário.
56
+ """
57
+ host = get_host()
58
+ dataset_run_id = get_id_dataset_run()
59
+ header = get_auth_header()
60
+
61
+ url = f"{host}/dataset-run/get-all-params/{dataset_run_id}"
62
+
63
+ response = requests.get(url, headers=header)
64
+
65
+ if response.status_code == 200:
66
+ return response.json() # Retorna os parâmetros de cenário em formato JSON
67
+ else:
68
+ return {}
69
+
70
+ def get_proton_all_component_parameters():
71
+ """
72
+ Retorna todos os parâmetros de componente do Proton.
73
+
74
+ Retorna:
75
+ dict: Um dicionário contendo todos os parâmetros de componente.
76
+ """
77
+ host = get_host()
78
+ dataset_run_id = get_id_dataset_run()
79
+ header = get_auth_header()
80
+
81
+ url = f"{host}/dataset-run/get-component-params/{dataset_run_id}"
82
+
83
+ response = requests.get(url, headers=header)
84
+
85
+ if response.status_code == 200:
86
+ return response.json() # Retorna os parâmetros de componente em formato JSON
87
+ else:
88
+ return {}
89
+
90
+ def get_proton_output_parameter_list(output_parameter_name=None):
91
+ """
92
+ Retorna as ocorrências de um parâmetro de output (saída) específico ou
93
+ de todos os parâmetros de output presentes na ID do Dataset Run.
94
+
95
+ Args:
96
+ output_parameter_name (str, opcional): Nome do parâmetro de output a ser filtrado.
97
+ Se não for fornecido, retorna todos os parâmetros.
98
+
99
+ Retorna:
100
+ list: Uma lista com as ocorrências dos parâmetros de output no formato JSON.
101
+ Caso não encontre parâmetros ou ocorra algum erro, retorna uma lista vazia.
102
+ """
103
+
104
+ host = get_host() # Lembre-se de definir a lógica para obter o 'host'
105
+ url = f"{host}/results-run"
106
+ header = get_auth_header()
107
+
108
+
109
+ params = {
110
+ "idDatasetRun": get_id_dataset_run()
111
+ }
112
+ if output_parameter_name:
113
+ params["parameter"] = output_parameter_name
114
+
115
+ response = requests.get(url, params=params, headers=header)
116
+
117
+ if response.status_code == 200:
118
+ return response.json() # Assumindo que a resposta é em formato JSON
119
+ else:
120
+ return [] # Indica que não foram encontrados outputs ou houve erro
121
+
122
+ # Versão sobrecarregada do método, chamando a função principal com output_parameter_name=""
123
+ def get_proton_output_parameter_list_all():
124
+ """
125
+ Retorna as ocorrências de todos os parâmetros de output presentes na ID do Dataset Run.
126
+ """
127
+ return get_proton_output_parameter_list()
proton/run_status.py ADDED
@@ -0,0 +1,9 @@
1
+ from enum import Enum
2
+
3
+ class RunStatus(Enum):
4
+ RUNNING = "Running"
5
+ PASSED = "Passed"
6
+ FAILED = "Failed"
7
+ FAILED_DATA = "Failed (Dados)"
8
+ FAILED_ENVIRONMENT = "Failed (Ambiente)"
9
+ IN_PROCESS = "In Process"
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.5
2
+ Name: protoncloud-sdk
3
+ Version: 5.0.0
4
+ Summary: Cliente Python da plataforma Proton: execução de automações, logs, parâmetros e recursos.
5
+ Project-URL: Homepage, https://protoncloud.com.br
6
+ Author: Atomic Solutions
7
+ License: Proprietary
8
+ License-File: LICENSE
9
+ Keywords: automation,proton,qa,rpa,testing
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: Other/Proprietary License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: requests>=2.31
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Proton Lib (Python)
23
+
24
+ Cliente Python da plataforma [Proton](https://protoncloud.com.br). A automação usa
25
+ esta biblioteca para reportar ao Proton o progresso, o status, os logs, os
26
+ parâmetros e os recursos de cada execução.
27
+
28
+ Distribuição: `protoncloud-sdk`. Import: `proton`.
29
+
30
+ ```python
31
+ from proton.proton_automation import start_component, end_component, update_run_status
32
+ from proton.run_status import RunStatus
33
+ ```
34
+
35
+ ## Instalação
36
+
37
+ ```bash
38
+ pip install protoncloud-sdk
39
+ ```
40
+
41
+ ou, com [uv](https://docs.astral.sh/uv/):
42
+
43
+ ```bash
44
+ uv add protoncloud-sdk
45
+ ```
46
+
47
+ ## Configuração
48
+
49
+ Host e token são resolvidos nesta ordem — a mesma convenção das bibliotecas Java
50
+ do Proton:
51
+
52
+ 1. **Variável de ambiente** — `PROTON_HOST`, `PROTON_TOKEN`
53
+ 2. **`proton.ini`** no diretório de execução
54
+
55
+ Quando a automação roda pelo Proton Runner, ele injeta `PROTON_HOST` e
56
+ `PROTON_TOKEN` no processo, apontando para o servidor de onde a execução nasceu.
57
+ É isso que permite o mesmo projeto rodar em qualquer ambiente sem editar arquivo
58
+ — e sem guardar token no repositório.
59
+
60
+ O arquivo continua válido e é a única fonte quando se roda a automação fora do
61
+ runner:
62
+
63
+ ```ini
64
+ [server]
65
+ hostname = https://app.protoncloud.com.br/api
66
+ token = <token>
67
+
68
+ [reports]
69
+ video_record = false
70
+ video_upload = true
71
+ ```
72
+
73
+ O token pode vir com ou sem o prefixo `Bearer `; a biblioteca normaliza e nunca
74
+ o duplica.
75
+
76
+ Não versione o `proton.ini` com o token preenchido: prefira deixá-lo fora do
77
+ controle de versão e deixar o runner fornecer as credenciais.
78
+
79
+ ### Fixando o ambiente do projeto
80
+
81
+ Para um teste apontado a outro servidor, ou um projeto que precise ignorar o que
82
+ o runner injeta:
83
+
84
+ ```ini
85
+ [server]
86
+ config_precedence = file
87
+ ```
88
+
89
+ Sem isso, quando o valor injetado difere do arquivo, a biblioteca imprime uma
90
+ linha dizendo qual venceu. O valor do token nunca é impresso, porque a saída da
91
+ automação sobe como log da execução no Proton.
92
+
93
+ ## Contexto da execução
94
+
95
+ | variável | origem | uso |
96
+ |---|---|---|
97
+ | `idDatasetRun` | runner | identifica a execução; sem ela a biblioteca fica inerte (`is_proton_execution()` é `False`) |
98
+ | `PROTON_HOST` | runner ou `proton.ini` | base da API |
99
+ | `PROTON_TOKEN` | runner ou `proton.ini` | autenticação |
100
+ | `idRunProgress` | definida pela própria biblioteca em `start_component()` | componente em andamento |
101
+
102
+ Fora de uma execução do Proton, `is_proton_execution()` é `False` e as chamadas
103
+ viram no-op — o mesmo teste roda localmente sem falar com o servidor.
104
+
105
+ ## Migrando de uma cópia local da pasta `proton/`
106
+
107
+ Projetos que carregam esta biblioteca como uma pasta `proton/` copiada migram em
108
+ dois passos:
109
+
110
+ 1. adicione `protoncloud-sdk` às dependências;
111
+ 2. **apague a pasta `proton/` do projeto.**
112
+
113
+ O segundo passo não é opcional: a pasta local tem precedência sobre o pacote
114
+ instalado, e enquanto ela existir o projeto continua executando a cópia antiga.
@@ -0,0 +1,13 @@
1
+ proton/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ proton/proton_automation.py,sha256=7QFahZuiEvqP_8054yE5Yb5ndwqY0xDSAVv6zlhhrZI,8366
3
+ proton/proton_client_parama.py,sha256=4PDY3eUl9a_BfjACvAskmT5RU1D4fKhppN57Md3qS1E,1605
4
+ proton/proton_env.py,sha256=0HyTV_Hi9Dp-I6w0EAwLyBg7FSVf_HOzzoDkPETu8SI,6296
5
+ proton/proton_files_and_resources.py,sha256=sZnjLQ0UEh20ggCONS23xBMUq9xxkAMNVgRB8D3CtfI,5802
6
+ proton/proton_git.py,sha256=RZr2iRmySlF4HIE450GFphwxlBctrLeD_eNgNf01H1E,2904
7
+ proton/proton_logs.py,sha256=sbvjTyebUQYJ6OTQckjDDfbLtvLlAPoMEj9l3-zH2Tw,1511
8
+ proton/proton_parameter.py,sha256=I3RflQBM4njYkEt38PHDR8h3JJz_s8nYvSbfJM4lLvo,4113
9
+ proton/run_status.py,sha256=m5lD3a0nVOvmMvoxxb6Ch04Pno370J2xGuvg0fx6XNk,223
10
+ protoncloud_sdk-5.0.0.dist-info/METADATA,sha256=mfVlD5Nckz8EXEWqCX3iwXo3Onr81SSVi3q7wfn4tDU,3721
11
+ protoncloud_sdk-5.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
12
+ protoncloud_sdk-5.0.0.dist-info/licenses/LICENSE,sha256=FQ6Qm5N4ElW76McTRb-CEyjUAHvWkKOYllXmix3SgDw,530
13
+ protoncloud_sdk-5.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,12 @@
1
+ Copyright (c) Atomic Solutions. Todos os direitos reservados.
2
+
3
+ Este software é proprietário. O uso é permitido exclusivamente em conjunto com
4
+ a plataforma Proton, por licenciados da Atomic Solutions e nos termos do
5
+ contrato firmado com a empresa.
6
+
7
+ Não é concedida permissão para redistribuir, sublicenciar, vender ou criar
8
+ trabalhos derivados a partir deste software sem autorização prévia por escrito
9
+ da Atomic Solutions.
10
+
11
+ O SOFTWARE É FORNECIDO "COMO ESTÁ", SEM GARANTIA DE QUALQUER NATUREZA, EXPRESSA
12
+ OU IMPLÍCITA.