protoncloud-sdk 5.3.0__tar.gz → 5.3.2__tar.gz
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.
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/PKG-INFO +1 -1
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/pyproject.toml +1 -1
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/component_runner.py +4 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_files_and_resources.py +66 -28
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_component_runner.py +11 -0
- protoncloud_sdk-5.3.2/tests/test_download_do_app.py +92 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/.gitignore +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/LICENSE +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/README.md +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/__init__.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_automation.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_client_parama.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_env.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_environment.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_git.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_logs.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_parameter.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/run_status.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_dados_da_execucao.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_proton_env.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_proton_environment.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_proton_parameter.py +0 -0
- {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_run_status.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "protoncloud-sdk"
|
|
7
|
-
version = "5.3.
|
|
7
|
+
version = "5.3.2"
|
|
8
8
|
description = "Proton Cloud SDK para Python: execução de automações, ambientes, logs, parâmetros e recursos."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -125,6 +125,10 @@ def run_components(resolve: Localizador, *, evidence_dir: Optional[str] = "evide
|
|
|
125
125
|
return 0
|
|
126
126
|
raise ProtonRunError("A execução não tem passo pendente: nada para rodar.")
|
|
127
127
|
|
|
128
|
+
# A execução sai de Loading (o runner preparando o projeto) para Running quando o
|
|
129
|
+
# primeiro passo vai rodar, como os TestProtonScript copiados faziam.
|
|
130
|
+
proton_automation.update_run_status(RunStatus.RUNNING)
|
|
131
|
+
|
|
128
132
|
while True:
|
|
129
133
|
nome = proton_automation.get_current_component_name()
|
|
130
134
|
|
|
@@ -87,54 +87,92 @@ def get_mobile_app(mobile_app_id):
|
|
|
87
87
|
return {}
|
|
88
88
|
|
|
89
89
|
def get_signed_url_file(entity, entity_id):
|
|
90
|
-
"""
|
|
91
|
-
|
|
92
|
-
header = get_auth_header()
|
|
90
|
+
"""
|
|
91
|
+
Retorna o link assinado do arquivo de uma entidade do Proton.
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"idEntity": entity_id
|
|
97
|
-
}
|
|
93
|
+
Usa a rota `GET /file-system/download` do Proton 5, com a entidade em minúsculas
|
|
94
|
+
(por exemplo, `mobile_app`).
|
|
98
95
|
|
|
99
|
-
|
|
96
|
+
Returns:
|
|
97
|
+
dict: `{"id", "url", "fileName"}`, ou `{}` quando o arquivo não existe ou o
|
|
98
|
+
Proton recusa.
|
|
99
|
+
"""
|
|
100
|
+
url = get_api_url("file-system/download")
|
|
101
|
+
params = {"entity": str(entity).strip().lower(), "idEntity": entity_id}
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
response = requests.get(url, params=params, headers=get_auth_header(), timeout=60)
|
|
105
|
+
except requests.exceptions.RequestException as e:
|
|
106
|
+
set_log(f"Erro ao pedir o link do arquivo ({params['entity']} {entity_id}): {e}")
|
|
107
|
+
return {}
|
|
100
108
|
|
|
101
109
|
if response.status_code == 200:
|
|
102
110
|
return response.json()
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
|
|
112
|
+
set_log(f"O Proton não entregou o arquivo ({params['entity']} {entity_id}). Status: {response.status_code}")
|
|
113
|
+
return {}
|
|
105
114
|
|
|
106
115
|
def download_mobile_app(entity_id):
|
|
107
|
-
"""
|
|
108
|
-
|
|
116
|
+
"""
|
|
117
|
+
Baixa o arquivo de um app cadastrado em Aplicativos Mobile do Proton.
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
app_file_path = ProtonClientParam._app_folder() + os.sep
|
|
119
|
+
O arquivo vai para `~/.proton-client/app/<id do app>/`, com o nome e a extensão
|
|
120
|
+
do cadastro (o Appium exige a extensão `.apk`).
|
|
113
121
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
Args:
|
|
123
|
+
entity_id: O id do app no Proton (o campo `app` da execução).
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
str: O caminho do arquivo baixado, ou `""` quando não foi possível baixar.
|
|
127
|
+
"""
|
|
128
|
+
json_file = get_signed_url_file("mobile_app", entity_id)
|
|
129
|
+
file_url = json_file.get("url", "")
|
|
130
|
+
file_name = os.path.basename(json_file.get("fileName", "") or "")
|
|
131
|
+
|
|
132
|
+
if not file_url or not file_name:
|
|
133
|
+
return ""
|
|
134
|
+
|
|
135
|
+
app_file_path = os.path.join(ProtonClientParam._app_folder(), str(entity_id))
|
|
136
|
+
os.makedirs(app_file_path, exist_ok=True)
|
|
117
137
|
|
|
118
|
-
|
|
119
|
-
print(f"Erro ao obter informações de download: {e}")
|
|
138
|
+
if not execute_download_file(file_url, file_name, app_file_path):
|
|
120
139
|
return ""
|
|
121
140
|
|
|
141
|
+
return os.path.join(app_file_path, file_name)
|
|
142
|
+
|
|
122
143
|
def execute_download_file(file_url, file_name, save_dir):
|
|
123
|
-
"""
|
|
144
|
+
"""
|
|
145
|
+
Baixa um arquivo para a pasta indicada.
|
|
146
|
+
|
|
147
|
+
Grava num arquivo temporário e só então troca pelo definitivo: duas execuções em
|
|
148
|
+
paralelo com o mesmo app não leem um arquivo pela metade.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
bool: True quando o arquivo foi baixado.
|
|
152
|
+
"""
|
|
153
|
+
save_file_path = os.path.join(save_dir, file_name)
|
|
154
|
+
temp_file_path = f"{save_file_path}.{os.getpid()}.part"
|
|
155
|
+
|
|
124
156
|
try:
|
|
125
|
-
|
|
126
|
-
|
|
157
|
+
with requests.get(file_url, stream=True, timeout=60) as response:
|
|
158
|
+
response.raise_for_status()
|
|
127
159
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
f.write(chunk)
|
|
160
|
+
with open(temp_file_path, 'wb') as f:
|
|
161
|
+
for chunk in response.iter_content(chunk_size=BUFFER_SIZE):
|
|
162
|
+
f.write(chunk)
|
|
132
163
|
|
|
164
|
+
os.replace(temp_file_path, save_file_path)
|
|
133
165
|
set_log("Arquivo " + file_name + " baixado!")
|
|
166
|
+
return True
|
|
134
167
|
|
|
135
|
-
except requests.exceptions.RequestException as e:
|
|
168
|
+
except (requests.exceptions.RequestException, OSError) as e:
|
|
136
169
|
set_log(f"Erro ao baixar arquivo: {e}")
|
|
137
170
|
|
|
171
|
+
if os.path.exists(temp_file_path):
|
|
172
|
+
os.remove(temp_file_path)
|
|
173
|
+
|
|
174
|
+
return False
|
|
175
|
+
|
|
138
176
|
def get_component_file_id(component_id):
|
|
139
177
|
"""Retorna informações sobre o arquivo anexado no componente."""
|
|
140
178
|
url = get_api_url("file")
|
|
@@ -22,6 +22,7 @@ class ProtonFalso:
|
|
|
22
22
|
self.aberto = None
|
|
23
23
|
self.status = "Running"
|
|
24
24
|
self.chamadas = []
|
|
25
|
+
self.eventos = []
|
|
25
26
|
self.saidas = []
|
|
26
27
|
self.logs = []
|
|
27
28
|
self.erros = []
|
|
@@ -43,6 +44,7 @@ class ProtonFalso:
|
|
|
43
44
|
|
|
44
45
|
def iniciar(self):
|
|
45
46
|
self.chamadas.append("start")
|
|
47
|
+
self.eventos.append("start")
|
|
46
48
|
passo = self._pendente()
|
|
47
49
|
|
|
48
50
|
if self.inicio_falha or passo is None:
|
|
@@ -73,6 +75,7 @@ class ProtonFalso:
|
|
|
73
75
|
|
|
74
76
|
def atualizar_status(self, status):
|
|
75
77
|
self.status = status.value
|
|
78
|
+
self.eventos.append(f"status {status.value}")
|
|
76
79
|
|
|
77
80
|
|
|
78
81
|
@pytest.fixture
|
|
@@ -126,6 +129,14 @@ def test_roda_os_passos_na_ordem_com_os_parametros_de_cada_um(proton):
|
|
|
126
129
|
assert falso.status == RunStatus.PASSED.value
|
|
127
130
|
|
|
128
131
|
|
|
132
|
+
def test_marca_running_antes_do_primeiro_passo_e_passed_no_fim(proton):
|
|
133
|
+
falso = proton([{"nome": "FazerLogin", "ordem": 1}, {"nome": "FinalizarCompra", "ordem": 2}])
|
|
134
|
+
|
|
135
|
+
run_components(lambda nome: lambda params: None)
|
|
136
|
+
|
|
137
|
+
assert falso.eventos == ["status Running", "start", "start", "status Passed"]
|
|
138
|
+
|
|
139
|
+
|
|
129
140
|
def test_o_mesmo_componente_duas_vezes_recebe_os_parametros_da_sua_posicao(proton):
|
|
130
141
|
proton([
|
|
131
142
|
{"nome": "AdicionarAoCarrinho", "ordem": 1, "parametros": {"in_produto": "Mochila"}},
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Download do app cadastrado em Aplicativos Mobile: a SDK pede o link assinado pela rota
|
|
3
|
+
do Proton 5 (`GET /file-system/download?entity=mobile_app&idEntity=<app>`) e baixa o
|
|
4
|
+
arquivo para `~/.proton-client/app/<app>/`, com o nome e a extensão do cadastro.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from proton import proton_files_and_resources as arquivos
|
|
13
|
+
from proton.proton_client_parama import ProtonClientParam
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _Resposta:
|
|
17
|
+
def __init__(self, status_code=200, corpo=None, conteudo=b""):
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self._corpo = corpo or {}
|
|
20
|
+
self._conteudo = conteudo
|
|
21
|
+
|
|
22
|
+
def json(self):
|
|
23
|
+
return self._corpo
|
|
24
|
+
|
|
25
|
+
def raise_for_status(self):
|
|
26
|
+
if self.status_code >= 400:
|
|
27
|
+
raise requests.exceptions.HTTPError(f"{self.status_code}")
|
|
28
|
+
|
|
29
|
+
def iter_content(self, chunk_size):
|
|
30
|
+
yield self._conteudo
|
|
31
|
+
|
|
32
|
+
def __enter__(self):
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def __exit__(self, *args):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def proton(monkeypatch, tmp_path):
|
|
41
|
+
monkeypatch.setenv("PROTON_HOST", "http://localhost:3333/api")
|
|
42
|
+
monkeypatch.setenv("PROTON_TOKEN", "proton_run_teste")
|
|
43
|
+
monkeypatch.setattr(ProtonClientParam, "_app_folder", classmethod(lambda cls: str(tmp_path / "app")))
|
|
44
|
+
|
|
45
|
+
estado = {"link": _Resposta(200, {"id": 7, "url": "https://s3/assinado", "fileName": "mda-2.3.0.apk"}),
|
|
46
|
+
"arquivo": _Resposta(200, conteudo=b"APK"), "pedidos": []}
|
|
47
|
+
|
|
48
|
+
def get(url, params=None, headers=None, stream=False, timeout=None):
|
|
49
|
+
estado["pedidos"].append((url, params, headers))
|
|
50
|
+
return estado["arquivo"] if stream else estado["link"]
|
|
51
|
+
|
|
52
|
+
monkeypatch.setattr(arquivos.requests, "get", get)
|
|
53
|
+
return estado
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_pede_o_link_pela_rota_do_proton_5(proton):
|
|
57
|
+
arquivos.download_mobile_app("15")
|
|
58
|
+
|
|
59
|
+
url, params, headers = proton["pedidos"][0]
|
|
60
|
+
assert url == "http://localhost:3333/api/file-system/download"
|
|
61
|
+
assert params == {"entity": "mobile_app", "idEntity": "15"}
|
|
62
|
+
assert headers == {"Authorization": "Bearer proton_run_teste"}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_baixa_o_app_com_a_extensao_na_pasta_do_app(proton, tmp_path):
|
|
66
|
+
caminho = arquivos.download_mobile_app("15")
|
|
67
|
+
|
|
68
|
+
assert caminho == os.path.join(str(tmp_path / "app"), "15", "mda-2.3.0.apk")
|
|
69
|
+
with open(caminho, "rb") as f:
|
|
70
|
+
assert f.read() == b"APK"
|
|
71
|
+
assert os.listdir(tmp_path / "app" / "15") == ["mda-2.3.0.apk"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_proton_recusa_o_link_devolve_vazio(proton):
|
|
75
|
+
proton["link"] = _Resposta(403, {"message": "executionTokenOtherRun"})
|
|
76
|
+
|
|
77
|
+
assert arquivos.download_mobile_app("15") == ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_falha_no_download_devolve_vazio_e_nao_deixa_arquivo(proton, tmp_path):
|
|
81
|
+
proton["arquivo"] = _Resposta(404)
|
|
82
|
+
|
|
83
|
+
assert arquivos.download_mobile_app("15") == ""
|
|
84
|
+
assert os.listdir(tmp_path / "app" / "15") == []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_nome_do_arquivo_nao_sai_da_pasta_do_app(proton, tmp_path):
|
|
88
|
+
proton["link"] = _Resposta(200, {"id": 7, "url": "https://s3/assinado", "fileName": "../../fora.apk"})
|
|
89
|
+
|
|
90
|
+
caminho = arquivos.download_mobile_app("15")
|
|
91
|
+
|
|
92
|
+
assert caminho == os.path.join(str(tmp_path / "app"), "15", "fora.apk")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|