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.
Files changed (23) hide show
  1. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/PKG-INFO +1 -1
  2. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/pyproject.toml +1 -1
  3. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/component_runner.py +4 -0
  4. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_files_and_resources.py +66 -28
  5. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_component_runner.py +11 -0
  6. protoncloud_sdk-5.3.2/tests/test_download_do_app.py +92 -0
  7. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/.gitignore +0 -0
  8. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/LICENSE +0 -0
  9. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/README.md +0 -0
  10. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/__init__.py +0 -0
  11. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_automation.py +0 -0
  12. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_client_parama.py +0 -0
  13. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_env.py +0 -0
  14. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_environment.py +0 -0
  15. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_git.py +0 -0
  16. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_logs.py +0 -0
  17. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/proton_parameter.py +0 -0
  18. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/src/proton/run_status.py +0 -0
  19. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_dados_da_execucao.py +0 -0
  20. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_proton_env.py +0 -0
  21. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_proton_environment.py +0 -0
  22. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_proton_parameter.py +0 -0
  23. {protoncloud_sdk-5.3.0 → protoncloud_sdk-5.3.2}/tests/test_run_status.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: protoncloud-sdk
3
- Version: 5.3.0
3
+ Version: 5.3.2
4
4
  Summary: Proton Cloud SDK para Python: execução de automações, ambientes, logs, parâmetros e recursos.
5
5
  Project-URL: Homepage, https://protoncloud.com.br
6
6
  Author: Atomic Solutions
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "protoncloud-sdk"
7
- version = "5.3.0"
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
- """Retorna a URL assinada para um arquivo."""
91
- url = get_api_url("file-system")
92
- header = get_auth_header()
90
+ """
91
+ Retorna o link assinado do arquivo de uma entidade do Proton.
93
92
 
94
- params = {
95
- "entity": entity,
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
- response = requests.get(url, params=params, headers=header)
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
- else:
104
- return {}
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
- """Baixa uma aplicação mobile e retorna o caminho para o arquivo."""
108
- json_file = get_signed_url_file("Mobile_App", entity_id)
116
+ """
117
+ Baixa o arquivo de um app cadastrado em Aplicativos Mobile do Proton.
109
118
 
110
- try:
111
- file_name = json_file.get("fileName", "")
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
- 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)
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
- except KeyError as e:
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
- """Realiza o download de um arquivo."""
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
- response = requests.get(file_url, stream=True)
126
- response.raise_for_status() # Lança exceção em caso de código de erro
157
+ with requests.get(file_url, stream=True, timeout=60) as response:
158
+ response.raise_for_status()
127
159
 
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)
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