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.
Files changed (23) hide show
  1. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/PKG-INFO +1 -1
  2. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/pyproject.toml +1 -1
  3. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_files_and_resources.py +66 -28
  4. protoncloud_sdk-5.3.2/tests/test_download_do_app.py +92 -0
  5. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/.gitignore +0 -0
  6. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/LICENSE +0 -0
  7. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/README.md +0 -0
  8. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/__init__.py +0 -0
  9. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/component_runner.py +0 -0
  10. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_automation.py +0 -0
  11. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_client_parama.py +0 -0
  12. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_env.py +0 -0
  13. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_environment.py +0 -0
  14. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_git.py +0 -0
  15. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_logs.py +0 -0
  16. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/proton_parameter.py +0 -0
  17. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/src/proton/run_status.py +0 -0
  18. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/tests/test_component_runner.py +0 -0
  19. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/tests/test_dados_da_execucao.py +0 -0
  20. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/tests/test_proton_env.py +0 -0
  21. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/tests/test_proton_environment.py +0 -0
  22. {protoncloud_sdk-5.3.1 → protoncloud_sdk-5.3.2}/tests/test_proton_parameter.py +0 -0
  23. {protoncloud_sdk-5.3.1 → 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.1
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.1"
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
- """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")
@@ -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