protoncloud-sdk 5.3.1__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.1 → protoncloud_sdk-5.3.2}/PKG-INFO +1 -1
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/pyproject.toml +1 -1
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_files_and_resources.py +66 -28
- protoncloud_sdk-5.3.2/tests/test_download_do_app.py +92 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/.gitignore +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/LICENSE +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/README.md +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/__init__.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/component_runner.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_automation.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_client_parama.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_env.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_environment.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_git.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_logs.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_parameter.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/run_status.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/tests/test_component_runner.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/tests/test_dados_da_execucao.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/tests/test_proton_env.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/tests/test_proton_environment.py +0 -0
- {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/tests/test_proton_parameter.py +0 -0
- {protoncloud_sdk-5.3.1 → 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"
|
|
@@ -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")
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|